Skip to content

Commit 9478820

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "TPM: support instances with host secret security"
2 parents 32ad7a0 + 245a321 commit 9478820

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)