Skip to content

Commit 32ad7a0

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "TPM: support instances with user secret security"
2 parents d6b0961 + ad1dd5e commit 32ad7a0

9 files changed

Lines changed: 246 additions & 4 deletions

File tree

nova/conf/libvirt.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1634,6 +1634,20 @@
16341634
Related options:
16351635
16361636
* ``swtpm_user`` must also be set.
1637+
"""),
1638+
cfg.ListOpt('supported_tpm_secret_security',
1639+
default=['user'],
1640+
help="""
1641+
The list of TPM security policies supported by this compute host. If a value is
1642+
absent, it is not supported by this host, and any instance that requests it
1643+
will not be scheduled on this host.
1644+
1645+
Possible values are:
1646+
1647+
* ``user``: The Barbican secret is owned by the instance owner and cannot be
1648+
accessed by anyone else. The Libvirt secret is private and non-persistent.
1649+
The instance cannot be live-migrated or automatically resumed after host
1650+
reboot.
16371651
"""),
16381652
]
16391653

nova/scheduler/request_filter.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,34 @@ def virtio_sound_filter(
465465
return True
466466

467467

468+
@trace_request_filter
469+
def tpm_secret_security_filter(
470+
ctxt: nova_context.RequestContext,
471+
request_spec: 'objects.RequestSpec'
472+
) -> bool:
473+
has_vtpm = hardware.get_vtpm_constraint(
474+
request_spec.flavor, request_spec.image) is not None
475+
476+
if not has_vtpm:
477+
LOG.debug("tpm_secret_security_filter skipped")
478+
return False
479+
480+
security = hardware.get_tpm_secret_security_constraint(
481+
request_spec.flavor) or 'user'
482+
483+
if security == 'user':
484+
request_spec.root_required.add(
485+
os_traits.COMPUTE_SECURITY_TPM_SECRET_SECURITY_USER)
486+
else:
487+
# We can get here if the requested TPM secret security passed extra
488+
# spec validation but is not otherwise supported in the code at this
489+
# time.
490+
msg = f"TPM secret security '{security}' is not supported."
491+
LOG.warning(msg)
492+
raise exception.RequestFilterFailed(reason=_(msg))
493+
return True
494+
495+
468496
ALL_REQUEST_FILTERS = [
469497
require_tenant_aggregate,
470498
map_az_to_placement_aggregate,
@@ -477,7 +505,8 @@ def virtio_sound_filter(
477505
routed_networks_filter,
478506
remote_managed_ports_filter,
479507
ephemeral_encryption_filter,
480-
virtio_sound_filter
508+
virtio_sound_filter,
509+
tpm_secret_security_filter,
481510
]
482511

483512

nova/tests/fixtures/libvirt.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1910,6 +1910,12 @@ def __init__(
19101910
self._id_counter = 1 # libvirt reserves 0 for the hypervisor.
19111911
self._nodedevs = {}
19121912
self._secrets = {}
1913+
# NOTE(artom) The secret is undefined as soon as the guest has
1914+
# successfully started, but we still want to assert that is had been
1915+
# defined in this libvirt connection. We therefore keep a history of
1916+
# self._removed_secrets to allow functional tests to make those
1917+
# assertions.
1918+
self._removed_secrets = {}
19131919
self._event_callbacks = {}
19141920
self.fakeLibVersion = version
19151921
self.fakeVersion = hv_version
@@ -1931,7 +1937,7 @@ def _add_secret(self, secret):
19311937
self._secrets[secret._uuid] = secret
19321938

19331939
def _remove_secret(self, secret):
1934-
del self._secrets[secret._uuid]
1940+
self._removed_secrets[secret._uuid] = self._secrets.pop(secret._uuid)
19351941

19361942
def _mark_running(self, dom):
19371943
self._running_vms[self._id_counter] = dom

nova/tests/functional/libvirt/test_vtpm.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,14 @@ def setUp(self):
166166

167167
self.key_mgr = crypto._get_key_manager()
168168

169-
def _create_server_with_vtpm(self):
169+
def _create_server_with_vtpm(self, secret_security=None,
170+
expected_state='ACTIVE'):
170171
extra_specs = {'hw:tpm_model': 'tpm-tis', 'hw:tpm_version': '1.2'}
172+
if secret_security:
173+
extra_specs.update({'hw:tpm_secret_security': secret_security})
171174
flavor_id = self._create_flavor(extra_spec=extra_specs)
172-
server = self._create_server(flavor_id=flavor_id)
175+
server = self._create_server(flavor_id=flavor_id,
176+
expected_state=expected_state)
173177

174178
return server
175179

@@ -185,13 +189,47 @@ def assertInstanceHasSecret(self, server):
185189
self.assertIn(
186190
instance.system_metadata['vtpm_secret_uuid'],
187191
self.key_mgr._passphrases)
192+
return instance.system_metadata['vtpm_secret_uuid']
188193

189194
def assertInstanceHasNoSecret(self, server):
190195
ctx = nova_context.get_admin_context()
191196
instance = objects.Instance.get_by_uuid(ctx, server['id'])
192197
self.assertNotIn('vtpm_secret_uuid', instance.system_metadata)
193198
self.assertEqual(0, len(self.key_mgr._passphrases))
194199

200+
def _assert_libvirt_had_secret(self, compute, secret_uuid):
201+
# This assert is for ephemeral private libvirt secrets that we
202+
# undefine immediately after guest creation. Examples include 'user'
203+
# and 'deployment' TPM secret security modes and legacy servers.
204+
# The LibvirtFixture tracks secrets that existed before they were
205+
# removed, so we can assert this.
206+
conn = compute.driver._host.get_connection()
207+
self.assertIn(secret_uuid, conn._removed_secrets)
208+
209+
def test_tpm_secret_security_user(self):
210+
self.flags(supported_tpm_secret_security=['user'], group='libvirt')
211+
host = self.start_compute(hostname='tpm-host')
212+
compute = self.computes['tpm-host']
213+
214+
# ensure we are reporting the correct traits
215+
traits = self._get_provider_traits(self.compute_rp_uuids[host])
216+
self.assertIn('COMPUTE_SECURITY_TPM_SECRET_SECURITY_USER', traits)
217+
218+
server = self._create_server_with_vtpm(secret_security='user')
219+
220+
# The server should have a secret in the key manager service.
221+
secret_uuid = self.assertInstanceHasSecret(server)
222+
223+
# And it should have had a libvirt secret created and undefined.
224+
self._assert_libvirt_had_secret(compute, secret_uuid)
225+
226+
def test_tpm_secret_security_user_negative(self):
227+
self.flags(supported_tpm_secret_security=['deployment'],
228+
group='libvirt')
229+
self.start_compute(hostname='tpm-host')
230+
self._create_server_with_vtpm(secret_security='user',
231+
expected_state='ERROR')
232+
195233
def test_create_server(self):
196234
compute = self.start_compute()
197235

@@ -393,6 +431,24 @@ def test_resize_server__vtpm_to_no_vtpm(self):
393431
# there is no going back now
394432
self.assertInstanceHasNoSecret(server)
395433

434+
def test_create_server_secret_security_unsupported(self):
435+
"""Test when a not supported TPM secret security mode is requested
436+
437+
We expect the create to fail for NoValidHost.
438+
"""
439+
# Start a compute host which supports no modes.
440+
self.flags(supported_tpm_secret_security=[], group='libvirt')
441+
self.start_compute('test_compute0')
442+
443+
# Try to create an instance on that host defaulting to 'user'.
444+
server = self._create_server_with_vtpm(expected_state='ERROR')
445+
446+
# The create should have failed for NoValidHost.
447+
event = self._wait_for_instance_action_event(
448+
server, 'create', 'conductor_schedule_and_build_instances',
449+
'Error')
450+
self.assertIn('NoValidHost', event['traceback'])
451+
396452
def test_migrate_server(self):
397453
"""Test cold migrate as a non-admin user.
398454

