Add policy rule to block image-backed servers with 0 root disk flavor
This adds a new policy rule which defaults to behave in a backward compatible way, but will allow operators to enforce that servers created with a zero disk flavor must also be volume-backed servers. Allowing users to upload their own images and create image-backed servers on local disk with zero root disk size flavors can be potentially hazardous if the size of the image is unexpectedly large, since it can consume the local disk (or shared storage pool). It should be noted that disabling the new policy rule will result in a non-backward compatible API behavior change and no microversion is being introduced for this because enforcement via a new microversion would not close the security gap on any previous microversions. Related compute API reference and user documentation is updated to mention the policy rule along with a release note since this is tied to a security bug, which will be backported to stable branches. Change-Id: Id67e1285a0522474844de130c9263e11868f67fb Closes-Bug: #1739646
This commit is contained in:
parent
bfeea18358
commit
763fd62464
@ -2714,7 +2714,9 @@ flavor_disk:
|
|||||||
deploy the instance. However, in this case filter scheduler cannot
|
deploy the instance. However, in this case filter scheduler cannot
|
||||||
select the compute host based on the virtual image size. Therefore,
|
select the compute host based on the virtual image size. Therefore,
|
||||||
0 should only be used for volume booted instances or for testing
|
0 should only be used for volume booted instances or for testing
|
||||||
purposes.
|
purposes. Volume-backed instances can be enforced for flavors with
|
||||||
|
zero root disk via the ``os_compute_api:servers:create:zero_disk_flavor``
|
||||||
|
policy rule.
|
||||||
flavor_disk_2_47:
|
flavor_disk_2_47:
|
||||||
min_version: 2.47
|
min_version: 2.47
|
||||||
in: body
|
in: body
|
||||||
|
@ -43,7 +43,9 @@ Root Disk GB
|
|||||||
case which uses the native base image size as the size of the ephemeral root
|
case which uses the native base image size as the size of the ephemeral root
|
||||||
volume. However, in this case the filter scheduler cannot select the compute
|
volume. However, in this case the filter scheduler cannot select the compute
|
||||||
host based on the virtual image size. As a result, ``0`` should only be used
|
host based on the virtual image size. As a result, ``0`` should only be used
|
||||||
for volume booted instances or for testing purposes.
|
for volume booted instances or for testing purposes. Volume-backed instances
|
||||||
|
can be enforced for flavors with zero root disk via the
|
||||||
|
``os_compute_api:servers:create:zero_disk_flavor`` policy rule.
|
||||||
|
|
||||||
Ephemeral Disk GB
|
Ephemeral Disk GB
|
||||||
Amount of disk space (in gigabytes) to use for the ephemeral partition. This
|
Amount of disk space (in gigabytes) to use for the ephemeral partition. This
|
||||||
|
@ -593,7 +593,8 @@ class ServersController(wsgi.Controller):
|
|||||||
except exception.ConfigDriveInvalidValue:
|
except exception.ConfigDriveInvalidValue:
|
||||||
msg = _("Invalid config_drive provided.")
|
msg = _("Invalid config_drive provided.")
|
||||||
raise exc.HTTPBadRequest(explanation=msg)
|
raise exc.HTTPBadRequest(explanation=msg)
|
||||||
except exception.ExternalNetworkAttachForbidden as error:
|
except (exception.BootFromVolumeRequiredForZeroDiskFlavor,
|
||||||
|
exception.ExternalNetworkAttachForbidden) as error:
|
||||||
raise exc.HTTPForbidden(explanation=error.format_message())
|
raise exc.HTTPForbidden(explanation=error.format_message())
|
||||||
except messaging.RemoteError as err:
|
except messaging.RemoteError as err:
|
||||||
msg = "%(err_type)s: %(err_msg)s" % {'err_type': err.exc_type,
|
msg = "%(err_type)s: %(err_msg)s" % {'err_type': err.exc_type,
|
||||||
|
@ -72,6 +72,7 @@ from nova.objects import fields as fields_obj
|
|||||||
from nova.objects import keypair as keypair_obj
|
from nova.objects import keypair as keypair_obj
|
||||||
from nova.objects import quotas as quotas_obj
|
from nova.objects import quotas as quotas_obj
|
||||||
from nova.pci import request as pci_request
|
from nova.pci import request as pci_request
|
||||||
|
from nova.policies import servers as servers_policies
|
||||||
import nova.policy
|
import nova.policy
|
||||||
from nova import profiler
|
from nova import profiler
|
||||||
from nova import rpc
|
from nova import rpc
|
||||||
@ -605,6 +606,15 @@ class API(base.Base):
|
|||||||
if image_min_disk > dest_size:
|
if image_min_disk > dest_size:
|
||||||
raise exception.FlavorDiskSmallerThanMinDisk(
|
raise exception.FlavorDiskSmallerThanMinDisk(
|
||||||
flavor_size=dest_size, image_min_disk=image_min_disk)
|
flavor_size=dest_size, image_min_disk=image_min_disk)
|
||||||
|
else:
|
||||||
|
# The user is attempting to create a server with a 0-disk
|
||||||
|
# image-backed flavor, which can lead to issues with a large
|
||||||
|
# image consuming an unexpectedly large amount of local disk
|
||||||
|
# on the compute host. Check to see if the deployment will
|
||||||
|
# allow that.
|
||||||
|
if not context.can(
|
||||||
|
servers_policies.ZERO_DISK_FLAVOR, fatal=False):
|
||||||
|
raise exception.BootFromVolumeRequiredForZeroDiskFlavor()
|
||||||
|
|
||||||
def _get_image_defined_bdms(self, instance_type, image_meta,
|
def _get_image_defined_bdms(self, instance_type, image_meta,
|
||||||
root_device_name):
|
root_device_name):
|
||||||
|
@ -1402,6 +1402,11 @@ class VolumeSmallerThanMinDisk(FlavorDiskTooSmall):
|
|||||||
"size is %(image_min_disk)i bytes.")
|
"size is %(image_min_disk)i bytes.")
|
||||||
|
|
||||||
|
|
||||||
|
class BootFromVolumeRequiredForZeroDiskFlavor(Forbidden):
|
||||||
|
msg_fmt = _("Only volume-backed servers are allowed for flavors with "
|
||||||
|
"zero disk.")
|
||||||
|
|
||||||
|
|
||||||
class InsufficientFreeMemory(NovaException):
|
class InsufficientFreeMemory(NovaException):
|
||||||
msg_fmt = _("Insufficient free memory on compute node to start %(uuid)s.")
|
msg_fmt = _("Insufficient free memory on compute node to start %(uuid)s.")
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ from nova.policies import base
|
|||||||
RULE_AOO = base.RULE_ADMIN_OR_OWNER
|
RULE_AOO = base.RULE_ADMIN_OR_OWNER
|
||||||
SERVERS = 'os_compute_api:servers:%s'
|
SERVERS = 'os_compute_api:servers:%s'
|
||||||
NETWORK_ATTACH_EXTERNAL = 'network:attach_external_network'
|
NETWORK_ATTACH_EXTERNAL = 'network:attach_external_network'
|
||||||
|
ZERO_DISK_FLAVOR = SERVERS % 'create:zero_disk_flavor'
|
||||||
|
|
||||||
rules = [
|
rules = [
|
||||||
policy.DocumentedRuleDefault(
|
policy.DocumentedRuleDefault(
|
||||||
@ -137,6 +138,34 @@ rules = [
|
|||||||
'path': '/servers'
|
'path': '/servers'
|
||||||
}
|
}
|
||||||
]),
|
]),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
ZERO_DISK_FLAVOR,
|
||||||
|
# TODO(mriedem): Default to RULE_ADMIN_API in Stein.
|
||||||
|
RULE_AOO,
|
||||||
|
"""
|
||||||
|
This rule controls the compute API validation behavior of creating a server
|
||||||
|
with a flavor that has 0 disk, indicating the server should be volume-backed.
|
||||||
|
|
||||||
|
For a flavor with disk=0, the root disk will be set to exactly the size of the
|
||||||
|
image used to deploy the instance. However, in this case the filter_scheduler
|
||||||
|
cannot select the compute host based on the virtual image size. Therefore, 0
|
||||||
|
should only be used for volume booted instances or for testing purposes.
|
||||||
|
|
||||||
|
WARNING: It is a potential security exposure to enable this policy rule
|
||||||
|
if users can upload their own images since repeated attempts to
|
||||||
|
create a disk=0 flavor instance with a large image can exhaust
|
||||||
|
the local disk of the compute (or shared storage cluster). See bug
|
||||||
|
https://bugs.launchpad.net/nova/+bug/1739646 for details.
|
||||||
|
|
||||||
|
This rule defaults to ``rule:admin_or_owner`` for backward compatibility but
|
||||||
|
will be changed to default to ``rule:admin_api`` in a subsequent release.
|
||||||
|
""",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'method': 'POST',
|
||||||
|
'path': '/servers'
|
||||||
|
}
|
||||||
|
]),
|
||||||
policy.DocumentedRuleDefault(
|
policy.DocumentedRuleDefault(
|
||||||
NETWORK_ATTACH_EXTERNAL,
|
NETWORK_ATTACH_EXTERNAL,
|
||||||
'is_admin:True',
|
'is_admin:True',
|
||||||
|
@ -11,10 +11,14 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
|
import six
|
||||||
|
|
||||||
from nova.compute import api as compute_api
|
from nova.compute import api as compute_api
|
||||||
|
from nova.policies import base as base_policies
|
||||||
|
from nova.policies import servers as servers_policies
|
||||||
from nova import test
|
from nova import test
|
||||||
from nova.tests import fixtures as nova_fixtures
|
from nova.tests import fixtures as nova_fixtures
|
||||||
|
from nova.tests.functional.api import client as api_client
|
||||||
from nova.tests.functional import integrated_helpers
|
from nova.tests.functional import integrated_helpers
|
||||||
from nova.tests.unit.image import fake as fake_image
|
from nova.tests.unit.image import fake as fake_image
|
||||||
from nova.tests.unit import policy_fixture
|
from nova.tests.unit import policy_fixture
|
||||||
@ -314,3 +318,79 @@ class ServersPreSchedulingTestCase(test.TestCase,
|
|||||||
# The volume should no longer have any attachments as instance delete
|
# The volume should no longer have any attachments as instance delete
|
||||||
# should have removed them.
|
# should have removed them.
|
||||||
self.assertNotIn(volume_id, cinder.attachments[server['id']])
|
self.assertNotIn(volume_id, cinder.attachments[server['id']])
|
||||||
|
|
||||||
|
|
||||||
|
class EnforceVolumeBackedForZeroDiskFlavorTestCase(
|
||||||
|
test.TestCase, integrated_helpers.InstanceHelperMixin):
|
||||||
|
"""Tests for the os_compute_api:servers:create:zero_disk_flavor policy rule
|
||||||
|
|
||||||
|
These tests explicitly rely on microversion 2.1.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(EnforceVolumeBackedForZeroDiskFlavorTestCase, self).setUp()
|
||||||
|
fake_image.stub_out_image_service(self)
|
||||||
|
self.addCleanup(fake_image.FakeImageService_reset)
|
||||||
|
self.useFixture(nova_fixtures.NeutronFixture(self))
|
||||||
|
self.policy_fixture = (
|
||||||
|
self.useFixture(policy_fixture.RealPolicyFixture()))
|
||||||
|
api_fixture = self.useFixture(nova_fixtures.OSAPIFixture(
|
||||||
|
api_version='v2.1'))
|
||||||
|
|
||||||
|
self.api = api_fixture.api
|
||||||
|
self.admin_api = api_fixture.admin_api
|
||||||
|
# We need a zero disk flavor for the tests in this class.
|
||||||
|
flavor_req = {
|
||||||
|
"flavor": {
|
||||||
|
"name": "zero-disk-flavor",
|
||||||
|
"ram": 1024,
|
||||||
|
"vcpus": 2,
|
||||||
|
"disk": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.zero_disk_flavor = self.admin_api.post_flavor(flavor_req)
|
||||||
|
|
||||||
|
def test_create_image_backed_server_with_zero_disk_fails(self):
|
||||||
|
"""Tests that a non-admin trying to create an image-backed server
|
||||||
|
using a flavor with 0 disk will result in a 403 error when rule
|
||||||
|
os_compute_api:servers:create:zero_disk_flavor is set to admin-only.
|
||||||
|
"""
|
||||||
|
self.policy_fixture.set_rules({
|
||||||
|
servers_policies.ZERO_DISK_FLAVOR: base_policies.RULE_ADMIN_API},
|
||||||
|
overwrite=False)
|
||||||
|
server_req = self._build_minimal_create_server_request(
|
||||||
|
self.api,
|
||||||
|
'test_create_image_backed_server_with_zero_disk_fails',
|
||||||
|
fake_image.AUTO_DISK_CONFIG_ENABLED_IMAGE_UUID,
|
||||||
|
self.zero_disk_flavor['id'])
|
||||||
|
ex = self.assertRaises(api_client.OpenStackApiException,
|
||||||
|
self.api.post_server, {'server': server_req})
|
||||||
|
self.assertIn('Only volume-backed servers are allowed for flavors '
|
||||||
|
'with zero disk.', six.text_type(ex))
|
||||||
|
self.assertEqual(403, ex.response.status_code)
|
||||||
|
|
||||||
|
def test_create_volume_backed_server_with_zero_disk_allowed(self):
|
||||||
|
"""Tests that creating a volume-backed server with a zero-root
|
||||||
|
disk flavor will be allowed for admins.
|
||||||
|
"""
|
||||||
|
# For this test, we want to start conductor and the scheduler but
|
||||||
|
# we don't start compute so that scheduling fails; we don't really
|
||||||
|
# care about successfully building an active server here.
|
||||||
|
self.useFixture(nova_fixtures.PlacementFixture())
|
||||||
|
self.useFixture(nova_fixtures.CinderFixture(self))
|
||||||
|
self.start_service('conductor')
|
||||||
|
self.start_service('scheduler')
|
||||||
|
server_req = self._build_minimal_create_server_request(
|
||||||
|
self.api,
|
||||||
|
'test_create_volume_backed_server_with_zero_disk_allowed',
|
||||||
|
flavor_id=self.zero_disk_flavor['id'])
|
||||||
|
server_req.pop('imageRef', None)
|
||||||
|
server_req['block_device_mapping_v2'] = [{
|
||||||
|
'uuid': nova_fixtures.CinderFixture.IMAGE_BACKED_VOL,
|
||||||
|
'source_type': 'volume',
|
||||||
|
'destination_type': 'volume',
|
||||||
|
'boot_index': 0
|
||||||
|
}]
|
||||||
|
server = self.admin_api.post_server({'server': server_req})
|
||||||
|
server = self._wait_for_state_change(self.api, server, 'ERROR')
|
||||||
|
self.assertIn('No valid host', server['fault']['message'])
|
||||||
|
@ -378,6 +378,7 @@ class RealRolePolicyTestCase(test.NoDBTestCase):
|
|||||||
"os_compute_api:servers:create:attach_network",
|
"os_compute_api:servers:create:attach_network",
|
||||||
"os_compute_api:servers:create:attach_volume",
|
"os_compute_api:servers:create:attach_volume",
|
||||||
"os_compute_api:servers:create:trusted_certs",
|
"os_compute_api:servers:create:trusted_certs",
|
||||||
|
"os_compute_api:servers:create:zero_disk_flavor",
|
||||||
"os_compute_api:servers:create_image",
|
"os_compute_api:servers:create_image",
|
||||||
"os_compute_api:servers:delete",
|
"os_compute_api:servers:delete",
|
||||||
"os_compute_api:servers:detail",
|
"os_compute_api:servers:detail",
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
security:
|
||||||
|
- |
|
||||||
|
A new policy rule, ``os_compute_api:servers:create:zero_disk_flavor``, has
|
||||||
|
been introduced which defaults to ``rule:admin_or_owner`` for backward
|
||||||
|
compatibility, but can be configured to make the compute
|
||||||
|
API enforce that server create requests using a flavor with zero root disk
|
||||||
|
must be volume-backed or fail with a ``403 HTTPForbidden`` error.
|
||||||
|
|
||||||
|
Allowing image-backed servers with a zero root disk flavor can be
|
||||||
|
potentially hazardous if users are allowed to upload their own images,
|
||||||
|
since an instance created with a zero root disk flavor gets its size
|
||||||
|
from the image, which can be unexpectedly large and exhaust local disk
|
||||||
|
on the compute host. See https://bugs.launchpad.net/nova/+bug/1739646 for
|
||||||
|
more details.
|
||||||
|
|
||||||
|
While this is introduced in a backward-compatible way, the default will
|
||||||
|
be changed to ``rule:admin_api`` in a subsequent release. It is advised
|
||||||
|
that you communicate this change to your users before turning on
|
||||||
|
enforcement since it will result in a compute API behavior change.
|
Loading…
Reference in New Issue
Block a user