diff --git a/nova/api/openstack/placement/deploy.py b/nova/api/openstack/placement/deploy.py
index 1edf9724b..34001f34a 100644
--- a/nova/api/openstack/placement/deploy.py
+++ b/nova/api/openstack/placement/deploy.py
@@ -12,7 +12,8 @@
 """Deployment handling for Placmenent API."""
 
 from keystonemiddleware import auth_token
-from oslo_middleware import request_id
+import oslo_middleware
+from oslo_middleware import cors
 
 from nova.api import openstack as common_api
 from nova.api.openstack.placement import auth
@@ -41,8 +42,18 @@ def deploy(conf, project_name):
         auth_middleware = auth_token.filter_factory(
             {}, oslo_config_project=project_name)
 
+    # Pass in our CORS config, if any, manually as that's a)
+    # explicit, b) makes testing more straightfoward, c) let's
+    # us control the use of cors by the presence of its config.
+    conf.register_opts(cors.CORS_OPTS, 'cors')
+    if conf.cors.allowed_origin:
+        cors_middleware = oslo_middleware.CORS.factory(
+            {}, **conf.cors)
+    else:
+        cors_middleware = None
+
     context_middleware = auth.PlacementKeystoneContext
-    req_id_middleware = request_id.RequestId
+    req_id_middleware = oslo_middleware.RequestId
     microversion_middleware = microversion.MicroversionMiddleware
     fault_wrap = common_api.FaultWrapper
     request_log = requestlog.RequestLog
@@ -62,9 +73,11 @@ def deploy(conf, project_name):
                        request_log,
                        context_middleware,
                        auth_middleware,
+                       cors_middleware,
                        req_id_middleware,
                        ):
-        application = middleware(application)
+        if middleware:
+            application = middleware(application)
 
     return application
 
diff --git a/nova/tests/functional/api/openstack/placement/fixtures.py b/nova/tests/functional/api/openstack/placement/fixtures.py
index 8a3d6106b..f766728ae 100644
--- a/nova/tests/functional/api/openstack/placement/fixtures.py
+++ b/nova/tests/functional/api/openstack/placement/fixtures.py
@@ -13,6 +13,7 @@
 import os
 
 from gabbi import fixture
+from oslo_middleware import cors
 from oslo_utils import uuidutils
 
 from nova.api.openstack.placement import deploy
@@ -55,7 +56,16 @@ class APIFixture(fixture.GabbiFixture):
                                group='api_database')
         self.conf.set_override('connection', "sqlite://",
                                group='placement_database')
