diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index bb4993a172..01a1eeee95 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -277,6 +277,10 @@ config_entity:
     uuid:
       type: string
       label: 'UUID'
+      constraints:
+        Length:
+          max: 128
+          maxMessage: 'UUID: may not be longer than 128 characters.'
     langcode:
       type: string
       label: 'Language code'
diff --git a/core/lib/Drupal/Core/Config/StorableConfigBase.php b/core/lib/Drupal/Core/Config/StorableConfigBase.php
index 0751e9f59f..1c8fb2c828 100644
--- a/core/lib/Drupal/Core/Config/StorableConfigBase.php
+++ b/core/lib/Drupal/Core/Config/StorableConfigBase.php
@@ -129,7 +129,7 @@ public function getStorage() {
    *
    * @return \Drupal\Core\Config\Schema\Element
    */
-  protected function getSchemaWrapper() {
+  public function getSchemaWrapper() {
     if (!isset($this->schemaWrapper)) {
       $definition = $this->typedConfigManager->getDefinition($this->name);
       $data_definition = $this->typedConfigManager->buildDataDefinition($definition, $this->data);
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityType.php b/core/lib/Drupal/Core/Entity/ContentEntityType.php
index 0e26c3bb51..c4033b5dbb 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityType.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityType.php
@@ -17,6 +17,11 @@ class ContentEntityType extends EntityType implements ContentEntityTypeInterface
   /**
    * {@inheritdoc}
    */
+  protected $supports_validation = TRUE;
+
+  /**
+   * {@inheritdoc}
+   */
   public function __construct($definition) {
     parent::__construct($definition);
     $this->handlers += [
diff --git a/core/lib/Drupal/Core/Entity/EntityType.php b/core/lib/Drupal/Core/Entity/EntityType.php
index b39fb3fa46..97cdda785f 100644
--- a/core/lib/Drupal/Core/Entity/EntityType.php
+++ b/core/lib/Drupal/Core/Entity/EntityType.php
@@ -278,6 +278,13 @@ class EntityType extends PluginDefinition implements EntityTypeInterface {
   protected $additional = [];
 
   /**
+   * Entity type supports validation.
+   *
+   * @var bool
+   */
+  protected $supports_validation = FALSE;
+
+  /**
    * Constructs a new EntityType.
    *
    * @param array $definition
diff --git a/core/modules/config/src/Tests/ConfigEntityListTest.php b/core/modules/config/src/Tests/ConfigEntityListTest.php
index e9950ea424..cb0c730b33 100644
--- a/core/modules/config/src/Tests/ConfigEntityListTest.php
+++ b/core/modules/config/src/Tests/ConfigEntityListTest.php
@@ -29,6 +29,8 @@ protected function setUp() {
     // test.
     \Drupal::entityManager()->getStorage('config_test')->load('override')->delete();
     $this->drupalPlaceBlock('local_actions_block');
+
+    $this->drupalLogin($this->createUser(['view config_test', 'administer config_test']));
   }
 
   /**
@@ -149,7 +151,7 @@ public function testList() {
    */
   public function testListUI() {
     // Log in as an administrative user to access the full menu trail.
-    $this->drupalLogin($this->drupalCreateUser(['access administration pages', 'administer site configuration']));
+    $this->drupalLogin($this->drupalCreateUser(['access administration pages', 'administer site configuration', 'administer config_test']));
 
     // Get the list callback page.
     $this->drupalGet('admin/structure/config_test');
diff --git a/core/modules/config/src/Tests/ConfigEntityTest.php b/core/modules/config/src/Tests/ConfigEntityTest.php
index c0dc97bc7b..78cd31e705 100644
--- a/core/modules/config/src/Tests/ConfigEntityTest.php
+++ b/core/modules/config/src/Tests/ConfigEntityTest.php
@@ -231,7 +231,7 @@ public function testCRUD() {
    * Tests CRUD operations through the UI.
    */
   public function testCRUDUI() {
-    $this->drupalLogin($this->drupalCreateUser(['administer site configuration']));
+    $this->drupalLogin($this->drupalCreateUser(['administer site configuration', 'administer config_test']));
 
     $id = strtolower($this->randomMachineName());
     $label1 = $this->randomMachineName();
diff --git a/core/modules/config/tests/config_test/config_test.permissions.yml b/core/modules/config/tests/config_test/config_test.permissions.yml
new file mode 100644
index 0000000000..eaf1be61b7
--- /dev/null
+++ b/core/modules/config/tests/config_test/config_test.permissions.yml
@@ -0,0 +1,4 @@
+view config_test:
+  title: 'View ConfigTest entities'
+administer config_test:
+  title: 'Administer ConfigTest entities'
diff --git a/core/modules/config/tests/config_test/src/ConfigTestAccessControlHandler.php b/core/modules/config/tests/config_test/src/ConfigTestAccessControlHandler.php
index 88896f0b96..0aeb6e0d63 100644
--- a/core/modules/config/tests/config_test/src/ConfigTestAccessControlHandler.php
+++ b/core/modules/config/tests/config_test/src/ConfigTestAccessControlHandler.php
@@ -18,14 +18,17 @@ class ConfigTestAccessControlHandler extends EntityAccessControlHandler {
    * {@inheritdoc}
    */
   public function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
-    return AccessResult::allowed();
+    if ($operation === 'view') {
+      return AccessResult::allowedIfHasPermission($account, 'view config_test');
+    }
+    return AccessResult::allowedIfHasPermission($account, 'administer config_test');
   }
 
   /**
    * {@inheritdoc}
    */
   protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
-    return AccessResult::allowed();
+    return AccessResult::allowedIfHasPermission($account, 'administer config_test');
   }
 
 }
diff --git a/core/modules/config/tests/config_test/src/Entity/ConfigTest.php b/core/modules/config/tests/config_test/src/Entity/ConfigTest.php
index 1bd1d8dee9..4f84ecfe1f 100644
--- a/core/modules/config/tests/config_test/src/Entity/ConfigTest.php
+++ b/core/modules/config/tests/config_test/src/Entity/ConfigTest.php
@@ -22,6 +22,7 @@
  *     },
  *     "access" = "Drupal\config_test\ConfigTestAccessControlHandler"
  *   },
+ *   supports_validation = TRUE,
  *   config_prefix = "dynamic",
  *   entity_keys = {
  *     "id" = "id",
diff --git a/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceIntegrationTest.php b/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceIntegrationTest.php
index 56484705e1..14b2cae406 100644
--- a/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceIntegrationTest.php
+++ b/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceIntegrationTest.php
@@ -54,7 +54,7 @@ protected function setUp() {
     parent::setUp();
 
     // Create a test user.
-    $web_user = $this->drupalCreateUser(['administer entity_test content', 'administer entity_test fields', 'view test entity']);
+    $web_user = $this->drupalCreateUser(['administer entity_test content', 'administer entity_test fields', 'view test entity', 'view config_test', 'administer config_test']);
     $this->drupalLogin($web_user);
   }
 
diff --git a/core/modules/rest/src/Plugin/Deriver/EntityDeriver.php b/core/modules/rest/src/Plugin/Deriver/EntityDeriver.php
index 010fad7363..634a1bdd31 100644
--- a/core/modules/rest/src/Plugin/Deriver/EntityDeriver.php
+++ b/core/modules/rest/src/Plugin/Deriver/EntityDeriver.php
@@ -2,8 +2,10 @@
 
 namespace Drupal\rest\Plugin\Deriver;
 
+use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
+use Drupal\rest\Plugin\rest\resource\ConfigEntityResource;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -88,6 +90,10 @@ public function getDerivativeDefinitions($base_plugin_definition) {
           }
         }
 
+        if ($entity_type instanceof ConfigEntityTypeInterface) {
+          $this->derivatives[$entity_type_id]['class'] = ConfigEntityResource::class;
+        }
+
         $this->derivatives[$entity_type_id] += $base_plugin_definition;
       }
     }
diff --git a/core/modules/rest/src/Plugin/rest/resource/ConfigEntityResource.php b/core/modules/rest/src/Plugin/rest/resource/ConfigEntityResource.php
new file mode 100644
index 0000000000..d4b98c342f
--- /dev/null
+++ b/core/modules/rest/src/Plugin/rest/resource/ConfigEntityResource.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\rest\Plugin\rest\resource;
+
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Specific config entity resource with special behaviour for validation.
+ */
+class ConfigEntityResource extends EntityResource {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function validate(EntityInterface $entity) {
+    /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */
+    $config = \Drupal::configFactory()->getEditable($entity->getConfigDependencyName());
+    $config->setData($entity->toArray());
+
+    $typed_config = $config->getSchemaWrapper();
+
+    $violations = $typed_config->validate();
+
+    $this->processViolations($violations);
+  }
+
+}
diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
index a9e0ff0e86..834c2bbaf8 100644
--- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
@@ -264,36 +264,38 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
 
     // Overwrite the received properties.
     $entity_keys = $entity->getEntityType()->getKeys();
-    foreach ($entity->_restSubmittedFields as $field_name) {
-      $field = $entity->get($field_name);
-
-      // Entity key fields need special treatment: together they uniquely
-      // identify the entity. Therefore it does not make sense to modify any of
-      // them. However, rather than throwing an error, we just ignore them as
-      // long as their specified values match their current values.
-      if (in_array($field_name, $entity_keys, TRUE)) {
-        // @todo Work around the wrong assumption that entity keys need special
-        // treatment, when only read-only fields need it.
-        // This will be fixed in https://www.drupal.org/node/2824851.
-        if ($entity->getEntityTypeId() == 'comment' && $field_name == 'status' && !$original_entity->get($field_name)->access('edit')) {
-          throw new AccessDeniedHttpException("Access denied on updating field '$field_name'.");
+    if ($entity instanceof FieldableEntityInterface) {
+      foreach ($entity->_restSubmittedFields as $field_name) {
+        $field = $entity->get($field_name);
+
+        // Entity key fields need special treatment: together they uniquely
+        // identify the entity. Therefore it does not make sense to modify any of
+        // them. However, rather than throwing an error, we just ignore them as
+        // long as their specified values match their current values.
+        if (in_array($field_name, $entity_keys, TRUE)) {
+          // @todo Work around the wrong assumption that entity keys need special
+          // treatment, when only read-only fields need it.
+          // This will be fixed in https://www.drupal.org/node/2824851.
+          if ($entity->getEntityTypeId() == 'comment' && $field_name == 'status' && !$original_entity->get($field_name)->access('edit')) {
+            throw new AccessDeniedHttpException("Access denied on updating field '$field_name'.");
+          }
+
+          // Unchanged values for entity keys don't need access checking.
+          if ($this->getCastedValueFromFieldItemList($original_entity->get($field_name)) === $this->getCastedValueFromFieldItemList($entity->get($field_name))) {
+            continue;
+          }
+          // It is not possible to set the language to NULL as it is automatically
+          // re-initialized. As it must not be empty, skip it if it is.
+          elseif (isset($entity_keys['langcode']) && $field_name === $entity_keys['langcode'] && $field->isEmpty()) {
+            continue;
+          }
         }
 
-        // Unchanged values for entity keys don't need access checking.
-        if ($this->getCastedValueFromFieldItemList($original_entity->get($field_name)) === $this->getCastedValueFromFieldItemList($entity->get($field_name))) {
-          continue;
-        }
-        // It is not possible to set the language to NULL as it is automatically
-        // re-initialized. As it must not be empty, skip it if it is.
-        elseif (isset($entity_keys['langcode']) && $field_name === $entity_keys['langcode'] && $field->isEmpty()) {
-          continue;
+        if (!$original_entity->get($field_name)->access('edit')) {
+          throw new AccessDeniedHttpException("Access denied on updating field '$field_name'.");
         }
+        $original_entity->set($field_name, $field->getValue());
       }
-
-      if (!$original_entity->get($field_name)->access('edit')) {
-        throw new AccessDeniedHttpException("Access denied on updating field '$field_name'.");
-      }
-      $original_entity->set($field_name, $field->getValue());
     }
 
     // Validate the received data before saving.
@@ -392,7 +394,7 @@ protected function getBaseRoute($canonical_path, $method) {
    */
   public function availableMethods() {
     $methods = parent::availableMethods();
-    if ($this->isConfigEntityResource()) {
+    if (!$this->entityType->get('supports_validation')) {
       // Currently only GET is supported for Config Entities.
       // @todo Remove when supported https://www.drupal.org/node/2300677
       $unsupported_methods = ['POST', 'PUT', 'DELETE', 'PATCH'];
diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php b/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php
index 7bf8e824e1..4480a85bcb 100644
--- a/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php
@@ -3,6 +3,7 @@
 namespace Drupal\rest\Plugin\rest\resource;
 
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\FieldableEntityInterface;
 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 
 /**
@@ -22,6 +23,10 @@
    *   field.
    */
   protected function checkEditFieldAccess(EntityInterface $entity) {
+    if (!$entity instanceof FieldableEntityInterface) {
+      return;
+    }
+
     // Only check 'edit' permissions for fields that were actually submitted by
     // the user. Field access makes no difference between 'create' and 'update',
     // so the 'edit' operation is used here.
diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php b/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php
index 09b4b64bae..de1bb77f03 100644
--- a/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php
@@ -33,6 +33,19 @@ protected function validate(EntityInterface $entity) {
     // changes.
     $violations->filterByFieldAccess();
 
+    $this->processViolations($violations);
+  }
+
+  /**
+   * Processes violations and creates a helpful exception message.
+   *
+   * @param \Drupal\Core\Entity\EntityConstraintViolationListInterface $violations
+   *   The entity constraint violations to process.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException
+   *   Throws a HTTP exception when the validation fails.
+   */
+  protected function processViolations($violations) {
     if ($violations->count() > 0) {
       $message = "Unprocessable Entity: validation failed.\n";
       foreach ($violations as $violation) {
diff --git a/core/modules/rest/tests/modules/config_test_rest/config_test_rest.module b/core/modules/rest/tests/modules/config_test_rest/config_test_rest.module
index fcd9979a11..0cffb9c1f4 100644
--- a/core/modules/rest/tests/modules/config_test_rest/config_test_rest.module
+++ b/core/modules/rest/tests/modules/config_test_rest/config_test_rest.module
@@ -26,5 +26,8 @@ function config_test_rest_config_test_access(EntityInterface $entity, $operation
   // Add permission, so that EntityResourceTestBase's scenarios can test access
   // being denied. By default, all access is always allowed for the config_test
   // config entity.
-  return AccessResult::forbiddenIf(!$account->hasPermission('view config_test'))->cachePerPermissions();
+  if ($operation === 'view') {
+    return AccessResult::forbiddenIf(!$account->hasPermission('view config_test'))->cachePerPermissions();
+  }
+  return AccessResult::neutral();
 }
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestResourceTestBase.php
index 9fe073b097..b89a2718dc 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestResourceTestBase.php
@@ -23,10 +23,31 @@
   protected $entity;
 
   /**
+   * Counter used internally to have deterministic IDs.
+   *
+   * @var int
+   */
+  protected $counter = 0;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $firstCreatedEntityId = 'llama';
+
+  /**
    * {@inheritdoc}
    */
   protected function setUpAuthorization($method) {
-    $this->grantPermissionsToTestedRole(['view config_test']);
+    switch ($method) {
+      case 'GET':
+        $this->grantPermissionsToTestedRole(['view config_test']);
+        break;
+      case 'POST':
+      case 'PATCH':
+      case 'DELETE':
+        $this->grantPermissionsToTestedRole(['administer config_test']);
+        break;
+    }
   }
 
   /**
@@ -67,7 +88,33 @@ protected function getExpectedNormalizedEntity() {
    * {@inheritdoc}
    */
   protected function getNormalizedPostEntity() {
-    // @todo Update in https://www.drupal.org/node/2300677.
+    $this->counter++;
+    return [
+      'id' => 'llama' . (string) $this->counter,
+      'label' => 'Llamam',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function makeNormalizationInvalid(array $normalization) {
+    $normalization['label'] = ['foo', 'bar'];
+    return $normalization;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedUnauthorizedAccessMessage($method) {
+    if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
+      return parent::getExpectedUnauthorizedAccessMessage($method);
+    }
+
+    if ($method === 'GET') {
+      return 'You are not authorized to view this config_test entity.';
+    }
+    return "The 'administer config_test' permission is required.";
   }
 
 }
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
index a55ae929e9..42c42e5bb5 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -75,7 +75,7 @@
    *
    * @var string[]
    */
-  protected static $patchProtectedFieldNames;
+  protected static $patchProtectedFieldNames = [];
 
   /**
    * Optionally specify which field is the 'label' field. Some entities specify
@@ -585,8 +585,8 @@ protected static function castToString(array $normalization) {
    */
   public function testPost() {
     // @todo Remove this in https://www.drupal.org/node/2300677.
-    if ($this->entity instanceof ConfigEntityInterface) {
-      $this->assertTrue(TRUE, 'POSTing config entities is not yet supported.');
+    if (!$this->entity->getEntityType()->get('supports_validation')) {
+      $this->assertTrue(TRUE, "This entity type doesn't support posting.");
       return;
     }
 
@@ -598,7 +598,14 @@ public function testPost() {
     $parseable_valid_request_body   = $this->serializer->encode($this->getNormalizedPostEntity(), static::$format);
     $parseable_valid_request_body_2 = $this->serializer->encode($this->getNormalizedPostEntity(), static::$format);
     $parseable_invalid_request_body   = $this->serializer->encode($this->makeNormalizationInvalid($this->getNormalizedPostEntity()), static::$format);
-    $parseable_invalid_request_body_2 = $this->serializer->encode($this->getNormalizedPostEntity() + ['uuid' => [$this->randomMachineName(129)]], static::$format);
+    // The normalized structure is different for fieldable and non-fieldable
+    // entities.
+    if ($this->entity instanceof FieldableEntityInterface) {
+      $parseable_invalid_request_body_2 = $this->serializer->encode($this->getNormalizedPostEntity() + ['uuid' => [$this->randomMachineName(129)]], static::$format);
+    }
+    else {
+      $parseable_invalid_request_body_2 = $this->serializer->encode($this->getNormalizedPostEntity() + ['uuid' => $this->randomMachineName(129)], static::$format);
+    }
     $parseable_invalid_request_body_3 = $this->serializer->encode($this->getNormalizedPostEntity() + ['field_rest_test' => [['value' => $this->randomString()]]], static::$format);
 
     // The URL and Guzzle request options that will be used in this test. The
@@ -686,10 +693,15 @@ public function testPost() {
 
     // DX: 422 when invalid entity: multiple values sent for single-value field.
     $response = $this->request('POST', $url, $request_options);
-    $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName;
-    $label_field_capitalized = $this->entity->getFieldDefinition($label_field)->getLabel();
-    $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n", $response);
 
+    if ($this->entity instanceof ConfigEntityInterface) {
+      $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nlabel: This value should be of the correct primitive type.\n", $response);
+    }
+    else {
+      $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName;
+      $label_field_capitalized = $this->entity instanceof FieldableEntityInterface ? $this->entity->getFieldDefinition($label_field)->getLabel() : 'label';
+      $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n", $response);
+    }
 
     $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_2;
 
@@ -698,16 +710,23 @@ public function testPost() {
     // @todo Fix this in https://www.drupal.org/node/2149851.
     if ($this->entity->getEntityType()->hasKey('uuid')) {
       $response = $this->request('POST', $url, $request_options);
-      $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nuuid.0.value: UUID: may not be longer than 128 characters.\n", $response);
+      if ($this->entity instanceof FieldableEntityInterface) {
+        $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nuuid.0.value: UUID: may not be longer than 128 characters.\n", $response);
+      }
+      else {
+        $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nuuid: UUID: may not be longer than 128 characters.\n", $response);
+      }
     }
 
 
-    $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_3;
+    if ($this->entity instanceof FieldableEntityInterface) {
+      $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_3;
 
 
-    // DX: 403 when entity contains field without 'edit' access.
-    $response = $this->request('POST', $url, $request_options);
-    $this->assertResourceErrorResponse(403, "Access denied on creating field 'field_rest_test'.", $response);
+      // DX: 403 when entity contains field without 'edit' access.
+      $response = $this->request('POST', $url, $request_options);
+      $this->assertResourceErrorResponse(403, "Access denied on creating field 'field_rest_test'.", $response);
+    }
 
 
     $request_options[RequestOptions::BODY] = $parseable_valid_request_body;
@@ -775,9 +794,8 @@ public function testPost() {
    * Tests a PATCH request for an entity, plus edge cases to ensure good DX.
    */
   public function testPatch() {
-    // @todo Remove this in https://www.drupal.org/node/2300677.
-    if ($this->entity instanceof ConfigEntityInterface) {
-      $this->assertTrue(TRUE, 'PATCHing config entities is not yet supported.');
+    if (!$this->entity->getEntityType()->get('supports_validation')) {
+      $this->assertTrue(TRUE, "This entity type doesn't support patching.");
       return;
     }
 
@@ -887,18 +905,22 @@ public function testPatch() {
 
 
     // DX: 422 when invalid entity: multiple values sent for single-value field.
-    $response = $this->request('PATCH', $url, $request_options);
-    $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName;
-    $label_field_capitalized = $this->entity->getFieldDefinition($label_field)->getLabel();
-    $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n", $response);
+    if ($this->entity instanceof FieldableEntityInterface) {
+      $response = $this->request('PATCH', $url, $request_options);
+      $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName;
+      $label_field_capitalized = $this->entity->getFieldDefinition($label_field)->getLabel();
+      $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n", $response);
+    }
 
 
     $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_2;
 
 
     // DX: 403 when entity contains field without 'edit' access.
-    $response = $this->request('PATCH', $url, $request_options);
-    $this->assertResourceErrorResponse(403, "Access denied on updating field 'field_rest_test'.", $response);
+    if ($this->entity instanceof FieldableEntityInterface) {
+      $response = $this->request('PATCH', $url, $request_options);
+      $this->assertResourceErrorResponse(403, "Access denied on updating field 'field_rest_test'.", $response);
+    }
 
 
     // DX: 403 when sending PATCH request with read-only fields.
@@ -941,10 +963,12 @@ public function testPatch() {
     $response = $this->request('PATCH', $url, $request_options);
     $this->assertResourceResponse(200, FALSE, $response);
     $this->assertFalse($response->hasHeader('X-Drupal-Cache'));
-    // Ensure that fields do not get deleted if they're not present in the PATCH
-    // request. Test this using the configurable field that we added, but which
-    // is not sent in the PATCH request.
-    $this->assertSame('All the faith he had had had had no effect on the outcome of his life.', $this->entityStorage->loadUnchanged($this->entity->id())->get('field_rest_test')->value);
+    if ($this->entity instanceof FieldableEntityInterface) {
+      // Ensure that fields do not get deleted if they're not present in the PATCH
+      // request. Test this using the configurable field that we added, but which
+      // is not sent in the PATCH request.
+      $this->assertSame('All the faith he had had had had no effect on the outcome of his life.', $this->entityStorage->loadUnchanged($this->entity->id())->get('field_rest_test')->value);
+    }
 
 
     $this->config('rest.settings')->set('bc_entity_resource_permissions', TRUE)->save(TRUE);
@@ -970,9 +994,8 @@ public function testPatch() {
    * Tests a DELETE request for an entity, plus edge cases to ensure good DX.
    */
   public function testDelete() {
-    // @todo Remove this in https://www.drupal.org/node/2300677.
-    if ($this->entity instanceof ConfigEntityInterface) {
-      $this->assertTrue(TRUE, 'DELETEing config entities is not yet supported.');
+    if (!$this->entity->getEntityType()->get('supports_validation')) {
+      $this->assertTrue(TRUE, "This entity type doesn't support deletion.");
       return;
     }
 
@@ -1145,7 +1168,16 @@ protected function getPostUrl() {
   protected function makeNormalizationInvalid(array $normalization) {
     // Add a second label to this entity to make it invalid.
     $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName;
-    $normalization[$label_field][1]['value'] = 'Second Title';
+
+    if ($this->entity instanceof FieldableEntityInterface) {
+      $normalization[$label_field][1]['value'] = 'Second Title';
+    }
+    else {
+      $normalization[$label_field] = [
+        $normalization[$label_field],
+        'Second title',
+      ];
+    }
 
     return $normalization;
   }
diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php
index fb3573eec9..7145520b72 100644
--- a/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php
@@ -172,6 +172,12 @@ public function testSchemaMapping() {
     $expected['mapping']['name']['type'] = 'string';
     $expected['mapping']['uuid']['type'] = 'string';
     $expected['mapping']['uuid']['label'] = 'UUID';
+    $expected['mapping']['uuid']['constraints'] = [
+      'Length' => [
+        'max' => 128,
+        'maxMessage' => 'UUID: may not be longer than 128 characters.',
+      ],
+    ];
     $expected['mapping']['langcode']['type'] = 'string';
     $expected['mapping']['langcode']['label'] = 'Language code';
     $expected['mapping']['status']['type'] = 'boolean';
