diff --git a/src/Entity/ContentWorkspace.php b/src/Entity/ContentWorkspace.php
index d134014..01d8d59 100644
--- a/src/Entity/ContentWorkspace.php
+++ b/src/Entity/ContentWorkspace.php
@@ -26,6 +26,10 @@ use Drupal\Core\Field\BaseFieldDefinition;
  *     "uuid" = "uuid",
  *   }
  * )
+ *
+ * @internal
+ *   This entity is marked internal because it should not be used directly to
+ *   alter the workspace an entity belongs to.
  */
 class ContentWorkspace extends ContentEntityBase implements ContentWorkspaceInterface {
 
diff --git a/src/Entity/ReplicationLog.php b/src/Entity/ReplicationLog.php
index 806eb00..ac856ba 100644
--- a/src/Entity/ReplicationLog.php
+++ b/src/Entity/ReplicationLog.php
@@ -26,6 +26,10 @@ use Drupal\Core\StringTranslation\TranslatableMarkup;
  *     "revision" = "revision_id",
  *   },
  * )
+ *
+ * @internal
+ *   This entity is marked internal because it should only be created, read,
+ *   updated, or deleted, by the content replication process.
  */
 class ReplicationLog extends ContentEntityBase implements ReplicationLogInterface {
 
diff --git a/src/Entity/Workspace.php b/src/Entity/Workspace.php
index 3b90272..5e917b8 100644
--- a/src/Entity/Workspace.php
+++ b/src/Entity/Workspace.php
@@ -38,7 +38,6 @@ use Drupal\workspace\UpstreamPluginInterface;
  *       "deploy" = "\Drupal\workspace\Form\WorkspaceDeployForm",
  *     },
  *   },
- *   admin_permission = "administer workspaces",
  *   base_table = "workspace",
  *   revision_table = "workspace_revision",
  *   data_table = "workspace_field_data",
