Skip to content

Commit 245a321

Browse files
notartommelwitt
authored andcommitted
TPM: support instances with host secret security
Start supporting booting instances with the `host` TPM secret security. This means setting the `ephemeral` and `private` attributes on the Libvirt secret correctly, and not undefining the secret once the instance has spawned. The Libvirt fixture's Secret support is extended to be able to test all that in a functional test. For functional testing, we need to: * Extend our libvirt fixture's Secret object to properly set the usage id (which is just the instance UUID) when parsing vTPM secret XML. Related to blueprint vtpm-live-migration Change-Id: I5a38a0de76a78b28a205a8d19f2374830054e1ab Signed-off-by: melanie witt <melwittt@gmail.com>
1 parent ad1dd5e commit 245a321

10 files changed

Lines changed: 215 additions & 21 deletions

File tree

nova/conf/libvirt.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1636,7 +1636,7 @@
16361636
* ``swtpm_user`` must also be set.
16371637
"""),
16381638
cfg.ListOpt('supported_tpm_secret_security',
1639-
default=['user'],
1639+
default=['user', 'host'],
16401640
help="""
16411641
The list of TPM security policies supported by this compute host. If a value is
16421642
absent, it is not supported by this host, and any instance that requests it
@@ -1648,6 +1648,10 @@
16481648
accessed by anyone else. The Libvirt secret is private and non-persistent.
16491649
The instance cannot be live-migrated or automatically resumed after host
16501650
reboot.
1651+
* ``host``: The Barbican secret is owned by the instance owner and cannot be
1652+
accessed by anyone else. The Libvirt secret is public and persistent. It
1653+
can be read by anyone with sufficient access on the host. The instance can
1654+
be live-migrated and automatically resumed after host reboot.
16511655
"""),
16521656
]
16531657

nova/scheduler/request_filter.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,9 @@ def tpm_secret_security_filter(
483483
if security == 'user':
484484
request_spec.root_required.add(
485485
os_traits.COMPUTE_SECURITY_TPM_SECRET_SECURITY_USER)
486+
elif security == 'host':
487+
request_spec.root_required.add(
488+
os_traits.COMPUTE_SECURITY_TPM_SECRET_SECURITY_HOST)
486489
else:
487490
# We can get here if the requested TPM secret security passed extra
488491
# spec validation but is not otherwise supported in the code at this

nova/tests/fixtures/libvirt.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ def _reset():
198198
VIR_SECRET_USAGE_TYPE_VOLUME = 1
199199
VIR_SECRET_USAGE_TYPE_CEPH = 2
200200
VIR_SECRET_USAGE_TYPE_ISCSI = 3
201+
VIR_SECRET_USAGE_TYPE_VTPM = 5
201202

202203
# metadata types
203204
VIR_DOMAIN_METADATA_DESCRIPTION = 0
@@ -1834,12 +1835,15 @@ def __init__(self, connection, xml):
18341835
def _parse_xml(self, xml):
18351836
tree = etree.fromstring(xml)
18361837
self._uuid = tree.find('./uuid').text
1838+
self._ephemeral = tree.get('ephemeral') == 'yes'
18371839
self._private = tree.get('private') == 'yes'
18381840
self._usage_id = None
18391841
usage = tree.find('./usage')
18401842
if usage is not None:
18411843
if usage.get('type') == 'volume':
18421844
self._usage_id = usage.find('volume').text
1845+
if usage.get('type') == 'vtpm':
1846+
self._usage_id = usage.find('name').text
18431847

18441848
def setValue(self, value, flags=0):
18451849
self._value = value

nova/tests/functional/integrated_helpers.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -516,12 +516,16 @@ def _delete_server(self, server):
516516
self.api.delete_server(server['id'])
517517
self._wait_until_deleted(server)
518518

519-
def _reboot_server(self, server, hard=False, expected_state='ACTIVE'):
519+
def _reboot_server(self, server, hard=False, expected_state='ACTIVE',
520+
api=None):
520521
"""Reboot a server."""
521-
self.api.post_server_action(
522+
api = api or self.api
523+
api.post_server_action(
522524
server['id'], {'reboot': {'type': 'HARD' if hard else 'SOFT'}},
523525
)
524-
self.notifier.wait_for_versioned_notifications('instance.reboot.end')
526+
if expected_state != 'ERROR':
527+
self.notifier.wait_for_versioned_notifications(
528+
'instance.reboot.end')
525529
return self._wait_for_state_change(server, expected_state)
526530

527531
def _show_server(self, server):

nova/tests/functional/libvirt/test_vtpm.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from castellan.common import exception as castellan_exc
1919
from castellan.common.objects import passphrase
2020
from castellan.key_manager import key_manager
21+
import ddt
2122
import fixtures
2223
from oslo_log import log as logging
2324
from oslo_utils import uuidutils
@@ -140,6 +141,7 @@ def remove_consumer(self, context, managed_object_id, consumer_data):
140141
)
141142

142143

144+
@ddt.ddt
143145
class VTPMServersTest(base.ServersTestBase):
144146

145147
# NOTE: ADMIN_API is intentionally not set to True in order to catch key
@@ -197,6 +199,14 @@ def assertInstanceHasNoSecret(self, server):
197199
self.assertNotIn('vtpm_secret_uuid', instance.system_metadata)
198200
self.assertEqual(0, len(self.key_mgr._passphrases))
199201

202+
def _assert_libvirt_has_secret(self, host, instance_uuid):
203+
s = host.driver._host.find_secret('vtpm', instance_uuid)
204+
self.assertIsNotNone(s)
205+
ctx = nova_context.get_admin_context()
206+
instance = objects.Instance.get_by_uuid(ctx, instance_uuid)
207+
secret_uuid = instance.system_metadata['vtpm_secret_uuid']
208+
self.assertEqual(secret_uuid, s.UUIDString())
209+
200210
def _assert_libvirt_had_secret(self, compute, secret_uuid):
201211
# This assert is for ephemeral private libvirt secrets that we
202212
# undefine immediately after guest creation. Examples include 'user'
@@ -206,6 +216,10 @@ def _assert_libvirt_had_secret(self, compute, secret_uuid):
206216
conn = compute.driver._host.get_connection()
207217
self.assertIn(secret_uuid, conn._removed_secrets)
208218

219+
def _assert_libvirt_secret_missing(self, host, instance_uuid):
220+
s = host.driver._host.find_secret('vtpm', instance_uuid)
221+
self.assertIsNone(s)
222+
209223
def test_tpm_secret_security_user(self):
210224
self.flags(supported_tpm_secret_security=['user'], group='libvirt')
211225
host = self.start_compute(hostname='tpm-host')
@@ -251,6 +265,38 @@ def test_create_server(self):
251265
# ensure we deleted the key now that we no longer need it
252266
self.assertEqual(0, len(self.key_mgr._passphrases))
253267

268+
def test_create_server_secret_security_host(self):
269+
self.flags(supported_tpm_secret_security=['host'], group='libvirt')
270+
compute = self.start_compute()
271+
272+
# ensure we are reporting the correct traits
273+
traits = self._get_provider_traits(self.compute_rp_uuids[compute])
274+
self.assertIn('COMPUTE_SECURITY_TPM_SECRET_SECURITY_HOST', traits)
275+
276+
# create a server with vTPM
277+
server = self._create_server_with_vtpm(secret_security='host')
278+
279+
# ensure our instance's system_metadata field and key manager inventory
280+
# is correct
281+
self.assertInstanceHasSecret(server)
282+
283+
# ensure the libvirt secret is defined correctly
284+
ctx = nova_context.get_admin_context()
285+
instance = objects.Instance.get_by_uuid(ctx, server['id'])
286+
conn = self.computes[compute].driver._host.get_connection()
287+
secret = conn._secrets[instance.system_metadata['vtpm_secret_uuid']]
288+
self.assertFalse(secret._ephemeral)
289+
self.assertFalse(secret._private)
290+
291+
# now delete the server
292+
self._delete_server(server)
293+
294+
# ensure we deleted the key and undefined the secret now that we no
295+
# longer need it
296+
self.assertEqual(0, len(self.key_mgr._passphrases))
297+
self.assertNotIn(instance.system_metadata['vtpm_secret_uuid'],
298+
conn._secrets)
299+
254300
def test_suspend_resume_server(self):
255301
self.start_compute()
256302

@@ -300,6 +346,24 @@ def test_hard_reboot_server(self):
300346
# is still correct
301347
self.assertInstanceHasSecret(server)
302348

349+
@ddt.data(None, 'user', 'host')
350+
def test_hard_reboot_server_as_admin(self, secret_security):
351+
"""Test hard rebooting a non-admin user's instance as admin.
352+
353+
This should only work for the 'host' TPM secret security policy.
354+
"""
355+
self.start_compute()
356+
357+
# create a server with vTPM
358+
server = self._create_server_with_vtpm(secret_security=secret_security)
359+
360+
# Attempt to reboot the server as admin, should only work for 'host'.
361+
if secret_security == 'host':
362+
self._reboot_server(server, hard=True, api=self.admin_api)
363+
else:
364+
self._reboot_server(server, hard=True, expected_state='ERROR',
365+
api=self.admin_api)
366+
303367
def _test_resize_revert_server__vtpm_to_vtpm(self, extra_specs=None):
304368
"""Test behavior of revert when a vTPM is retained across a resize.
305369

