Skip to content

Commit

Permalink
Adds ephemeral storage encryption for LVM back-end images
Browse files Browse the repository at this point in the history
This patch adds ephemeral storage encryption for LVM back-end instances.
Encryption is implemented by passing all data written to and read from
the logical volumes through a dm-crypt layer. Most instance operations
such as pause/continue, suspend/resume, reboot, etc. are supported.
Snapshots are also supported but are not encrypted at present. VM rescue
and migration are not supported at present.

The proposed code provides data-at-rest security for all ephemeral
storage disks, preventing access to data while an instance is
shut down, or in case the compute host is shut down while an instance is
running.

Options controlling the encryption state, cipher and key size are
specified in the "ephemeral_storage_encryption" options group. The boolean
"enabled" option turns encryption on and off and the "cipher" and "key_size"
options specify the cipher and key size, respectively.

Note: depends on cryptsetup being installed.

Implements: blueprint lvm-ephemeral-storage-encryption
Change-Id: I871af4018f99ddfcc8408708bdaaf480088ac477
DocImpact
SecurityImpact
  • Loading branch information
Daniel Genin authored and Daniel Genin committed Sep 6, 2014
1 parent 844d0ca commit 5fa74bc
Show file tree
Hide file tree
Showing 9 changed files with 731 additions and 44 deletions.
36 changes: 34 additions & 2 deletions nova/compute/api.py
Expand Up @@ -46,6 +46,7 @@
from nova.i18n import _
from nova.i18n import _LE
from nova import image
from nova import keymgr
from nova import network
from nova.network import model as network_model
from nova.network.security_group import openstack_driver
Expand Down Expand Up @@ -111,9 +112,32 @@
'boot from volume. A negative number means unlimited.'),
]

ephemeral_storage_encryption_group = cfg.OptGroup(
name='ephemeral_storage_encryption',
title='Ephemeral storage encryption options')

ephemeral_storage_encryption_opts = [
cfg.BoolOpt('enabled',
default=False,
help='Whether to encrypt ephemeral storage'),
cfg.StrOpt('cipher',
default='aes-xts-plain64',
help='The cipher and mode to be used to encrypt ephemeral '
'storage. Which ciphers are available ciphers depends '
'on kernel support. See /proc/crypto for the list of '
'available options.'),
cfg.IntOpt('key_size',
default=512,
help='The bit length of the encryption key to be used to '
'encrypt ephemeral storage (in XTS mode only half of '
'the bits are used for encryption key)')
]

