Skip to content

Commit ad1dd5e

Browse files
notartommelwitt
authored andcommitted
TPM: support instances with user secret security
The `user` secret security policy is just existing behavior. No changes are necessary in the mechanics, so this patch just adds a scheduler prefilter and tests. The functional tests add some groundwork to make future tests easier as well by making the helper methods more flexible. For functional testing, we need to: * Have our libvirt fixture keep track of undefined secrets. Secrets are undefined as soon as the VM that uses them successfully boots (as mentioned previously, VM creation follows this pattern), but our tests would still like to assert that the secret had been created on a host. Just add a _removed_secrets dict that _remove_secret() populates. Related to blueprint vtpm-live-migration Change-Id: Ib449dc2f1c4a9af9d423252594261947e811452e Signed-off-by: melanie witt <melwittt@gmail.com>
1 parent 0f82c29 commit ad1dd5e

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)