nova/tests/unit/scheduler/test_request_filter.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -749,8 +749,13 @@ def test_virtio_sound_filter(self):
749749
reqspec.root_required)
750750
self.assertEqual(set(), reqspec.root_forbidden)
751751

752-
@ddt.data('flavor', 'image')
753-
def test_tpm_secret_security_filter(self, source):
752+
@ddt.data(
753+
('flavor', 'user', ot.COMPUTE_SECURITY_TPM_SECRET_SECURITY_USER),
754+
('flavor', 'host', ot.COMPUTE_SECURITY_TPM_SECRET_SECURITY_HOST),
755+
('image', 'user', ot.COMPUTE_SECURITY_TPM_SECRET_SECURITY_USER),
756+
('image', 'host', ot.COMPUTE_SECURITY_TPM_SECRET_SECURITY_HOST))
757+
@ddt.unpack
758+
def test_tpm_secret_security_filter(self, source, secret_security, trait):
754759
# First ensure that tpm_secret_security_filter is included
755760
self.assertIn(request_filter.tpm_secret_security_filter,
756761
request_filter.ALL_REQUEST_FILTERS)
@@ -761,14 +766,14 @@ def test_tpm_secret_security_filter(self, source):
761766
extra_specs={
762767
'hw:tpm_model': 'tpm-tis',
763768
'hw:tpm_version': '1.2',
764-
'hw:tpm_secret_security': 'user',
769+
'hw:tpm_secret_security': secret_security,
765770
}),
766771
image=objects.ImageMeta(properties=objects.ImageMetaProps()))
767772
elif source == 'image':
768773
reqspec = objects.RequestSpec(
769774
flavor=objects.Flavor(
770775
extra_specs={
771-
'hw:tpm_secret_security': 'user',
776+
'hw:tpm_secret_security': secret_security,
772777
}),
773778
image=objects.ImageMeta(
774779
properties=objects.ImageMetaProps(hw_tpm_model='tpm-tis',
@@ -778,9 +783,7 @@ def test_tpm_secret_security_filter(self, source):
778783
self.assertEqual(set(), reqspec.root_forbidden)
779784
self.assertTrue(
780785
request_filter.tpm_secret_security_filter(self.context, reqspec))
781-
self.assertEqual(
782-
{ot.COMPUTE_SECURITY_TPM_SECRET_SECURITY_USER},
783-
reqspec.root_required)
786+
self.assertEqual({trait}, reqspec.root_required)
784787
self.assertEqual(set(), reqspec.root_forbidden)
785788

786789
def test_tpm_secret_security_filter_skip(self):

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

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20978,6 +20978,55 @@ def test_create_guest__with_vtpm_error(
2097820978
# ...and undefined it after, despite the error
2097920979
drvr._host.create_secret.return_value.undefine.assert_called_once()
2098020980

20981+
@mock.patch('nova.virt.libvirt.host.Host')
20982+
@mock.patch('nova.crypto.ensure_vtpm_secret')
20983+
def test_get_or_create_secret_for_vtpm_host_security_found(
20984+
self, mock_secret, mock_host):
20985+
"""Test that the key manager service API is not called.
20986+
20987+
If the secret can be found locally from libvirt with 'host' TPM secret
20988+
security, there should be no call to the key manager API.
20989+
"""
20990+
instance = objects.Instance(**self.test_instance)
20991+
instance.flavor.extra_specs = {'hw:tpm_secret_security': 'host'}
20992+
mock_host.return_value.find_secret.return_value = mock.sentinel.secret
20993+
20994+
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True)
20995+
secret, security = drvr._get_or_create_secret_for_vtpm(self.context,
20996+
instance)
20997+
20998+
mock_secret.assert_not_called()
20999+
self.assertEqual(mock.sentinel.secret, secret)
21000+
self.assertEqual('host', security)
21001+
21002+
@mock.patch('nova.virt.libvirt.host.Host')
21003+
@mock.patch('nova.crypto.ensure_vtpm_secret')
21004+
def test_get_or_create_secret_for_vtpm_host_security_not_found(
21005+
self, mock_secret, mock_host):
21006+
"""Test that the key manager service API is called.
21007+
21008+
If the secret is not found locally from libvirt with 'host' TPM secret
21009+
security, there should be a call to the key manager API.
21010+
"""
21011+
instance = objects.Instance(**self.test_instance)
21012+
instance.flavor.extra_specs = {'hw:tpm_secret_security': 'host'}
21013+
mock_host.return_value.find_secret.return_value = None
21014+
mock_secret.return_value = (uuids.secret, mock.sentinel.passphrase)
21015+
21016+
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True)
21017+
secret, security = drvr._get_or_create_secret_for_vtpm(self.context,
21018+
instance)
21019+
21020+
mock_secret.assert_called_once_with(self.context, instance)
21021+
# ensure_vtpm_secret() returns (secret_uuid, passphrase)
21022+
mock_host.return_value.create_secret.assert_called_once_with(
21023+
'vtpm', uuids.instance, password=mock.sentinel.passphrase,
21024+
uuid=uuids.secret, ephemeral=False, private=False)
21025+
21026+
self.assertEqual(
21027+
mock_host.return_value.create_secret.return_value, secret)
21028+
self.assertEqual('host', security)
21029+
2098121030
@mock.patch('nova.virt.disk.api.clean_lxc_namespace')
2098221031
@mock.patch('nova.virt.libvirt.driver.LibvirtDriver.get_info')
2098321032
@mock.patch('nova.virt.disk.api.setup_container')
@@ -27807,7 +27856,7 @@ def test_cleanup_lvm_encrypted(self, mock_save, mock_undefine_domain,
2780727856
instance = objects.Instance(
2780827857
uuid=uuids.instance, id=1,
2780927858
ephemeral_key_uuid=uuids.ephemeral_key_uuid,
27810-
resources=None)
27859+
resources=None, flavor=objects.Flavor())
2781127860
instance.system_metadata = {}
2781227861
block_device_info = {'root_device_name': '/dev/vda',
2781327862
'ephemerals': [],

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import eventlet
2222
from eventlet import greenthread
2323
from eventlet import tpool
24+
from lxml import etree
2425
from oslo_serialization import jsonutils
2526
from oslo_utils.fixture import uuidsentinel as uuids
2627
from oslo_utils import uuidutils
@@ -1051,6 +1052,21 @@ def test_create_secret(self, mock_sec):
10511052
self.host.create_secret('iscsi', 'iscsivol', password="foo")
10521053
secret.setValue.assert_called_once_with("foo")
10531054

1055+
@mock.patch.object(fakelibvirt.virConnect, "secretDefineXML")
1056+
def test_create_secret_vtpm_ephemeral_private_default(self, mock_sec):
1057+
self.host.create_secret('vtpm', uuids.instance)
1058+
xml = etree.fromstring(mock_sec.call_args.args[0])
1059+
self.assertEqual('yes', xml.get('ephemeral'))
1060+
self.assertEqual('yes', xml.get('private'))
1061+
1062+
@mock.patch.object(fakelibvirt.virConnect, "secretDefineXML")
1063+
def test_create_secret_vtpm_not_ephemeral_private(self, mock_sec):
1064+
self.host.create_secret('vtpm', uuids.instance, ephemeral=False,
1065+
private=False)
1066+
xml = etree.fromstring(mock_sec.call_args.args[0])
1067+
self.assertEqual('no', xml.get('ephemeral'))
1068+
self.assertEqual('no', xml.get('private'))
1069+
10541070
@mock.patch('nova.virt.libvirt.host.Host.find_secret')
10551071
def test_delete_secret(self, mock_find_secret):
10561072
"""deleting secret."""

0 commit comments

Comments
 (0)