CONF = cfg.CONF
CONF.register_opts(compute_opts)
CONF.register_group(ephemeral_storage_encryption_group)
CONF.register_opts(ephemeral_storage_encryption_opts,
group='ephemeral_storage_encryption')
CONF.import_opt('compute_topic', 'nova.compute.rpcapi')
CONF.import_opt('enable', 'nova.cells.opts', group='cells')
CONF.import_opt('default_ephemeral_format', 'nova.virt.driver')
Expand Down Expand Up @@ -244,6 +268,8 @@ def __init__(self, image_api=None, network_api=None, volume_api=None,
self._compute_task_api = None
self.servicegroup_api = servicegroup.API()
self.notifier = rpc.get_notifier('compute', CONF.host)
if CONF.ephemeral_storage_encryption.enabled:
self.key_manager = keymgr.API()

super(API, self).__init__(**kwargs)

Expand Down Expand Up @@ -1185,7 +1211,7 @@ def _populate_instance_names(self, instance, num_instances):
def _default_display_name(self, instance_uuid):
return "Server %s" % instance_uuid

def _populate_instance_for_create(self, instance, image,
def _populate_instance_for_create(self, context, instance, image,
index, security_groups, instance_type):
"""Build the beginning of a new instance."""

Expand All @@ -1201,6 +1227,12 @@ def _populate_instance_for_create(self, instance, image,
info_cache.instance_uuid = instance.uuid
info_cache.network_info = network_model.NetworkInfo()
instance.info_cache = info_cache
if CONF.ephemeral_storage_encryption.enabled:
instance.ephemeral_key_uuid = self.key_manager.create_key(
context,
length=CONF.ephemeral_storage_encryption.key_size)
else:
instance.ephemeral_key_uuid = None

# Store image properties so we can use them later
# (for notifications, etc). Only store what we can.
Expand Down Expand Up @@ -1233,7 +1265,7 @@ def create_db_entry_for_new_instance(self, context, instance_type, image,
This is called by the scheduler after a location for the
instance has been determined.
"""
self._populate_instance_for_create(instance, image, index,
self._populate_instance_for_create(context, instance, image, index,
security_group, instance_type)

self._populate_instance_names(instance, num_instances)
Expand Down
5 changes: 3 additions & 2 deletions nova/tests/compute/test_compute.py
Expand Up @@ -7313,6 +7313,7 @@ def test_populate_instance_for_create(self):
instance.update(base_options)
inst_type = flavors.get_flavor_by_name("m1.tiny")
instance = self.compute_api._populate_instance_for_create(
self.context,
instance,
self.fake_image,
1,
Expand All @@ -7331,9 +7332,9 @@ def test_default_hostname_generator(self):

orig_populate = self.compute_api._populate_instance_for_create

def _fake_populate(base_options, *args, **kwargs):
def _fake_populate(context, base_options, *args, **kwargs):
base_options['uuid'] = fake_uuids.pop(0)
return orig_populate(base_options, *args, **kwargs)
return orig_populate(context, base_options, *args, **kwargs)

self.stubs.Set(self.compute_api,
'_populate_instance_for_create',
Expand Down
6 changes: 4 additions & 2 deletions nova/tests/virt/libvirt/fake_imagebackend.py
Expand Up @@ -52,10 +52,12 @@ def libvirt_info(self, disk_bus, disk_dev, device_type,

return FakeImage(instance, name)

def snapshot(self, path, image_type=''):
def snapshot(self, instance, disk_path, image_type=''):
# NOTE(bfilippov): this is done in favor for
# snapshot tests in test_libvirt.LibvirtConnTestCase
return imagebackend.Backend(True).snapshot(path, image_type)
return imagebackend.Backend(True).snapshot(instance,
disk_path,
image_type=image_type)


class Raw(imagebackend.Image):
Expand Down
7 changes: 6 additions & 1 deletion nova/tests/virt/libvirt/fake_libvirt_utils.py
Expand Up @@ -156,7 +156,12 @@ def file_open(path, mode=None):


def find_disk(virt_dom):
return "filename"
if disk_type == 'lvm':
return "/dev/nova-vg/lv"
elif disk_type in ['raw', 'qcow2']:
return "filename"
else:
return "unknown_type_disk"


def load_file(path):
Expand Down
158 changes: 155 additions & 3 deletions nova/tests/virt/libvirt/test_driver.py
Expand Up @@ -3634,6 +3634,83 @@ def convert_image(source, dest, out_format):
self.assertEqual(snapshot['disk_format'], 'raw')
self.assertEqual(snapshot['name'], snapshot_name)

def test_lvm_snapshot_in_raw_format(self):
# Tests Lvm backend snapshot functionality with raw format
# snapshots.
xml = """
<domain type='kvm'>
<devices>
<disk type='block' device='disk'>
<source dev='/dev/some-vg/some-lv'/>
</disk>
</devices>
</domain>
"""
update_task_state_calls = [
mock.call(task_state=task_states.IMAGE_PENDING_UPLOAD),
mock.call(task_state=task_states.IMAGE_UPLOADING,
expected_state=task_states.IMAGE_PENDING_UPLOAD)]
mock_update_task_state = mock.Mock()
mock_lookupByName = mock.Mock(return_value=FakeVirtDomain(xml),
autospec=True)
volume_info = {'VG': 'nova-vg', 'LV': 'disk'}
mock_volume_info = mock.Mock(return_value=volume_info,
autospec=True)
mock_volume_info_calls = [mock.call('/dev/nova-vg/lv')]
mock_convert_image = mock.Mock()

def convert_image_side_effect(source, dest, out_format,
run_as_root=True):
libvirt_driver.libvirt_utils.files[dest] = ''
mock_convert_image.side_effect = convert_image_side_effect

self.flags(snapshots_directory='./',
snapshot_image_format='raw',
images_type='lvm',
images_volume_group='nova-vg', group='libvirt')
libvirt_driver.libvirt_utils.disk_type = "lvm"

# Start test
image_service = nova.tests.image.fake.FakeImageService()
instance_ref = db.instance_create(self.context, self.test_instance)
properties = {'instance_id': instance_ref['id'],
'user_id': str(self.context.user_id)}
snapshot_name = 'test-snap'
sent_meta = {'name': snapshot_name, 'is_public': False,
'status': 'creating', 'properties': properties}
recv_meta = image_service.create(context, sent_meta)

conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
with contextlib.nested(
mock.patch.object(libvirt_driver.LibvirtDriver,
'_conn',
autospec=True),
mock.patch.object(libvirt_driver.imagebackend.lvm,
'volume_info',
mock_volume_info),
mock.patch.object(libvirt_driver.imagebackend.images,
'convert_image',
mock_convert_image),
mock.patch.object(libvirt_driver.LibvirtDriver,
'_lookup_by_name',
mock_lookupByName)):
conn.snapshot(self.context, instance_ref, recv_meta['id'],
mock_update_task_state)

mock_lookupByName.assert_called_once_with("instance-00000001")
mock_volume_info.assert_has_calls(mock_volume_info_calls)
mock_convert_image.assert_called_once()
snapshot = image_service.show(context, recv_meta['id'])
mock_update_task_state.assert_has_calls(update_task_state_calls)
self.assertEqual('available', snapshot['properties']['image_state'])
self.assertEqual('active', snapshot['status'])
self.assertEqual('raw', snapshot['disk_format'])
self.assertEqual(snapshot_name, snapshot['name'])
# This is for all the subsequent tests that do not set the value of
# images type
self.flags(images_type='default', group='libvirt')
libvirt_driver.libvirt_utils.disk_type = "qcow2"

def test_lxc_snapshot_in_raw_format(self):
expected_calls = [
{'args': (),
Expand Down Expand Up @@ -3665,6 +3742,7 @@ def test_lxc_snapshot_in_raw_format(self):
self.mox.StubOutWithMock(libvirt_driver.utils, 'execute')
libvirt_driver.utils.execute = self.fake_execute
self.stubs.Set(libvirt_driver.libvirt_utils, 'disk_type', 'raw')
libvirt_driver.libvirt_utils.disk_type = "raw"

def convert_image(source, dest, out_format):
libvirt_driver.libvirt_utils.files[dest] = ''
Expand Down Expand Up @@ -3775,6 +3853,80 @@ def test_lxc_snapshot_in_qcow2_format(self):
self.assertEqual(snapshot['disk_format'], 'qcow2')
self.assertEqual(snapshot['name'], snapshot_name)

def test_lvm_snapshot_in_qcow2_format(self):
# Tests Lvm backend snapshot functionality with raw format
# snapshots.
xml = """
<domain type='kvm'>
<devices>
<disk type='block' device='disk'>
<source dev='/dev/some-vg/some-lv'/>
</disk>
</devices>
</domain>
"""
update_task_state_calls = [
mock.call(task_state=task_states.IMAGE_PENDING_UPLOAD),
mock.call(task_state=task_states.IMAGE_UPLOADING,
expected_state=task_states.IMAGE_PENDING_UPLOAD)]
mock_update_task_state = mock.Mock()
mock_lookupByName = mock.Mock(return_value=FakeVirtDomain(xml),
autospec=True)
volume_info = {'VG': 'nova-vg', 'LV': 'disk'}
mock_volume_info = mock.Mock(return_value=volume_info, autospec=True)
mock_volume_info_calls = [mock.call('/dev/nova-vg/lv')]
mock_convert_image = mock.Mock()

def convert_image_side_effect(source, dest, out_format,
run_as_root=True):
libvirt_driver.libvirt_utils.files[dest] = ''
mock_convert_image.side_effect = convert_image_side_effect

self.flags(snapshots_directory='./',
snapshot_image_format='qcow2',
images_type='lvm',
images_volume_group='nova-vg', group='libvirt')
libvirt_driver.libvirt_utils.disk_type = "lvm"

# Start test
image_service = nova.tests.image.fake.FakeImageService()
instance_ref = db.instance_create(self.context, self.test_instance)
properties = {'instance_id': instance_ref['id'],
'user_id': str(self.context.user_id)}
snapshot_name = 'test-snap'
sent_meta = {'name': snapshot_name, 'is_public': False,
'status': 'creating', 'properties': properties}
recv_meta = image_service.create(context, sent_meta)

conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
with contextlib.nested(
mock.patch.object(libvirt_driver.LibvirtDriver,
'_conn',
autospec=True),
mock.patch.object(libvirt_driver.imagebackend.lvm,
'volume_info',
mock_volume_info),
mock.patch.object(libvirt_driver.imagebackend.images,
'convert_image',
mock_convert_image),
mock.patch.object(libvirt_driver.LibvirtDriver,
'_lookup_by_name',
mock_lookupByName)):
conn.snapshot(self.context, instance_ref, recv_meta['id'],
mock_update_task_state)

mock_lookupByName.assert_called_once_with("instance-00000001")
mock_volume_info.assert_has_calls(mock_volume_info_calls)
mock_convert_image.assert_called_once()
snapshot = image_service.show(context, recv_meta['id'])
mock_update_task_state.assert_has_calls(update_task_state_calls)
self.assertEqual('available', snapshot['properties']['image_state'])
self.assertEqual('active', snapshot['status'])
self.assertEqual('qcow2', snapshot['disk_format'])
self.assertEqual(snapshot_name, snapshot['name'])
self.flags(images_type='default', group='libvirt')
libvirt_driver.libvirt_utils.disk_type = "qcow2"

def test_snapshot_no_image_architecture(self):
expected_calls = [
{'args': (),
Expand Down Expand Up @@ -3857,6 +4009,7 @@ def test_lxc_snapshot_no_image_architecture(self):
libvirt_driver.LibvirtDriver._conn.lookupByName = self.fake_lookup
self.mox.StubOutWithMock(libvirt_driver.utils, 'execute')
libvirt_driver.utils.execute = self.fake_execute
libvirt_driver.libvirt_utils.disk_type = "qcow2"

self.mox.ReplayAll()

Expand Down Expand Up @@ -3927,6 +4080,7 @@ def test_lxc_snapshot_no_original_image(self):
self.flags(snapshots_directory='./',
virt_type='lxc',
group='libvirt')
libvirt_driver.libvirt_utils.disk_type = "qcow2"

# Assign a non-existent image
test_instance = copy.deepcopy(self.test_instance)
Expand Down Expand Up @@ -6281,7 +6435,6 @@ def _test_destroy_removes_disk(self, volume_fail=False):

self.mox.StubOutWithMock(libvirt_driver.LibvirtDriver,
'_undefine_domain')
libvirt_driver.LibvirtDriver._undefine_domain(instance)
self.mox.StubOutWithMock(db, 'instance_get_by_uuid')
db.instance_get_by_uuid(mox.IgnoreArg(), mox.IgnoreArg(),
columns_to_join=['info_cache',
Expand All @@ -6304,8 +6457,7 @@ def _test_destroy_removes_disk(self, volume_fail=False):
'delete_instance_files')
(libvirt_driver.LibvirtDriver.delete_instance_files(mox.IgnoreArg()).
AndReturn(True))
self.mox.StubOutWithMock(libvirt_driver.LibvirtDriver, '_cleanup_lvm')
libvirt_driver.LibvirtDriver._cleanup_lvm(instance)
libvirt_driver.LibvirtDriver._undefine_domain(instance)

# Start test
self.mox.ReplayAll()
Expand Down

0 comments on commit 5fa74bc

Please sign in to comment.