nova/tests/unit/scheduler/test_request_filter.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import os_traits as ot
1414
from unittest import mock
1515

16+
import ddt
1617
from oslo_utils.fixture import uuidsentinel as uuids
1718
from oslo_utils import timeutils
1819

@@ -25,6 +26,7 @@
2526
from nova.tests.unit import utils
2627

2728

29+
@ddt.ddt
2830
class TestRequestFilter(test.NoDBTestCase):
2931
def setUp(self):
3032
super(TestRequestFilter, self).setUp()
@@ -746,3 +748,68 @@ def test_virtio_sound_filter(self):
746748
{ot.COMPUTE_SOUND_MODEL_VIRTIO},
747749
reqspec.root_required)
748750
self.assertEqual(set(), reqspec.root_forbidden)
751+
752+
@ddt.data('flavor', 'image')
753+
def test_tpm_secret_security_filter(self, source):
754+
# First ensure that tpm_secret_security_filter is included
755+
self.assertIn(request_filter.tpm_secret_security_filter,
756+
request_filter.ALL_REQUEST_FILTERS)
757+
758+
if source == 'flavor':
759+
reqspec = objects.RequestSpec(
760+
flavor=objects.Flavor(
761+
extra_specs={
762+
'hw:tpm_model': 'tpm-tis',
763+
'hw:tpm_version': '1.2',
764+
'hw:tpm_secret_security': 'user',
765+
}),
766+
image=objects.ImageMeta(properties=objects.ImageMetaProps()))
767+
elif source == 'image':
768+
reqspec = objects.RequestSpec(
769+
flavor=objects.Flavor(
770+
extra_specs={
771+
'hw:tpm_secret_security': 'user',
772+
}),
773+
image=objects.ImageMeta(
774+
properties=objects.ImageMetaProps(hw_tpm_model='tpm-tis',
775+
hw_tpm_version='1.2')))
776+
777+
self.assertEqual(set(), reqspec.root_required)
778+
self.assertEqual(set(), reqspec.root_forbidden)
779+
self.assertTrue(
780+
request_filter.tpm_secret_security_filter(self.context, reqspec))
781+
self.assertEqual(
782+
{ot.COMPUTE_SECURITY_TPM_SECRET_SECURITY_USER},
783+
reqspec.root_required)
784+
self.assertEqual(set(), reqspec.root_forbidden)
785+
786+
def test_tpm_secret_security_filter_skip(self):
787+
reqspec = objects.RequestSpec(
788+
flavor=objects.Flavor(extra_specs={}),
789+
image=objects.ImageMeta(
790+
properties=objects.ImageMetaProps()))
791+
self.assertEqual(set(), reqspec.root_required)
792+
self.assertEqual(set(), reqspec.root_forbidden)
793+
self.assertFalse(
794+
request_filter.tpm_secret_security_filter(self.context, reqspec))
795+
796+
def test_tpm_secret_security_filter_fail(self):
797+
reqspec = objects.RequestSpec(
798+
flavor=objects.Flavor(
799+
extra_specs={
800+
'hw:tpm_model': 'tpm-tis',
801+
'hw:tpm_version': '1.2',
802+
'hw:tpm_secret_security': 'bogus',
803+
}),
804+
image=objects.ImageMeta(
805+
properties=objects.ImageMetaProps()))
806+
self.assertEqual(set(), reqspec.root_required)
807+
self.assertEqual(set(), reqspec.root_forbidden)
808+
# Mock out get_tpm_secret_security_constraint() so we don't get caught
809+
# by the Invalid check before we can test the filter fail.
810+
with mock.patch(
811+
'nova.virt.hardware.get_tpm_secret_security_constraint'):
812+
self.assertRaises(
813+
exception.RequestFilterFailed,
814+
request_filter.tpm_secret_security_filter,
815+
self.context, reqspec)

