diff --git a/automatic_updates.info.yml b/automatic_updates.info.yml
index 54689a376789f85e63b4eaf07091556b1e29ba8d..3ba06a9eb1774016f6316ad6b73b1442a946f657 100644
--- a/automatic_updates.info.yml
+++ b/automatic_updates.info.yml
@@ -1,5 +1,5 @@
 name: 'Automatic Updates'
 type: module
 description: 'Drupal Automatic Updates'
-core: 8.7
-package: 'Maintenance'
+core: 8.x
+package: 'Security'
diff --git a/automatic_updates.module b/automatic_updates.module
new file mode 100644
index 0000000000000000000000000000000000000000..5157e76f2e011f53663f5195aed3b47ec2d08c97
--- /dev/null
+++ b/automatic_updates.module
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * @file
+ * Contains automatic_updates.module..
+ */
+
+/**
+ * Implements hook_page_top().
+ */
+function automatic_updates_page_top(array &$page_top) {
+  /** @var \Drupal\Core\Routing\AdminContext $admin_context */
+  $admin_context = \Drupal::service('router.admin_context');
+  $route_match = \Drupal::routeMatch();
+  if ($admin_context->isAdminRoute($route_match->getRouteObject()) && \Drupal::currentUser()->hasPermission('administer site configuration')) {
+    $disabled_routes = [
+      'update.theme_update',
+      'system.theme_install',
+      'update.module_update',
+      'update.module_install',
+      'update.status',
+      'update.report_update',
+      'update.report_install',
+      'update.settings',
+      'system.status',
+      'update.confirmation_page',
+    ];
+    // These routes don't need additional nagging.
+    if (in_array(\Drupal::routeMatch()->getRouteName(), $disabled_routes, TRUE)) {
+        return;
+    }
+    /** @var \Drupal\automatic_updates\Services\AutomaticUpdatesPsaInterface $psa */
+    $psa = \Drupal::service('automatic_updates.psa');
+    foreach ($psa->getPublicServiceMessages() as $psa) {
+      \Drupal::messenger()->addError($psa);
+    }
+  }
+}
diff --git a/automatic_updates.services.yml b/automatic_updates.services.yml
new file mode 100644
index 0000000000000000000000000000000000000000..26d646aca95ac75c9a7d6c87d23cd4daa7a498d1
--- /dev/null
+++ b/automatic_updates.services.yml
@@ -0,0 +1,15 @@
+services:
+  logger.channel.automatic_updates:
+    parent: logger.channel_base
+    arguments: ['automatic_updates']
+  automatic_updates.psa:
+    class: Drupal\automatic_updates\Services\AutomaticUpdatesPsa
+    arguments:
+      - '@config.factory'
+      - '@cache.default'
+      - '@datetime.time'
+      - '@http_client'
+      - '@extension.list.module'
+      - '@extension.list.profile'
+      - '@extension.list.theme'
+      - '@logger.channel.automatic_updates'
diff --git a/config/install/automatic_updates.settings.yml b/config/install/automatic_updates.settings.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2dc7865e6537becafe131b5f5c341e1e45a62671
--- /dev/null
+++ b/config/install/automatic_updates.settings.yml
@@ -0,0 +1,4 @@
+# Public service announcement URI endpoint.
+# TODO:  Update to correct end point once it is available. See
+# https://www.drupal.org/project/automatic_updates/issues/3045273
+psa_endpoint: 'http://localhost/automatic_updates/test-json'
diff --git a/config/schema/automatic_updates.schema.yml b/config/schema/automatic_updates.schema.yml
new file mode 100644
index 0000000000000000000000000000000000000000..467d316c33677bb767d3a127a075132a44c603ba
--- /dev/null
+++ b/config/schema/automatic_updates.schema.yml
@@ -0,0 +1,7 @@
+automatic_updates.settings:
+  type: config_object
+  label: 'Automatic updates settings'
+  mapping:
+    psa_endpoint:
+      type: string
+      label: 'Endpoint URI for PSAs'
diff --git a/src/Services/AutomaticUpdatesPsa.php b/src/Services/AutomaticUpdatesPsa.php
new file mode 100644
index 0000000000000000000000000000000000000000..5ca53f29005ec583284a2e4702dbfd3b1f645cb8
--- /dev/null
+++ b/src/Services/AutomaticUpdatesPsa.php
@@ -0,0 +1,227 @@
+<?php
+
+namespace Drupal\automatic_updates\Services;
+
+use Composer\Semver\VersionParser;
+use Drupal\Component\Datetime\TimeInterface;
+use Drupal\Component\Version\Constraint;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\DependencyInjection\DependencySerializationTrait;
+use Drupal\Core\Extension\ExtensionList;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use GuzzleHttp\Client;
+use GuzzleHttp\Exception\TransferException;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Class AutomaticUpdatesPsa.
+ */
+class AutomaticUpdatesPsa implements AutomaticUpdatesPsaInterface {
+  use StringTranslationTrait;
+  use DependencySerializationTrait;
+
+  /**
+   * Module's configuration.
+   *
+   * @var \Drupal\Core\Config\ImmutableConfig
+   */
+  protected $config;
+
+  /**
+   * The http client.
+   *
+   * @var \GuzzleHttp\Client
+   */
+  protected $httpClient;
+
+  /**
+   * The cache backend.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface
+   */
+  protected $cache;
+
+  /**
+   * The time service.
+   *
+   * @var \Drupal\Component\Datetime\TimeInterface
+   */
+  protected $time;
+
+  /**
+   * The module extension list.
+   *
+   * @var \Drupal\Core\Extension\ExtensionList
+   */
+  protected $module;
+
+  /**
+   * The profile extension list.
+   *
+   * @var \Drupal\Core\Extension\ExtensionList
+   */
+  protected $profile;
+
+  /**
+   * The theme extension list.
+   *
+   * @var \Drupal\Core\Extension\ExtensionList
+   */
+  protected $theme;
+
+  /**
+   * The logger.
+   *
+   * @var \Psr\Log\LoggerInterface
+   */
+  protected $logger;
+
+  /**
+   * AutomaticUpdatesPsa constructor.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
+   *   The cache backend.
+   * @param \Drupal\Component\Datetime\TimeInterface $time
+   *   The time service.
+   * @param \GuzzleHttp\Client $client
+   *   The HTTP client.
+   * @param \Drupal\Core\Extension\ExtensionList $module
+   *   The module extension list.
+   * @param \Drupal\Core\Extension\ExtensionList $profile
+   *   The profile extension list.
+   * @param \Drupal\Core\Extension\ExtensionList $theme
+   *   The theme extension list.
+   * @param \Psr\Log\LoggerInterface $logger
+   *   The logger.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory, CacheBackendInterface $cache, TimeInterface $time, Client $client, ExtensionList $module, ExtensionList $profile, ExtensionList $theme, LoggerInterface $logger) {
+    $this->config = $config_factory->get('automatic_updates.settings');
+    $this->cache = $cache;
+    $this->time = $time;
+    $this->httpClient = $client;
+    $this->module = $module;
+    $this->profile = $profile;
+    $this->theme = $theme;
+    $this->logger = $logger;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPublicServiceMessages() {
+    $messages = [];
+
+    if ($cache = $this->cache->get('automatic_updates_psa')) {
+      $response = $cache->data;
+    }
+    else {
+      try {
+        $psa_endpoint = $this->config->get('psa_endpoint');
+        $response = $this->httpClient->get($psa_endpoint)
+          ->getBody()
+          ->getContents();
+        // Set response in cache for 12 hours.
+        $this->cache->set('automatic_updates_psa', $response, $this->time->getCurrentTime() + 3600 * 12);
+      }
+      catch (TransferException $exception) {
+        $this->logger->error($exception->getMessage());
+        return [$this->t('Drupal PSA endpoint :url is unreachable.', [':url' => $psa_endpoint])];
+      }
+    }
+
+    try {
+      $json_payload = json_decode($response);
+      foreach ($json_payload as $json) {
+        if ($json->project === 'core') {
+          $this->coreParser($messages, $json);
+        }
+        else {
+          $this->contribParser($messages, $json);
+        }
+      }
+    }
+    catch (\UnexpectedValueException $exception) {
+      $this->logger->error($exception->getMessage());
+      $messages[] = $this->t('Drupal PSA endpoint service is malformed.');
+    }
+
+    return $messages;
+  }
+
+  /**
+   * Parse core project JSON version strings.
+   *
+   * @param array $messages
+   *   The messages array.
+   * @param object $json
+   *   The JSON object.
+   */
+  protected function coreParser(array &$messages, $json) {
+    $parser = new VersionParser();
+    array_walk($json->secure_versions, function (&$version) {
+      $version = '<' . $version;
+    });
+    $version_string = implode('||', $json->secure_versions);
+    $psa_constraint = $parser->parseConstraints($version_string);
+    $core_constraint = $parser->parseConstraints(\Drupal::VERSION);
+    if ($psa_constraint->matches($core_constraint)) {
+      $messages[] = $this->t('Drupal Core PSA: <a href=":url">:message</a>', [
+        ':message' => $json->title,
+        ':url' => $json->link,
+      ]);
+    }
+  }
+
+  /**
+   * Parse contrib project JSON version strings.
+   *
+   * @param array $messages
+   *   The messages array.
+   * @param object $json
+   *   The JSON object.
+   */
+  protected function contribParser(array &$messages, $json) {
+    $extension_list = $json->type;
+    if (!property_exists($this, $extension_list)) {
+      $this->logger->error('Extension list of type "%extension" does not exist.', ['%extension' => $extension_list]);
+      return;
+    }
+    foreach ($json->extensions as $extension_name) {
+      if ($this->{$extension_list}->exists($extension_name)) {
+        $extension = $this->{$extension_list}->getAllAvailableInfo()[$extension_name];
+        if (empty($extension['version'])) {
+          continue;
+        }
+        $this->contribMessage($messages, $json, $extension['version']);
+      }
+    }
+  }
+
+  /**
+   * Add a contrib message PSA, if appropriate.
+   *
+   * @param array $messages
+   *   The messages array.
+   * @param object $json
+   *   The JSON object.
+   * @param string $extension_version
+   *   The extension version.
+   */
+  protected function contribMessage(array &$messages, $json, $extension_version) {
+    array_walk($json->secure_versions, function (&$version) {
+      $version = \Drupal::CORE_COMPATIBILITY . '-' . $version;
+    });
+    $version_string = implode('||', $json->secure_versions);
+    $constraint = new Constraint($extension_version, \Drupal::CORE_COMPATIBILITY);
+    if (!$constraint->isCompatible($version_string)) {
+      $messages[] = $this->t('Drupal Contrib Project PSA: <a href=":url">:message</a>', [
+        ':message' => $json->title,
+        ':url' => $json->link,
+      ]);
+    }
+  }
+
+}
diff --git a/src/Services/AutomaticUpdatesPsaInterface.php b/src/Services/AutomaticUpdatesPsaInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..67ce4f2f1b98e34cf3fae96275c65aa04c5983ed
--- /dev/null
+++ b/src/Services/AutomaticUpdatesPsaInterface.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace Drupal\automatic_updates\Services;
+
+/**
+ * Interface AutomaticUpdatesPsaInterface.
+ */
+interface AutomaticUpdatesPsaInterface {
+
+  /**
+   * Get public service messages.
+   *
+   * @return array
+   *   A return of translatable strings.
+   */
+  public function getPublicServiceMessages();
+
+}
diff --git a/tests/modules/test_automatic_updates/src/Controller/JsonTestController.php b/tests/modules/test_automatic_updates/src/Controller/JsonTestController.php
index 90bbb34ca0404f0b43f0bb89342918187bdab4e9..cb72134a7939da980154ef446d1168bcf2e1dca9 100644
--- a/tests/modules/test_automatic_updates/src/Controller/JsonTestController.php
+++ b/tests/modules/test_automatic_updates/src/Controller/JsonTestController.php
@@ -20,17 +20,66 @@ class JsonTestController extends ControllerBase {
     $feed[] = [
       'title' => 'Critical Release - PSA-2019-02-19',
       'link' => 'https://www.drupal.org/psa-2019-02-19',
-      'project' => 'drupal/core',
-      'modules' => ['forum', 'node'],
-      'version' => '>=8.0.0 <8.6.10 || >=8.0.0 <8.5.11',
+      'project' => 'core',
+      'extensions' => [],
+      'type' => 'module',
+      'secure_versions' => [
+        '7.99',
+        '8.10.99',
+        '8.9.99',
+        '8.8.99',
+        '8.7.99',
+        '8.6.99',
+        '8.5.99',
+      ],
       'pubDate' => 'Tue, 19 Feb 2019 14:11:01 +0000',
     ];
     $feed[] = [
-      'title' => 'Critical Release - PSA-Fictional PSA',
-      'link' => 'https://www.drupal.org/psa-fictional-psa',
-      'project' => 'drupal/core',
-      'modules' => ['system'],
-      'version' => '>=8.6.10 || >=8.5.11',
+      'title' => 'Critical Release - PSA-Really Old',
+      'link' => 'https://www.drupal.org/psa',
+      'project' => 'core',
+      'extensions' => [],
+      'type' => 'module',
+      'secure_versions' => [
+        '7.0',
+        '8.4.0',
+      ],
+      'pubDate' => 'Tue, 19 Feb 2019 14:11:01 +0000',
+    ];
+    $feed[] = [
+      'title' => 'Node - Moderately critical - Access bypass - SA-CONTRIB-2019',
+      'link' => 'https://www.drupal.org/sa-contrib-2019',
+      'project' => 'node',
+      'extensions' => ['node'],
+      'type' => 'module',
+      'secure_versions' => ['8.10.99'],
+      'pubDate' => 'Tue, 19 Mar 2019 12:50:00 +0000',
+    ];
+    $feed[] = [
+      'title' => 'Standard - Moderately critical - Access bypass - SA-CONTRIB-2019',
+      'link' => 'https://www.drupal.org/sa-contrib-2019',
+      'project' => 'Standard Install Profile',
+      'extensions' => ['standard'],
+      'type' => 'profile',
+      'secure_versions' => ['8.10.99'],
+      'pubDate' => 'Tue, 19 Mar 2019 12:50:00 +0000',
+    ];
+    $feed[] = [
+      'title' => 'Seven - Moderately critical - Access bypass - SA-CONTRIB-2019',
+      'link' => 'https://www.drupal.org/sa-contrib-2019',
+      'project' => 'seven',
+      'extensions' => ['seven'],
+      'type' => 'theme',
+      'secure_versions' => ['8.10.99'],
+      'pubDate' => 'Tue, 19 Mar 2019 12:50:00 +0000',
+    ];
+    $feed[] = [
+      'title' => 'Foobar - Moderately critical - Access bypass - SA-CONTRIB-2019',
+      'link' => 'https://www.drupal.org/sa-contrib-2019',
+      'project' => 'foobar',
+      'extensions' => ['foobar'],
+      'type' => 'foobar',
+      'secure_versions' => ['8.10.99'],
       'pubDate' => 'Tue, 19 Mar 2019 12:50:00 +0000',
     ];
     return new JsonResponse($feed);
diff --git a/tests/modules/test_automatic_updates/test_automatic_updates.routing.yml b/tests/modules/test_automatic_updates/test_automatic_updates.routing.yml
index 388108af395ec7a06ebdccd65635868eeec56dd7..1920578446758ecc6567b1a121808fa97f9f9e35 100644
--- a/tests/modules/test_automatic_updates/test_automatic_updates.routing.yml
+++ b/tests/modules/test_automatic_updates/test_automatic_updates.routing.yml
@@ -5,3 +5,10 @@ test_automatic_updates.json_test_controller:
     _title: 'JSON'
   requirements:
     _access: 'TRUE'
+test_automatic_updates.json_test_denied_controller:
+  path: '/automatic_updates/test-json-denied'
+  defaults:
+    _controller: '\Drupal\test_automatic_updates\Controller\JsonTestController::json'
+    _title: 'JSON'
+  requirements:
+    _access: 'FALSE'
diff --git a/tests/src/Functional/AutomaticUpdatesTest.php b/tests/src/Functional/AutomaticUpdatesTest.php
index 23e089744a4657345c57aa3d42cd5cfb0b4a6479..823784db6663b975fbc6015399d9981c128d9935 100644
--- a/tests/src/Functional/AutomaticUpdatesTest.php
+++ b/tests/src/Functional/AutomaticUpdatesTest.php
@@ -2,7 +2,6 @@
 
 namespace Drupal\Tests\automatic_updates\Functional;
 
-use Composer\Semver\VersionParser;
 use Drupal\Core\Url;
 use Drupal\Tests\BrowserTestBase;
 
@@ -35,24 +34,40 @@ class AutomaticUpdatesTest extends BrowserTestBase {
    */
   protected function setUp() {
     parent::setUp();
-    $this->user = $this->drupalCreateUser(['administer site configuration']);
+    $this->user = $this->drupalCreateUser([
+      'administer site configuration',
+      'access administration pages',
+    ]);
     $this->drupalLogin($this->user);
   }
 
   /**
-   * Tests that the JSON is parsable.
+   * Tests that a PSA is displayed.
    */
-  public function testJson() {
-    $this->drupalGet(Url::fromRoute('test_automatic_updates.json_test_controller'));
-    $json = json_decode($this->getSession()->getPage()->getContent(), TRUE);
-    $this->assertEquals($json[0]['title'], 'Critical Release - PSA-2019-02-19');
-    $parser = new VersionParser();
-    $constraint = $parser->parseConstraints($json[0]['version']);
-    $core_constraint = $parser->parseConstraints(\Drupal::VERSION);
-    $this->assertFALSE($constraint->matches($core_constraint));
-    $constraint = $parser->parseConstraints($json[1]['version']);
-    $core_constraint = $parser->parseConstraints(\Drupal::VERSION);
-    $this->assertTRUE($constraint->matches($core_constraint));
+  public function testPsa() {
+    $end_point = $this->buildUrl(Url::fromRoute('test_automatic_updates.json_test_controller'));
+    $this->config('automatic_updates.settings')
+      ->set('psa_endpoint', $end_point)
+      ->save();
+    $this->drupalGet(Url::fromRoute('system.admin'));
+    $this->assertSession()->pageTextContains('Drupal Core PSA: Critical Release - PSA-2019-02-19');
+    $this->assertSession()->pageTextNotContains('Drupal Core PSA: Critical Release - PSA-Really Old');
+    $this->assertSession()->pageTextContains('Drupal Contrib Project PSA: Node - Moderately critical - Access bypass - SA-CONTRIB-2019');
+    $this->assertSession()->pageTextContains('Drupal Contrib Project PSA: Seven - Moderately critical - Access bypass - SA-CONTRIB-2019');
+    $this->assertSession()->pageTextContains('Drupal Contrib Project PSA: Standard - Moderately critical - Access bypass - SA-CONTRIB-2019');
+
+    // Test cache.
+    $end_point = $this->buildUrl(Url::fromRoute('test_automatic_updates.json_test_denied_controller'));
+    $this->config('automatic_updates.settings')
+      ->set('psa_endpoint', $end_point)
+      ->save();
+    $this->drupalGet(Url::fromRoute('system.admin'));
+    $this->assertSession()->pageTextContains('Drupal Core PSA: Critical Release - PSA-2019-02-19');
+
+    // Test transmit errors with JSON endpoint.
+    drupal_flush_all_caches();
+    $this->drupalGet(Url::fromRoute('system.admin'));
+    $this->assertSession()->pageTextContains('Drupal PSA endpoint http://localhost/automatic_updates/test-json-denied is unreachable.');
   }
 
 }