 core/core.services.yml                             |  7 ++-
 .../DefaultExceptionHtmlSubscriber.php             |  5 +-
 .../EventSubscriber/FinishResponseSubscriber.php   | 55 +++++++++++++++++++++-
 .../Core/PageCache/ResponsePolicy/MaxAge.php       | 31 ++++++++++++
 .../Core/Render/MainContent/HtmlRenderer.php       |  9 ++++
 core/lib/Drupal/Core/Routing/AccessAwareRouter.php | 10 +++-
 .../Core/Routing/AccessAwareRouterInterface.php    |  5 ++
 .../modules/node/src/Tests/Views/FrontPageTest.php |  2 +-
 .../Tests/Core/Routing/AccessAwareRouterTest.php   | 23 +++++++--
 9 files changed, 137 insertions(+), 10 deletions(-)

diff --git a/core/core.services.yml b/core/core.services.yml
index 8ce9b11..7a1fbb0 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -199,6 +199,11 @@ services:
     tags:
       - { name: service_collector, tag: page_cache_response_policy, call: addPolicy}
     lazy: true
+  page_cache_max_age:
+    class: Drupal\Core\PageCache\ResponsePolicy\MaxAge
+    public: false
+    tags:
+      - { name: page_cache_response_policy }
   page_cache_kill_switch:
     class: Drupal\Core\PageCache\ResponsePolicy\KillSwitch
     tags:
@@ -942,7 +947,7 @@ services:
     class: Drupal\Core\EventSubscriber\FinishResponseSubscriber
     tags:
       - { name: event_subscriber }
-    arguments: ['@language_manager', '@config.factory', '@page_cache_request_policy', '@page_cache_response_policy']
+    arguments: ['@language_manager', '@config.factory', '@page_cache_request_policy', '@page_cache_response_policy', '@cache_contexts']
   redirect_response_subscriber:
     class: Drupal\Core\EventSubscriber\RedirectResponseSubscriber
     arguments: ['@url_generator', '@router.request_context']
diff --git a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php
index a98bf57..5861e85 100644
--- a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core\EventSubscriber;
 
+use Drupal\Core\Routing\AccessAwareRouterInterface;
 use Drupal\Core\Url;
 use Drupal\Core\Utility\Error;
 use Psr\Log\LoggerInterface;