-        config.parse_args([], default_config_files=None, configure_db=False,
+
+        # Register CORS opts, but do not set config. This has the
+        # effect of exercising the "don't use cors" path in
+        # deploy.py. Without setting some config the group will not
+        # be present.
+        self.conf.register_opts(cors.CORS_OPTS, 'cors')
+
+        # Make sure default_config_files is an empty list, not None.
+        # If None /etc/nova/nova.conf is read and confuses results.
+        config.parse_args([], default_config_files=[], configure_db=False,
                           init_rpc=False)
 
         # NOTE(cdent): api and main database are not used but we still need
@@ -136,3 +146,15 @@ class AllocationFixture(APIFixture):
         # not been created in the Allocation fixture
         os.environ['ALT_RP_UUID'] = uuidutils.generate_uuid()
         os.environ['ALT_RP_NAME'] = uuidutils.generate_uuid()
+
+
+class CORSFixture(APIFixture):
+    """An APIFixture that turns on CORS."""
+
+    def start_fixture(self):
+        super(CORSFixture, self).start_fixture()
+        # NOTE(cdent): If we remove this override, then the cors
+        # group ends up not existing in the conf, so when deploy.py
+        # wants to load the CORS middleware, it will not.
+        self.conf.set_override('allowed_origin', 'http://valid.example.com',
+                               group='cors')
diff --git a/nova/tests/functional/api/openstack/placement/gabbits/basic-http.yaml b/nova/tests/functional/api/openstack/placement/gabbits/basic-http.yaml
index 00757ebba..fae0d2c24 100644
--- a/nova/tests/functional/api/openstack/placement/gabbits/basic-http.yaml
+++ b/nova/tests/functional/api/openstack/placement/gabbits/basic-http.yaml
@@ -52,6 +52,16 @@ tests:
   response_strings:
     - The method DELETE is not allowed for this resource.
 
+- name: 405 on bad options method on app
+  OPTIONS: /resource_providers
+  status: 405
+  response_headers:
+      allow: /(GET|POST), (POST|GET)/
+  response_json_paths:
+      $.errors[0].title: Method Not Allowed
+  response_strings:
+    - The method OPTIONS is not allowed for this resource.
+
 - name: bad accept resource providers
   GET: /resource_providers
   request_headers:
diff --git a/nova/tests/functional/api/openstack/placement/gabbits/cors.yaml b/nova/tests/functional/api/openstack/placement/gabbits/cors.yaml
new file mode 100644
index 000000000..291e1d5f7
--- /dev/null
+++ b/nova/tests/functional/api/openstack/placement/gabbits/cors.yaml
@@ -0,0 +1,47 @@
+# Confirm that CORS is present. No complex configuration is done so
+# this just tests the basics. Borrowed, in spirit, from
+# nova.tests.functional.test_middleware.
+
+fixtures:
+    - CORSFixture
+
+defaults:
+    request_headers:
+        x-auth-token: user
+
+tests:
+- name: valid options request
+  OPTIONS: /
+  request_headers:
+      origin: http://valid.example.com
+      access-control-request-method: GET
+  status: 200
+  response_headers:
+      access-control-allow-origin: http://valid.example.com
+
+- name: invalid options request
+  OPTIONS: /
+  request_headers:
+      origin: http://invalid.example.com
+      access-control-request-method: GET
+  status: 200
+  response_forbidden_headers:
+      - access-control-allow-origin
+
+- name: valid get request
+  GET: /
+  request_headers:
+      origin: http://valid.example.com
+      access-control-request-method: GET
+  status: 200
+  response_headers:
+      access-control-allow-origin: http://valid.example.com
+
+- name: invalid get request
+  GET: /
+  request_headers:
+      origin: http://invalid.example.com
+      access-control-request-method: GET
+  status: 200
+  response_forbidden_headers:
+      - access-control-allow-origin
diff --git a/nova/tests/functional/api/openstack/placement/gabbits/non-cors.yaml b/nova/tests/functional/api/openstack/placement/gabbits/non-cors.yaml
new file mode 100644
index 000000000..b0b974cc5
--- /dev/null
+++ b/nova/tests/functional/api/openstack/placement/gabbits/non-cors.yaml
@@ -0,0 +1,25 @@
+# Confirm that things work as intended when CORS is not configured.
+
+fixtures:
+    - APIFixture
+
+defaults:
+    request_headers:
+        x-auth-token: user
+
+tests:
+- name: options request not allowed
+  OPTIONS: /
+  request_headers:
+      origin: http://valid.example.com
+      access-control-request-method: GET
+  status: 405
+
+- name: get request no cors headers
+  GET: /
+  request_headers:
+      origin: http://valid.example.com
+      access-control-request-method: GET
+  status: 200
+  response_forbidden_headers:
+      - access-control-allow-origin
diff --git a/releasenotes/notes/placement-cors-c7a83e8c63787736.yaml b/releasenotes/notes/placement-cors-c7a83e8c63787736.yaml
new file mode 100644
index 000000000..6ea9af445
--- /dev/null
+++ b/releasenotes/notes/placement-cors-c7a83e8c63787736.yaml
@@ -0,0 +1,9 @@
+---
+features:
+  - |
+    The placement API service can now be configured to support
+    `CORS <http://docs.openstack.org/developer/oslo.middleware/cors.html>`_.
+    If a `cors` configuration group is present in the service's configuration
+    file (currently `nova.conf`), with `allowed_origin` configured, the values
+    within will be used to configure the middleware. If `cors.allowed_origin`
+    is not set, the middleware will not be used.