diff --git a/core/core.services.yml b/core/core.services.yml
index 8e203ed..d6d8b81 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -80,6 +80,19 @@ services:
     factory_method: get
     factory_service: cache_factory
     arguments: [discovery]
+  page_cache_request_policy:
+    class: Drupal\Core\PageCache\DefaultRequestPolicy
+    tags:
+      - { name: service_collector, tag: page_cache_request_policy_allow, call: allow}
+      - { name: service_collector, tag: page_cache_request_policy_deny, call: deny}
+  page_cache_response_policy:
+    class: Drupal\Core\PageCache\DefaultResponsePolicy
+    tags:
+      - { name: service_collector, tag: page_cache_response_policy_deny, call: deny }
+  page_cache_kill_switch:
+    class: Drupal\Core\PageCache\ResponsePolicyRule\KillSwitch
+    tags:
+      - { name: page_cache_response_policy_deny }
   config.manager:
     class: Drupal\Core\Config\ConfigManager
     arguments: ['@entity.manager', '@config.factory', '@config.typed', '@string_translation', '@config.storage']
@@ -581,7 +594,7 @@ services:
     class: Drupal\Core\EventSubscriber\FinishResponseSubscriber
     tags:
       - { name: event_subscriber }
-    arguments: ['@language_manager', '@config.factory']
+    arguments: ['@language_manager', '@config.factory', '@page_cache_request_policy', '@page_cache_response_policy']
     scope: request
   redirect_response_subscriber:
     class: Drupal\Core\EventSubscriber\RedirectResponseSubscriber
diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index d33d1ee..689026f 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -664,30 +664,6 @@ function drupal_page_get_cache(Request $request) {
 }
 
 /**
- * Determines the cacheability of the current page.
- *
- * Note: we do not serve cached pages to authenticated users, or to anonymous
- * users when $_SESSION is non-empty. $_SESSION may contain status messages
- * from a form submission, the contents of a shopping cart, or other user-
- * specific content that should not be cached and displayed to other users.
- *
- * @param $allow_caching
- *   Set to FALSE if you want to prevent this page to get cached.
- *
- * @return
- *   TRUE if the current page can be cached, FALSE otherwise.
- */
-function drupal_page_is_cacheable($allow_caching = NULL) {
-  $allow_caching_static = &drupal_static(__FUNCTION__, TRUE);
-  if (isset($allow_caching)) {
-    $allow_caching_static = $allow_caching;
-  }
-
-  return $allow_caching_static && ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD')
-    && !drupal_is_cli();
-}
-
-/**
  * Sets an HTTP response header for the current page.
  *
  * Note: When sending a Content-Type header, always include a 'charset' type,
@@ -1235,7 +1211,7 @@ function drupal_set_message($message = NULL, $type = 'status', $repeat = FALSE)
     }
 
     // Mark this page as being uncacheable.
-    drupal_page_is_cacheable(FALSE);
+    \Drupal::service('page_cache_kill_switch')->trigger();
   }
 
   // Messages not set when DB connection fails.
@@ -1582,8 +1558,26 @@ function _drupal_bootstrap_kernel() {
 
 /**
  * Attempts to serve a page from the cache.
+ *
+ * @todo The $disable parameter is a temporary workaround to make sure that
+ *   no cached page is delivered during a forced cache clear. This will become
+ *   obsolete as soon as deliver of cached pages is removed from the bootstrap.
+ * @see drupal_rebuild()
+ *
+ * @param bool $disable
+ *   (Optional) Temporary disable the delivery of cached pages.
  */