@@ -111,8 +112,10 @@ protected function makeSubrequest(GetResponseForExceptionEvent $event, $url, $st
       }
 
       try {
-        // Persist the 'exception' attribute to the subrequest.
+        // Persist the 'exception' and access result attributes to the
+        // subrequest.
         $sub_request->attributes->set('exception', $request->attributes->get('exception'));
+        $sub_request->attributes->set(AccessAwareRouterInterface::ACCESS_RESULT, $request->attributes->get(AccessAwareRouterInterface::ACCESS_RESULT));
 
         // Carry over the session to the subrequest.
         if ($session = $request->getSession()) {
diff --git a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
index 3c8bdf2..77efa31 100644
--- a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
@@ -8,11 +8,15 @@
 namespace Drupal\Core\EventSubscriber;
 
 use Drupal\Component\Datetime\DateTimePlus;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableInterface;
+use Drupal\Core\Cache\CacheContexts;
 use Drupal\Core\Config\Config;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Language\LanguageManagerInterface;
 use Drupal\Core\PageCache\RequestPolicyInterface;
 use Drupal\Core\PageCache\ResponsePolicyInterface;
+use Drupal\Core\Routing\AccessAwareRouterInterface;
 use Drupal\Core\Site\Settings;
 use Symfony\Component\HttpFoundation\BinaryFileResponse;
 use Symfony\Component\HttpFoundation\Request;
@@ -57,6 +61,13 @@ class FinishResponseSubscriber implements EventSubscriberInterface {
   protected $responsePolicy;
 
   /**
+   * The cache contexts service.
+   *
+   * @var \Drupal\Core\Cache\CacheContexts
+   */
+  protected $cacheContexts;
+
+  /**
    * Constructs a FinishResponseSubscriber object.
    *
    * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
@@ -67,12 +78,15 @@ class FinishResponseSubscriber implements EventSubscriberInterface {
    *   A policy rule determining the cacheability of a request.
    * @param \Drupal\Core\PageCache\ResponsePolicyInterface $response_policy
    *   A policy rule determining the cacheability of a response.
+   * @param \Drupal\Core\Cache\CacheContexts $cache_contexts
+   *   The cache contexts service.
    */
-  public function __construct(LanguageManagerInterface $language_manager, ConfigFactoryInterface $config_factory, RequestPolicyInterface $request_policy, ResponsePolicyInterface $response_policy) {
+  public function __construct(LanguageManagerInterface $language_manager, ConfigFactoryInterface $config_factory, RequestPolicyInterface $request_policy, ResponsePolicyInterface $response_policy, CacheContexts $cache_contexts) {
     $this->languageManager = $language_manager;
     $this->config = $config_factory->get('system.performance');
     $this->requestPolicy = $request_policy;
     $this->responsePolicy = $response_policy;
+    $this->cacheContexts = $cache_contexts;
   }
 
   /**
@@ -119,6 +133,12 @@ public function onRespond(FilterResponseEvent $event) {
       $response->headers->set($name, $value, FALSE);
     }
 
+    // Apply the request's access result cacheability metadata, if it has any.
+    $access_result = $request->attributes->get(AccessAwareRouterInterface::ACCESS_RESULT);
+    if ($access_result instanceof CacheableInterface) {
+      $this->updateDrupalCacheHeaders($response, $access_result);
+    }
+
     $is_cacheable = ($this->requestPolicy->check($request) === RequestPolicyInterface::ALLOW) && ($this->responsePolicy->check($response, $request) !== ResponsePolicyInterface::DENY);
 
     // Add headers necessary to specify whether the response should be cached by
@@ -139,6 +159,39 @@ public function onRespond(FilterResponseEvent $event) {
   }
 
   /**
+   * Updates Drupal's cache headers using the route's cacheable access result.
+   *
+   * @param Response $response
+   * @param CacheableInterface $cacheable_access_result
+   */
+  protected function updateDrupalCacheHeaders(Response $response, CacheableInterface $cacheable_access_result) {
+    // X-Drupal-Cache-Tags
+    $cache_tags = $cacheable_access_result->getCacheTags();
+    if ($response->headers->has('X-Drupal-Cache-Tags')) {
+      $existing_cache_tags = explode(' ', $response->headers->get('X-Drupal-Cache-Tags'));
+      $cache_tags = Cache::mergeTags($existing_cache_tags, $cache_tags);
+    }
+    $response->headers->set('X-Drupal-Cache-Tags', implode(' ', $cache_tags));
+
+    // X-Drupal-Cache-Contexts
+    $cache_contexts = $cacheable_access_result->getCacheContexts();
+    if ($response->headers->has('X-Drupal-Cache-Contexts')) {
+      $existing_cache_contexts = explode(' ', $response->headers->get('X-Drupal-Cache-Contexts'));
+      $cache_contexts = Cache::mergeTags($existing_cache_contexts, $cache_contexts);
+    }
+    $response->headers->set('X-Drupal-Cache-Contexts', implode(' ', $this->cacheContexts->optimizeTokens($cache_contexts)));
+
+    // Cache-Control: max-age
+    $max_age = Cache::mergeMaxAges($response->getMaxAge(), $cacheable_access_result->getCacheMaxAge());
+    $response->setMaxAge($max_age);
+
+    // @todo Remove once https://www.drupal.org/node/2444231 lands.
+    if (!$cacheable_access_result->isCacheable()) {
+      $response->setMaxAge(0);
+    }
+  }
+
+  /**
    * Determine whether the given response has a custom Cache-Control header.
    *
    * Upon construction, the ResponseHeaderBag is initialized with an empty
diff --git a/core/lib/Drupal/Core/PageCache/ResponsePolicy/MaxAge.php b/core/lib/Drupal/Core/PageCache/ResponsePolicy/MaxAge.php
new file mode 100644
index 0000000..590f5de
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/ResponsePolicy/MaxAge.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\ResponsePolicy\MaxAge.
+ */
+
+namespace Drupal\Core\PageCache\ResponsePolicy;
+
+use Drupal\Core\PageCache\ResponsePolicyInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * A policy denying caching of uncacheable (max-age zero) responses.
+ */
+class MaxAge implements ResponsePolicyInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function check(Response $response, Request $request) {
+    // We should be able to do a strict comparison with zero. But silly enough,
+    // \Symfony\Component\HttpFoundation\Response::getMaxAge() may also return
+    // NULL and even a negative value.
+    if ($response->getMaxAge() <= 0) {
+      return static::DENY;
+    }
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
index 249b47b..ecd712a 100644
--- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
+++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
@@ -11,6 +11,7 @@
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\CacheContexts;
+use Drupal\Core\Cache\CacheableInterface;
 use Drupal\Core\Controller\TitleResolverInterface;
 use Drupal\Core\Display\PageVariantInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
@@ -18,6 +19,7 @@
 use Drupal\Core\Render\Renderer;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Render\RenderEvents;
+use Drupal\Core\Routing\AccessAwareRouterInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Symfony\Component\DependencyInjection\ContainerAwareTrait;
 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@@ -155,6 +157,13 @@ public function renderResponse(array $main_content, Request $request, RouteMatch
       }
     }
 
+    // Merge the request's access result cacheability metadata, if it has any.
+    $access_result = $request->attributes->get(AccessAwareRouterInterface::ACCESS_RESULT);
+    if ($access_result instanceof CacheableInterface) {
+      $cache_contexts = Cache::mergeContexts($cache_contexts, $access_result->getCacheContexts());
+      $cache_tags = Cache::mergeTags($cache_tags, $access_result->getCacheTags());
+    }
+
     // Set the generator in the HTTP header.
     list($version) = explode('.', \Drupal::VERSION, 2);
 
diff --git a/core/lib/Drupal/Core/Routing/AccessAwareRouter.php b/core/lib/Drupal/Core/Routing/AccessAwareRouter.php
index 1cf937c..20b0ecf 100644
--- a/core/lib/Drupal/Core/Routing/AccessAwareRouter.php
+++ b/core/lib/Drupal/Core/Routing/AccessAwareRouter.php
@@ -101,7 +101,15 @@ public function matchRequest(Request $request) {
    *   The request to access check.
    */
   protected function checkAccess(Request $request) {
-    if (!$this->accessManager->checkRequest($request, $this->account)) {
+    // The cacheability (if any) of this request's access check result must be
+    // applied to the response.
+    $access_result = $this->accessManager->checkRequest($request, $this->account, TRUE);
+    // Allow a master request to set the access result for a subrequest: if an
+    // access result attribute is already set, don't overwrite it.
+    if (!$request->attributes->has(AccessAwareRouterInterface::ACCESS_RESULT)) {
+      $request->attributes->set(AccessAwareRouterInterface::ACCESS_RESULT, $access_result);
+    }
+    if (!$access_result->isAllowed()) {
       throw new AccessDeniedHttpException();
     }
   }
diff --git a/core/lib/Drupal/Core/Routing/AccessAwareRouterInterface.php b/core/lib/Drupal/Core/Routing/AccessAwareRouterInterface.php
index 34fa75a..b6cfe4a 100644
--- a/core/lib/Drupal/Core/Routing/AccessAwareRouterInterface.php
+++ b/core/lib/Drupal/Core/Routing/AccessAwareRouterInterface.php
@@ -16,6 +16,11 @@
 interface AccessAwareRouterInterface extends RouterInterface, RequestMatcherInterface {
 
   /**
+   * Attribute name of the access result for the request..
+   */
+  const ACCESS_RESULT = '_access_result';
+
+  /**
    * {@inheritdoc}
    *
    * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
diff --git a/core/modules/node/src/Tests/Views/FrontPageTest.php b/core/modules/node/src/Tests/Views/FrontPageTest.php
index ed6fd3d..a886fab 100644
--- a/core/modules/node/src/Tests/Views/FrontPageTest.php
+++ b/core/modules/node/src/Tests/Views/FrontPageTest.php
@@ -241,7 +241,7 @@ protected function assertFrontPageViewCacheTags($do_assert_views_caches) {
     $view = Views::getView('frontpage');
     $view->setDisplay('page_1');
 
-    $cache_contexts = ['user.node_grants:view', 'languages'];
+    $cache_contexts = ['user.node_grants:view', 'languages', 'user.roles'];
 
     // Test before there are any nodes.
     $empty_node_listing_cache_tags = [
diff --git a/core/tests/Drupal/Tests/Core/Routing/AccessAwareRouterTest.php b/core/tests/Drupal/Tests/Core/Routing/AccessAwareRouterTest.php
index 3e8676f..da58cd9 100644
--- a/core/tests/Drupal/Tests/Core/Routing/AccessAwareRouterTest.php
+++ b/core/tests/Drupal/Tests/Core/Routing/AccessAwareRouterTest.php
@@ -7,7 +7,9 @@
 
 namespace Drupal\Tests\Core\Routing;
 
+use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Routing\AccessAwareRouter;
+use Drupal\Core\Routing\AccessAwareRouterInterface;
 use Drupal\Tests\UnitTestCase;
 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
 use Symfony\Component\HttpFoundation\Request;
@@ -73,13 +75,18 @@ protected function setupRouter() {
   public function testMatchRequestAllowed() {
     $this->setupRouter();
     $request = new Request();
+    $access_result = AccessResult::allowed();
     $this->accessManager->expects($this->once())
       ->method('checkRequest')
       ->with($request)
-      ->will($this->returnValue(TRUE));
+      ->willReturn($access_result);
     $parameters = $this->router->matchRequest($request);
-    $this->assertSame($request->attributes->all(), array(RouteObjectInterface::ROUTE_OBJECT => $this->route));
-    $this->assertSame($parameters, array(RouteObjectInterface::ROUTE_OBJECT => $this->route));
+    $expected = [
+      RouteObjectInterface::ROUTE_OBJECT => $this->route,
+      AccessAwareRouterInterface::ACCESS_RESULT => $access_result,
+    ];
+    $this->assertSame($expected, $request->attributes->all());
+    $this->assertSame($expected, $parameters);
   }
 
   /**
@@ -90,11 +97,17 @@ public function testMatchRequestAllowed() {
   public function testMatchRequestDenied() {
     $this->setupRouter();
     $request = new Request();
+    $access_result = AccessResult::forbidden();
     $this->accessManager->expects($this->once())
       ->method('checkRequest')
       ->with($request)
-      ->will($this->returnValue(FALSE));
-    $this->router->matchRequest($request);
+      ->willReturn($access_result);
+    $parameters = $this->router->matchRequest($request);
+    $expected = [
+      AccessAwareRouterInterface::ACCESS_RESULT => $access_result,
+    ];
+    $this->assertSame($expected, $request->attributes->all());
+    $this->assertSame($expected, $parameters);
   }
 
   /**