diff --git a/tests/src/Functional/EntityResource/WorkspaceJsonAnonTest.php b/tests/src/Functional/EntityResource/WorkspaceJsonAnonTest.php
new file mode 100644
index 0000000..04e2510
--- /dev/null
+++ b/tests/src/Functional/EntityResource/WorkspaceJsonAnonTest.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Drupal\Tests\workspace\Functional\EntityResource;
+
+use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
+
+/**
+ * Test workspace entities for unauthenticated JSON requests.
+ *
+ * @group workspace
+ */
+class WorkspaceJsonAnonTest extends WorkspaceResourceTestBase {
+
+  use AnonResourceTestTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $format = 'json';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $mimeType = 'application/json';
+
+}
diff --git a/tests/src/Functional/EntityResource/WorkspaceJsonBasicAuthTest.php b/tests/src/Functional/EntityResource/WorkspaceJsonBasicAuthTest.php
new file mode 100644
index 0000000..dbae2d5
--- /dev/null
+++ b/tests/src/Functional/EntityResource/WorkspaceJsonBasicAuthTest.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\Tests\workspace\Functional\EntityResource;
+
+use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
+
+/**
+ * Test workspace entities for JSON requests via basic auth.
+ *
+ * @group workspace
+ */
+class WorkspaceJsonBasicAuthTest extends WorkspaceResourceTestBase {
+
+  use BasicAuthResourceTestTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['basic_auth'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $format = 'json';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $mimeType = 'application/json';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $auth = 'basic_auth';
+
+}
diff --git a/tests/src/Functional/EntityResource/WorkspaceJsonCookieTest.php b/tests/src/Functional/EntityResource/WorkspaceJsonCookieTest.php
new file mode 100644
index 0000000..f77dbf2
--- /dev/null
+++ b/tests/src/Functional/EntityResource/WorkspaceJsonCookieTest.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Drupal\Tests\workspace\Functional\EntityResource;
+
+use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
+
+/**
+ * Test workspace entities for JSON requests with cookie authentication.
+ *
+ * @group workspace
+ */
+class WorkspaceJsonCookieTest extends WorkspaceResourceTestBase {
+
+  use CookieResourceTestTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $format = 'json';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $mimeType = 'application/json';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $auth = 'cookie';
+
+}
diff --git a/tests/src/Functional/EntityResource/WorkspaceResourceTestBase.php b/tests/src/Functional/EntityResource/WorkspaceResourceTestBase.php
new file mode 100644
index 0000000..81e333a
--- /dev/null
+++ b/tests/src/Functional/EntityResource/WorkspaceResourceTestBase.php
@@ -0,0 +1,196 @@
+<?php
+
+namespace Drupal\Tests\workspace\Functional\EntityResource;
+
+use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
+use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
+use Drupal\user\Entity\User;
+use Drupal\workspace\Entity\Workspace;
+
+/**
+ * Base class for workspace EntityResource tests.
+ */
+abstract class WorkspaceResourceTestBase extends EntityResourceTestBase {
+
+  use BcTimestampNormalizerUnixTestTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['workspace'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $entityTypeId = 'workspace';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $patchProtectedFieldNames = ['changed'];
+
+  /**
+   * The entity ID for the first created entity in testPost().
+   *
+   * @var string
+   *
+   * @see ::testPost()
+   * @see ::getNormalizedPostEntity()
+   */
+  protected static $firstCreatedEntityId = 'running_on_faith';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUpAuthorization($method) {
+    switch ($method) {
+      case 'GET':
+        $this->grantPermissionsToTestedRole(['view workspace layla']);
+        break;
+      case 'POST':
+        $this->grantPermissionsToTestedRole(['view workspace layla', 'create workspace']);
+        break;
+      case 'PATCH':
+        $this->grantPermissionsToTestedRole(['view workspace layla', 'update workspace layla']);
+        break;
+      case 'DELETE':
+        $this->grantPermissionsToTestedRole(['view workspace layla', 'delete workspace layla']);
+        break;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function createEntity() {
+    $workspace = Workspace::create([
+      'id' => 'layla',
+      'label' => 'Layla',
+      'upstream' => 'local_workspace:live',
+    ]);
+    $workspace->save();
+    return $workspace;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedNormalizedEntity() {
+    $author = User::load($this->entity->getOwnerId());
+    return [
+      'created' => [
+        $this->formatExpectedTimestampItemValues((int) $this->entity->getStartTime()),
+      ],
+      'changed' => [
+        $this->formatExpectedTimestampItemValues($this->entity->getChangedTime()),
+      ],
+      'id' => [
+        [
+          'value' => 'layla',
+        ],
+      ],
+      'label' => [
+        [
+          'value' => 'Layla',
+        ],
+      ],
+      'revision_id' => [
+        [
+          'value' => 3,
+        ],
+      ],
+      'uid' => [
+        [
+          'target_id' => (int) $author->id(),
+          'target_type' => 'user',
+          'target_uuid' => $author->uuid(),
+          'url' => base_path() . 'user/' . $author->id(),
+        ],
+      ],
+      'upstream' => [
+        [
+          'value' => 'local_workspace:live',
+        ],
+      ],
+      'uuid' => [
+        [
+          'value' => $this->entity->uuid()
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getNormalizedPostEntity() {
+    return [
+      'id' => [
+        [
+          'value' => 'running_on_faith',
+        ],
+      ],
+      'label' => [
+        [
+          'value' => 'Running on faith',
+        ],
+      ],
+      'upstream' => [
+        [
+          'value' => 'local_workspace:stage',
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getNormalizedPatchEntity() {
+    return [
+      'label' => [
+        [
+          'value' => 'Running on faith',
+        ],
+      ],
+      'upstream' => [
+        [
+          'value' => 'local_workspace:stage',
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedUnauthorizedAccessMessage($method) {
+    if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
+      return parent::getExpectedUnauthorizedAccessMessage($method);
+    }
+
+    switch ($method) {
+      case 'GET':
+        return "The 'view workspace layla' permission is required.";
+        break;
+      case 'POST':
+        return "The 'create workspace' permission is required.";
+        break;
+      case 'PATCH':
+        return "The 'update workspace layla' permission is required.";
+        break;
+      case 'DELETE':
+        return "The 'delete workspace layla' permission is required.";
+        break;
+    }
+    return parent::getExpectedUnauthorizedAccessMessage($method);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedUnauthorizedAccessCacheability() {
+    return parent::getExpectedUnauthorizedAccessCacheability()->addCacheTags($this->entity->getCacheTags());
+  }
+
+}
diff --git a/tests/src/Functional/EntityResource/WorkspaceXmlAnonTest.php b/tests/src/Functional/EntityResource/WorkspaceXmlAnonTest.php
new file mode 100644
index 0000000..5004f53
--- /dev/null
+++ b/tests/src/Functional/EntityResource/WorkspaceXmlAnonTest.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\Tests\workspace\Functional\EntityResource;
+
+use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
+use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
+
+/**
+ * Test workspace entities for unauthenticated XML requests.
+ *
+ * @group workspace
+ */
+class WorkspaceXmlAnonTest extends WorkspaceResourceTestBase {
+
+  use AnonResourceTestTrait;
+  use XmlEntityNormalizationQuirksTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $format = 'xml';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $mimeType = 'text/xml; charset=UTF-8';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function testPatchPath() {
+    // Deserialization of the XML format is not supported.
+    $this->markTestSkipped();
+  }
+
+}
diff --git a/tests/src/Functional/EntityResource/WorkspaceXmlBasicAuthTest.php b/tests/src/Functional/EntityResource/WorkspaceXmlBasicAuthTest.php
new file mode 100644
index 0000000..ac6f3c5
--- /dev/null
+++ b/tests/src/Functional/EntityResource/WorkspaceXmlBasicAuthTest.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Drupal\Tests\workspace\Functional\EntityResource;
+
+use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
+use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
+
+/**
+ * Test workspace entities for XML requests with cookie authentication.
+ *
+ * @group workspace
+ */
+class WorkspaceXmlBasicAuthTest extends WorkspaceResourceTestBase {
+
+  use BasicAuthResourceTestTrait;
+  use XmlEntityNormalizationQuirksTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['basic_auth'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $format = 'xml';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $mimeType = 'text/xml; charset=UTF-8';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $auth = 'basic_auth';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function testPatchPath() {
+    // Deserialization of the XML format is not supported.
+    $this->markTestSkipped();
+  }
+
+}
diff --git a/tests/src/Functional/EntityResource/WorkspaceXmlCookieTest.php b/tests/src/Functional/EntityResource/WorkspaceXmlCookieTest.php
new file mode 100644
index 0000000..c8a23d1
--- /dev/null
+++ b/tests/src/Functional/EntityResource/WorkspaceXmlCookieTest.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Drupal\Tests\workspace\Functional\EntityResource;
+
+use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
+use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
+
+/**
+ * Test workspace entities for XML requests.
+ *
+ * @group workspace
+ */
+class WorkspaceXmlCookieTest extends WorkspaceResourceTestBase {
+
+  use CookieResourceTestTrait;
+  use XmlEntityNormalizationQuirksTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $format = 'xml';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $mimeType = 'text/xml; charset=UTF-8';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $auth = 'cookie';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function testPatchPath() {
+    // Deserialization of the XML format is not supported.
+    $this->markTestSkipped();
+  }
+
+}
diff --git a/tests/src/Kernel/WorkspaceInternalResourceTest.php b/tests/src/Kernel/WorkspaceInternalResourceTest.php
new file mode 100644
index 0000000..08c14fc
--- /dev/null
+++ b/tests/src/Kernel/WorkspaceInternalResourceTest.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace Drupal\Tests\workspace\Kernel;
+
+use Drupal\Component\Plugin\Exception\PluginNotFoundException;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\rest\Entity\RestResourceConfig;
+use Drupal\rest\RestResourceConfigInterface;
+
+/**
+ * Tests REST module with internal workspace entity types.
+ *
+ * @group workspace
+ */
+class WorkspaceInternalResourceTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['user', 'serialization', 'rest', 'workspace'];
+
+  /**
+   * Tests enabling content workspaces for REST throws an exception.
+   *
+   * @see workspace_rest_resource_alter()
+   */
+  public function testCreateContentWorkspaceResource() {
+    $this->setExpectedException(PluginNotFoundException::class, 'The "entity:content_workspace" plugin does not exist.');
+    RestResourceConfig::create([
+      'id' => 'entity.content_workspace',
+      'granularity' => RestResourceConfigInterface::RESOURCE_GRANULARITY,
+      'configuration' => [
+        'methods' => ['GET'],
+        'formats' => ['json'],
+        'authentication' => ['cookie'],
+      ],
+    ])
+      ->enable()
+      ->save();
+  }
+
+  /**
+   * Tests enabling replication logs for REST throws an exception.
+   *
+   * @see workspace_rest_resource_alter()
+   */
+  public function testCreateReplicationLogResource() {
+    $this->setExpectedException(PluginNotFoundException::class, 'The "entity:replication_log" plugin does not exist.');
+    RestResourceConfig::create([
+      'id' => 'entity.replication_log',
+      'granularity' => RestResourceConfigInterface::RESOURCE_GRANULARITY,
+      'configuration' => [
+        'methods' => ['GET'],
+        'formats' => ['json'],
+        'authentication' => ['cookie'],
+      ],
+    ])
+      ->enable()
+      ->save();
+  }
+
+}
diff --git a/workspace.link_relation_types.yml b/workspace.link_relation_types.yml
new file mode 100644
index 0000000..d591603
--- /dev/null
+++ b/workspace.link_relation_types.yml
@@ -0,0 +1,8 @@
+# Workspace extension relation types.
+# See https://tools.ietf.org/html/rfc5988#section-4.2.
+activate-form:
+  uri: https://drupal.org/link-relations/activate-form
+  description: A form where a workspace can be activated.
+deploy-form:
+  uri: https://drupal.org/link-relations/deploy-form
+  description: A form where a workspace can be deployed.
diff --git a/workspace.module b/workspace.module
index db1e2e7..5260976 100644
--- a/workspace.module
+++ b/workspace.module
@@ -245,3 +245,13 @@ function workspace_entity_create_access(AccountInterface $account, array $contex
     ->getInstanceFromDefinition(EntityAccess::class)
     ->entityCreateAccess($account, $context, $entity_bundle);
 }
+
+/**
+ * Implements hook_rest_resource_alter().
+ */
+function workspace_rest_resource_alter(&$definitions) {
+  // ContentWorkspace and ReplicationLog are internal entity types. Therefore
+  // they should also not be exposed via REST.
+  unset($definitions['entity:content_workspace']);
+  unset($definitions['entity:replication_log']);
+}