-function _drupal_bootstrap_page_cache() {
+function _drupal_bootstrap_page_cache($disable = NULL) {
+  static $disabled = FALSE;
+
+  if (isset($disable)) {
+    $disabled = $disable;
+    return;
+  }
+  elseif ($disabled) {
+    return;
+  }
+
   require_once __DIR__ . '/database.inc';
   // Check for a cache mode force from settings.php.
   if (Settings::get('page_cache_without_database')) {
@@ -1595,9 +1589,8 @@ function _drupal_bootstrap_page_cache() {
   }
 
   $request = \Drupal::request();
-  // If there is no session cookie and cache is enabled (or forced), try
-  // to serve a cached page.
-  if (!$request->cookies->has(session_name()) && $cache_enabled && drupal_page_is_cacheable()) {
+  $request_policy = \Drupal::service('page_cache_request_policy');
+  if ($cache_enabled && $request_policy->apply($request)) {
     // Get the page from the cache.
     $response = drupal_page_get_cache($request);
     // If there is a cached page, display it.
diff --git a/core/includes/utility.inc b/core/includes/utility.inc
index 6603166..5dce40a 100644
--- a/core/includes/utility.inc
+++ b/core/includes/utility.inc
@@ -46,8 +46,8 @@ function drupal_rebuild() {
   PhpStorageFactory::get('service_container')->deleteAll();
   PhpStorageFactory::get('twig')->deleteAll();
 
-  // Disable the page cache.
-  drupal_page_is_cacheable(FALSE);
+  // Disable delivery of cached pages.
+  _drupal_bootstrap_page_cache(TRUE);
 
   // Bootstrap up to where caches exist and clear them.
   drupal_bootstrap(DRUPAL_BOOTSTRAP_PAGE_CACHE);
@@ -58,6 +58,9 @@ function drupal_rebuild() {
   drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
   drupal_flush_all_caches();
 
+  // Restore delivery of cached pages.
+  _drupal_bootstrap_page_cache(FALSE);
+
   // Restore Drupal's error and exception handlers.
   // @see _drupal_bootstrap_configuration()
   set_error_handler('_drupal_error_handler');
diff --git a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
index 9c7f56c..8cef885 100644
--- a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
@@ -11,6 +11,8 @@
 use Drupal\Core\Config\ConfigFactory;
 use Drupal\Core\Language\Language;
 use Drupal\Core\Language\LanguageManager;
+use Drupal\Core\PageCache\RequestPolicyRuleInterface;
+use Drupal\Core\PageCache\ResponsePolicyRuleInterface;
 use Drupal\Core\Site\Settings;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
@@ -39,16 +41,36 @@ class FinishResponseSubscriber implements EventSubscriberInterface {
   protected $config;
 
   /**
+   * A policy rule determining the cacheability of a request.
+   *
+   * @var \Drupal\Core\PageCache\RequestPolicyRuleInterface
+   */
+  protected $requestPolicy;
+
+  /**
+   * A policy rule determining the cacheability of the response.
+   *
+   * @var \Drupal\Core\PageCache\ResponsePolicyRuleInterface
+   */
+  protected $responsePolicy;
+
+  /**
    * Constructs a FinishResponseSubscriber object.
    *
    * @param LanguageManager $language_manager
    *  The LanguageManager object for retrieving the correct language code.
    * @param \Drupal\Core\Config\ConfigFactory $config_factory
    *   A config factory for retrieving required config objects.
+   * @param \Drupal\Core\PageCache\RequestPolicyRuleInterface $request_policy
+   *   A policy rule determining the cacheability of a request.
+   * @param \Drupal\Core\PageCache\ResponsePolicyRuleInterface $response_policy
+   *   A policy rule determining the cacheability of a response.
    */
-  public function __construct(LanguageManager $language_manager, ConfigFactory $config_factory) {
+  public function __construct(LanguageManager $language_manager, ConfigFactory $config_factory, RequestPolicyRuleInterface $request_policy, ResponsePolicyRuleInterface $response_policy) {
     $this->languageManager = $language_manager;
     $this->config = $config_factory->get('system.performance');
+    $this->requestPolicy = $request_policy;
+    $this->responsePolicy = $response_policy;
   }
 
   /**
@@ -81,18 +103,19 @@ public function onRespond(FilterResponseEvent $event) {
       $response->headers->set($name, $value, FALSE);
     }
 
-    $is_cacheable = drupal_page_is_cacheable();
+    $is_cacheable = $this->requestPolicy->apply($request) && $this->responsePolicy->apply($response, $request);
+    $is_customized = $this->isCacheControlCustomized($response);
+    $is_configured = $this->config->get('cache.page.max_age') > 0;
+    $response_demands_caching = $is_configured || $is_customized;
 
     // Add headers necessary to specify whether the response should be cached by
     // proxies and/or the browser.
-    if ($is_cacheable && $this->config->get('cache.page.max_age') > 0) {
-      if (!$this->isCacheControlCustomized($response)) {
-        $this->setResponseCacheable($response, $request);
-      }
-    }
-    else {
+    if (!$is_cacheable || !$response_demands_caching) {
       $this->setResponseNotCacheable($response, $request);
     }
+    elseif ($is_configured && !$is_customized) {
+      $this->setResponseCacheable($response, $request);
+    }
 
     // Store the response in the internal page cache.
     if ($is_cacheable && $this->config->get('cache.page.use_internal')) {
diff --git a/core/lib/Drupal/Core/PageCache/DefaultRequestPolicy.php b/core/lib/Drupal/Core/PageCache/DefaultRequestPolicy.php
new file mode 100644
index 0000000..90b0e50
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/DefaultRequestPolicy.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\DefaultRequestPolicy.
+ */
+
+namespace Drupal\Core\PageCache;
+
+/**
+ * The default request policy.
+ */
+class DefaultRequestPolicy extends RequestPolicy {
+
+  /**
+   * Construct the default request policy.
+   */
+  public function __construct() {
+    $this->deny(new RequestPolicyRule\CommandLineOrUnsafeMethod());
+    $this->allow(new RequestPolicyRule\NoSessionOpen());
+  }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/DefaultResponsePolicy.php b/core/lib/Drupal/Core/PageCache/DefaultResponsePolicy.php
new file mode 100644
index 0000000..64b3008
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/DefaultResponsePolicy.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\DefaultResponsePolicy.
+ */
+
+namespace Drupal\Core\PageCache;
+
+/**
+ * Default response policy.
+ */
+class DefaultResponsePolicy extends ResponsePolicy {
+
+  /**
+   * Construct the default response policy.
+   */
+  public function __construct() {
+    $this->deny(new ResponsePolicyRule\IsBinaryFileResponse());
+    $this->deny(new ResponsePolicyRule\IsStreamedResponse());
+  }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/RequestPolicy.php b/core/lib/Drupal/Core/PageCache/RequestPolicy.php
new file mode 100644
index 0000000..c4fa1d0
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/RequestPolicy.php
@@ -0,0 +1,80 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\RequestPolicy.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * A policy rule implementing an allow-deny ACL-model.
+ *
+ * @todo: detailed docs
+ */
+class RequestPolicy implements RequestPolicyRuleInterface {
+
+  /**
+   * A set of policy rules representing the deny-chain.
+   *
+   * @var \Drupal\Core\PageCache\RequestPolicyRuleInterface[]
+   */
+  protected $deny;
+
+  /**
+   * A set of policy rules representing the allow-chain.
+   *
+   * @var \Drupal\Core\PageCache\RequestPolicyRuleInterface[]
+   */
+  protected $allow;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function apply(Request $request) {
+    foreach ($this->deny as $rule) {
+      if ($rule->apply($request)) {
+        return FALSE;
+      }
+    }
+
+    foreach ($this->allow as $rule) {
+      if ($rule->apply($request)) {
+        return TRUE;
+      }
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * Add one policy rule to the deny-chain.
+   *
+   * @param \Drupal\Core\PageCache\RequestPolicyRuleInterface $rule
+   *   The rules to add to the deny-chain.
+   *
+   * @return \Drupal\Core\PageCache\RequestPolicy
+   *   This policy instance.
+   */
+  public function deny(RequestPolicyRuleInterface $rule) {
+    $this->deny[spl_object_hash($rule)] = $rule;
+    return $this;
+  }
+
+  /**
+   * Add one policy rule to the allow-chain.
+   *
+   * @param \Drupal\Core\PageCache\RequestPolicyRuleInterface $rule
+   *   The rules to add to the deny-chain.
+   *
+   * @return \Drupal\Core\PageCache\RequestPolicy
+   *   This policy instance.
+   */
+  public function allow(RequestPolicyRuleInterface $rule) {
+    $this->allow[spl_object_hash($rule)] = $rule;
+    return $this;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/RequestPolicyRule/CommandLineOrUnsafeMethod.php b/core/lib/Drupal/Core/PageCache/RequestPolicyRule/CommandLineOrUnsafeMethod.php
new file mode 100644
index 0000000..8d86a97
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/RequestPolicyRule/CommandLineOrUnsafeMethod.php
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\RequestPolicyRule\CommandLineOrUnsafeMethod.
+ */
+
+namespace Drupal\Core\PageCache\RequestPolicyRule;
+
+use Drupal\Core\PageCache\RequestPolicyRuleInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Reject when running from the command line or when HTTP method is not safe.
+ *
+ * An instance of this class is normally used as the global policy by the main
+ * page caching service. The policy allows caching as long as the request was
+ * not initiated from the command line interface (drush) and the request method
+ * is either GET or HEAD (see RFC 2616, section 9.1.1 - Safe Methods).
+ */
+class CommandLineOrUnsafeMethod implements RequestPolicyRuleInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function apply(Request $request) {
+    return $this->isCliRequest($request) || !$request->isMethodSafe();
+  }
+
+  /**
+   * Exclude a page request when run from a command line script.
+   */
+  protected function isCliRequest(Request $request) {
+    return drupal_is_cli();
+  }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/RequestPolicyRule/NoSessionOpen.php b/core/lib/Drupal/Core/PageCache/RequestPolicyRule/NoSessionOpen.php
new file mode 100644
index 0000000..e048d8a
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/RequestPolicyRule/NoSessionOpen.php
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\RequestPolicyRule\NoSessionOpen.
+ */
+
+namespace Drupal\Core\PageCache\RequestPolicyRule;
+
+use Drupal\Core\PageCache\RequestPolicyRuleInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * A policy rule evaluating to TRUE only when there is no session open.
+ *
+ * Do not serve cached pages to authenticated users, or to anonymous users when
+ * $_SESSION is non-empty. $_SESSION may contain status messages from a form
+ * submission, the contents of a shopping cart, or other userspecific content
+ * that should not be cached and displayed to other users.
+ */
+class NoSessionOpen implements RequestPolicyRuleInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function apply(Request $request) {
+    return !$request->cookies->has(session_name());
+  }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/RequestPolicyRuleInterface.php b/core/lib/Drupal/Core/PageCache/RequestPolicyRuleInterface.php
new file mode 100644
index 0000000..4336dc8
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/RequestPolicyRuleInterface.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\RequestPolicyRuleInterface.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Defines the interface for request policy rule implementations.
+ */
+interface RequestPolicyRuleInterface {
+
+  /**
+   * Apply the policy rule to the given request and return the result.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   A request object.
+   *
+   * @return bool
+   *   TRUE when the policy rule allows caching for the request.
+   */
+  public function apply(Request $request);
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/ResponsePolicy.php b/core/lib/Drupal/Core/PageCache/ResponsePolicy.php
new file mode 100644
index 0000000..3bc11ef
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/ResponsePolicy.php
@@ -0,0 +1,52 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\ResponsePolicy.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Container for rules applied before a response is recorded in the cache.
+ */
+class ResponsePolicy implements ResponsePolicyRuleInterface {
+
+  /**
+   * A set of policy rules representing the deny-chain.
+   *
+   * @var \Drupal\Core\PageCache\ResponsePolicyRuleInterface[]
+   */
+  protected $deny;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function apply(Response $response, Request $request) {
+    foreach ($this->deny as $rule) {
+      if ($rule->apply($response, $request)) {
+        return FALSE;
+      }
+    }
+
+    return TRUE;
+  }
+
+  /**
+   * Add one policy rule to the deny-chain.
+   *
+   * @param \Drupal\Core\PageCache\ResponsePolicyRuleInterface $rule
+   *   The rules to add to the deny-chain.
+   *
+   * @return \Drupal\Core\PageCache\ResponsePolicy
+   *   This policy instance.
+   */
+  public function deny(ResponsePolicyRuleInterface $rule) {
+    $this->deny[spl_object_hash($rule)] = $rule;
+    return $this;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/ResponsePolicyRule/IsBinaryFileResponse.php b/core/lib/Drupal/Core/PageCache/ResponsePolicyRule/IsBinaryFileResponse.php
new file mode 100644
index 0000000..b928c8c
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/ResponsePolicyRule/IsBinaryFileResponse.php
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\ResponsePolicyRule\IsBinaryFileResponse.
+ */
+
+namespace Drupal\Core\PageCache\ResponsePolicyRule;
+
+use Drupal\Core\PageCache\ResponsePolicyRuleInterface;
+use Symfony\Component\HttpFoundation\BinaryFileResponse;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * A policy rule evaluating to TRUE when the response is a file download.
+ */
+class IsBinaryFileResponse implements ResponsePolicyRuleInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function apply(Response $response, Request $request) {
+    return ($response instanceof BinaryFileResponse);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/ResponsePolicyRule/IsStreamedResponse.php b/core/lib/Drupal/Core/PageCache/ResponsePolicyRule/IsStreamedResponse.php
new file mode 100644
index 0000000..20e4b18
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/ResponsePolicyRule/IsStreamedResponse.php
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\ResponsePolicyRule\IsStreamedResponse.
+ */
+
+namespace Drupal\Core\PageCache\ResponsePolicyRule;
+
+use Drupal\Core\PageCache\ResponsePolicyRuleInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpFoundation\StreamedResponse;
+
+/**
+ * A policy rule evaluating to TRUE when the response is a file download.
+ */
+class IsStreamedResponse implements ResponsePolicyRuleInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function apply(Response $response, Request $request) {
+    return ($response instanceof StreamedResponse);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/ResponsePolicyRule/KillSwitch.php b/core/lib/Drupal/Core/PageCache/ResponsePolicyRule/KillSwitch.php
new file mode 100644
index 0000000..53b1384
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/ResponsePolicyRule/KillSwitch.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\ResponsePolicyRule\KillSwitch.
+ */
+
+namespace Drupal\Core\PageCache\ResponsePolicyRule;
+
+use Drupal\Core\PageCache\ResponsePolicyRuleInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * A veto rule evaluating to TRUE when the kill switch was triggered.
+ */
+class KillSwitch implements ResponsePolicyRuleInterface {
+
+  /**
+   * A flag indicating whether the kill switch was triggered.
+   *
+   * @var bool
+   */
+  protected $kill = FALSE;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function apply(Response $response, Request $request) {
+    return $this->kill;
+  }
+
+  /**
+   * Deny any page caching on the current request.
+   */
+  public function trigger() {
+    $this->kill = TRUE;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/ResponsePolicyRuleInterface.php b/core/lib/Drupal/Core/PageCache/ResponsePolicyRuleInterface.php
new file mode 100644
index 0000000..86ec452
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/ResponsePolicyRuleInterface.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\ResponsePolicyRuleInterface.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Defines the interface for response policy rule implementations.
+ */
+interface ResponsePolicyRuleInterface {
+
+  /**
+   * Apply the policy rule to the given response and return the result.
+   *
+   * @param \Symfony\Component\HttpFoundation\Response $response
+   *   A response object.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   A request object.
+   *
+   * @return bool
+   *   TRUE when the policy rule allows caching of the response.
+   */
+  public function apply(Response $response, Request $request);
+
+}
diff --git a/core/lib/Drupal/Core/Session/SessionManager.php b/core/lib/Drupal/Core/Session/SessionManager.php
index f656b2d..a5e436f 100644
--- a/core/lib/Drupal/Core/Session/SessionManager.php
+++ b/core/lib/Drupal/Core/Session/SessionManager.php
@@ -126,9 +126,6 @@ public function initialize() {
       // anonymous users not use a session cookie unless something is stored in
       // $_SESSION. This allows HTTP proxies to cache anonymous pageviews.
       $this->start();
-      if ($user->isAuthenticated() || !$this->isSessionObsolete()) {
-        drupal_page_is_cacheable(FALSE);
-      }
     }
     else {
       // Set a session identifier for this request. This is necessary because
diff --git a/core/modules/toolbar/lib/Drupal/toolbar/Controller/ToolbarController.php b/core/modules/toolbar/lib/Drupal/toolbar/Controller/ToolbarController.php
index 11d17b4..5d47072 100644
--- a/core/modules/toolbar/lib/Drupal/toolbar/Controller/ToolbarController.php
+++ b/core/modules/toolbar/lib/Drupal/toolbar/Controller/ToolbarController.php
@@ -23,10 +23,22 @@ class ToolbarController extends ControllerBase {
    * @return \Symfony\Component\HttpFoundation\JsonResponse
    */
   public function subtreesJsonp() {
-    _toolbar_initialize_page_cache();
     $subtrees = toolbar_get_rendered_subtrees();
     $response = new JsonResponse($subtrees);
     $response->setCallback('Drupal.toolbar.setSubtrees.resolve');
+
+    // The Expires HTTP header is the heart of the client-side HTTP caching. The
+    // additional server-side page cache only takes effect when the client
+    // accesses the callback URL again (e.g., after clearing the browser cache
+    // or when force-reloading a Drupal page).
+    $max_age = 365 * 24 * 60 * 60;
+    $response->setPrivate();
+    $response->setMaxAge($max_age);
+
+    $expires = new \DateTime();
+    $expires->setTimestamp(REQUEST_TIME + $max_age);
+    $response->setExpires($expires);
+
     return $response;
   }
 
diff --git a/core/modules/toolbar/lib/Drupal/toolbar/PageCache/IsToolbarPath.php b/core/modules/toolbar/lib/Drupal/toolbar/PageCache/IsToolbarPath.php
new file mode 100644
index 0000000..ae4e408
--- /dev/null
+++ b/core/modules/toolbar/lib/Drupal/toolbar/PageCache/IsToolbarPath.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\toolbar\PageCache\IsToolbarPath.
+ */
+
+namespace Drupal\toolbar\PageCache;
+
+use Drupal\Core\PageCache\RequestPolicyRuleInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Cache policy for the toolbar page cache service.
+ *
+ * This policy allows caching of requests directed to /toolbar/subtrees/{hash}
+ * even for authenticated users.
+ */
+class IsToolbarPath implements RequestPolicyRuleInterface {
+
+  /**
+   * Test whether this request goes to /toolbar/subtrees.
+   */
+  public function apply(Request $request) {
+    return strpos($request->getPathInfo(), '/toolbar/subtrees/') === 0;
+  }
+
+}
diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module
index 3599c59..597d157 100644
--- a/core/modules/toolbar/toolbar.module
+++ b/core/modules/toolbar/toolbar.module
@@ -106,43 +106,6 @@ function toolbar_element_info() {
 }
 
 /**
- * Use Drupal's page cache for toolbar/subtrees/*, even for authenticated users.
- *
- * This gets invoked after full bootstrap, so must duplicate some of what's
- * done by _drupal_bootstrap_page_cache().
- *
- * @todo Replace this hack with something better integrated with DrupalKernel
- *   once Drupal's page caching itself is properly integrated.
- */
-function _toolbar_initialize_page_cache() {
-  $GLOBALS['conf']['system.performance']['cache']['page']['enabled'] = TRUE;
-  drupal_page_is_cacheable(TRUE);
-
-  // If we have a cache, serve it.
-  // @see _drupal_bootstrap_page_cache()
-  $request = \Drupal::request();
-  $response = drupal_page_get_cache($request);
-  if ($response) {
-    $response->headers->set('X-Drupal-Cache', 'HIT');
-
-    drupal_serve_page_from_cache($response, $request);
-
-    $response->prepare($request);
-    $response->send();
-    // We are done.
-    exit;
-  }
-
-  // The Expires HTTP header is the heart of the client-side HTTP caching. The
-  // additional server-side page cache only takes effect when the client
-  // accesses the callback URL again (e.g., after clearing the browser cache or
-  // when force-reloading a Drupal page).
-  $max_age = 3600 * 24 * 365;
-  drupal_add_http_header('Expires', gmdate(DATE_RFC1123, REQUEST_TIME + $max_age));
-  drupal_add_http_header('Cache-Control', 'private, max-age=' . $max_age);
-}
-
-/**
  * Implements hook_page_build().
  *
  * Add admin toolbar to the page_top region automatically.
diff --git a/core/modules/toolbar/toolbar.services.yml b/core/modules/toolbar/toolbar.services.yml
index 7f26968..10fad41 100644
--- a/core/modules/toolbar/toolbar.services.yml
+++ b/core/modules/toolbar/toolbar.services.yml
@@ -6,3 +6,7 @@ services:
     factory_method: get
     factory_service: cache_factory
     arguments: [toolbar]
+  toolbar.page_cache_request_policy.toolbar_path:
+    class: Drupal\toolbar\PageCache\IsToolbarPath
+    tags:
+      - { name: page_cache_request_policy_allow}