nova/tests/unit/virt/libvirt/test_driver.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23503,6 +23503,30 @@ def test_update_provider_tree_with_tpm_traits(self):
2350323503
'COMPUTE_SECURITY_TPM_2_0', 'COMPUTE_SECURITY_TPM_1_2'):
2350423504
self.assertIn(trait, self.pt.data(self.cn_rp['uuid']).traits)
2350523505

23506+
def test_update_provider_tree_with_tpm_secret_security_traits(self):
23507+
self.flags(swtpm_enabled=True, group='libvirt')
23508+
self.flags(
23509+
supported_tpm_secret_security=['user', 'host', 'deployment'],
23510+
group='libvirt')
23511+
self._test_update_provider_tree()
23512+
for trait in (
23513+
'COMPUTE_SECURITY_TPM_SECRET_SECURITY_USER',
23514+
'COMPUTE_SECURITY_TPM_SECRET_SECURITY_HOST',
23515+
'COMPUTE_SECURITY_TPM_SECRET_SECURITY_DEPLOYMENT'
23516+
):
23517+
self.assertIn(trait, self.pt.data(self.cn_rp['uuid']).traits)
23518+
23519+
def test_update_provider_tree_with_tpm_secret_security_traits_none(self):
23520+
self.flags(swtpm_enabled=True, group='libvirt')
23521+
self.flags(supported_tpm_secret_security=[], group='libvirt')
23522+
self._test_update_provider_tree()
23523+
for trait in (
23524+
'COMPUTE_SECURITY_TPM_SECRET_SECURITY_USER',
23525+
'COMPUTE_SECURITY_TPM_SECRET_SECURITY_HOST',
23526+
'COMPUTE_SECURITY_TPM_SECRET_SECURITY_DEPLOYMENT'
23527+
):
23528+
self.assertNotIn(trait, self.pt.data(self.cn_rp['uuid']).traits)
23529+
2350623530
@mock.patch.object(
2350723531
fakelibvirt.virConnect, '_domain_capability_devices', new=
2350823532
fakelibvirt.virConnect._domain_capability_devices_with_tpm_supported

nova/tests/unit/virt/test_hardware.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5931,6 +5931,28 @@ def test_get_vtpm_constraint(
59315931
expected, hw.get_vtpm_constraint(flavor, image_meta),
59325932
)
59335933

5934+
@ddt.unpack
5935+
@ddt.data(
5936+
# pass: no configuration
5937+
(None, None),
5938+
# pass: flavor-only
5939+
('user', 'user'),
5940+
('host', 'host'),
5941+
('deployment', 'deployment'),
5942+
)
5943+
def test_get_tpm_secret_security_constraint(self, flavor_security,
5944+
expected):
5945+
extra_specs = {}
5946+
5947+
if flavor_security:
5948+
extra_specs['hw:tpm_secret_security'] = flavor_security
5949+
5950+
flavor = objects.Flavor(
5951+
name='foo', vcpus=1, memory_mb=1024, extra_specs=extra_specs)
5952+
5953+
self.assertEqual(
5954+
expected, hw.get_tpm_secret_security_constraint(flavor))
5955+
59345956

59355957
@ddt.ddt
59365958
class SecureBootPolicyTest(test.NoDBTestCase):

nova/virt/hardware.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2123,6 +2123,20 @@ def get_vtpm_constraint(
21232123
return VTPMConfig(version, model)
21242124

21252125

2126+
def get_tpm_secret_security_constraint(
2127+
flavor: 'objects.Flavor',
2128+
) -> ty.Optional[str]:
2129+
# NOTE(melwitt): An image property for TPM secret security is intentionally
2130+
# not provided because server rebuild is blocked in the API. If a user were
2131+
# to create a server with a given TPM secret security policy via an image
2132+
# property, that policy would become locked-in and unable to be changed.
2133+
# The user would not be able to change the image property because they
2134+
# would not be able to rebuild, and they would not be able to resize to a
2135+
# different TPM secret security policy because the image property and
2136+
# flavor extra spec would conflict.
2137+
return flavor.get('extra_specs', {}).get('hw:tpm_secret_security')
2138+
2139+
21262140
def get_secure_boot_constraint(
21272141
flavor: 'objects.Flavor',
21282142
image_meta: 'objects.ImageMeta',

nova/virt/libvirt/driver.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13338,6 +13338,16 @@ def _get_tpm_traits(self) -> ty.Dict[str, bool]:
1333813338
ot.COMPUTE_SECURITY_TPM_1_2: '1.2' in tpm_versions,
1333913339
})
1334013340

13341+
if 'user' in CONF.libvirt.supported_tpm_secret_security:
13342+
tr.update({
13343+
ot.COMPUTE_SECURITY_TPM_SECRET_SECURITY_USER: True})
13344+
if 'host' in CONF.libvirt.supported_tpm_secret_security:
13345+
tr.update({
13346+
ot.COMPUTE_SECURITY_TPM_SECRET_SECURITY_HOST: True})
13347+
if 'deployment' in CONF.libvirt.supported_tpm_secret_security:
13348+
tr.update({
13349+
ot.COMPUTE_SECURITY_TPM_SECRET_SECURITY_DEPLOYMENT: True})
13350+
1334113351
return tr
1334213352

1334313353
def _get_vif_model_traits(self) -> ty.Dict[str, bool]:

0 commit comments

Comments
 (0)