Move libvirts dmcrypt support to privsep.

This is the first example of something where we can't just move
to python calls on the far side of the trust boundary. So we just
move the executes() to the trusted sie and make sure their
attack surface is as small as possible.

Change-Id: Ib8d3b48f5a1482a7a040e58bec6b3a599c8c6fd0
blueprint: hurrah-for-privsep
This commit is contained in:
Michael Still 2017-09-18 23:19:49 +10:00
parent 90e91ca052
commit a059e70486
4 changed files with 46 additions and 24 deletions

View File

@ -17,9 +17,12 @@
libvirt specific routines.
"""
import binascii
import errno
import os
from oslo_concurrency import processutils
import nova.privsep
@ -56,6 +59,36 @@ def _last_bytes_inner(file_like_object, num):
return (file_like_object.read(), remaining)
@nova.privsep.sys_admin_pctxt.entrypoint
def dmcrypt_create_volume(target, device, cipher, key_size, key):
"""Sets up a dmcrypt mapping
:param target: device mapper logical device name
:param device: underlying block device
:param cipher: encryption cipher string digestible by cryptsetup
:param key_size: encryption key size
:param key: encoded encryption key bytestring
"""
cmd = ('cryptsetup',
'create',
target,
device,
'--cipher=' + cipher,
'--key-size=' + str(key_size),
'--key-file=-')
key = binascii.hexlify(key).decode('utf-8')
processutils.execute(*cmd, process_input=key)
@nova.privsep.sys_admin_pctxt.entrypoint
def dmcrypt_delete_volume(target):
"""Deletes a dmcrypt mapping
:param target: name of the mapped logical device
"""
processutils.execute('cryptsetup', 'remove', target)
@nova.privsep.sys_admin_pctxt.entrypoint
def enable_hairpin(interface):
"""Enable hairpin mode for a libvirt guest."""

View File

@ -33,21 +33,18 @@ class LibvirtDmcryptTestCase(test.NoDBTestCase):
self.KEY = bytes(bytearray(x for x in range(0, self.KEY_SIZE)))
self.KEY_STR = binascii.hexlify(self.KEY).decode('utf-8')
@mock.patch('nova.utils.execute')
@mock.patch('nova.privsep.libvirt.dmcrypt_create_volume')
def test_create_volume(self, mock_execute):
dmcrypt.create_volume(self.TARGET, self.PATH, self.CIPHER,
self.KEY_SIZE, self.KEY)
mock_execute.assert_has_calls([
mock.call('cryptsetup', 'create', self.TARGET, self.PATH,
'--cipher=' + self.CIPHER,
'--key-size=' + str(self.KEY_SIZE),
'--key-file=-', process_input=self.KEY_STR,
run_as_root=True),
mock.call(self.TARGET, self.PATH, self.CIPHER, self.KEY_SIZE,
self.KEY)
])
@mock.patch('nova.virt.libvirt.storage.dmcrypt.LOG')
@mock.patch('nova.utils.execute')
@mock.patch('nova.privsep.libvirt.dmcrypt_create_volume')
def test_create_volume_fail(self, mock_execute, mock_log):
mock_execute.side_effect = processutils.ProcessExecutionError()
@ -58,16 +55,16 @@ class LibvirtDmcryptTestCase(test.NoDBTestCase):
self.assertEqual(1, mock_execute.call_count)
self.assertEqual(1, mock_log.error.call_count) # error logged
@mock.patch('nova.utils.execute')
@mock.patch('nova.privsep.libvirt.dmcrypt_delete_volume')
def test_delete_volume(self, mock_execute):
dmcrypt.delete_volume(self.TARGET)
mock_execute.assert_has_calls([
mock.call('cryptsetup', 'remove', self.TARGET, run_as_root=True),
mock.call(self.TARGET),
])
@mock.patch('nova.virt.libvirt.storage.dmcrypt.LOG')
@mock.patch('nova.utils.execute')
@mock.patch('nova.privsep.libvirt.dmcrypt_delete_volume')
def test_delete_volume_fail(self, mock_execute, mock_log):
mock_execute.side_effect = processutils.ProcessExecutionError()
@ -78,7 +75,7 @@ class LibvirtDmcryptTestCase(test.NoDBTestCase):
self.assertEqual(1, mock_log.error.call_count) # error logged
@mock.patch('nova.virt.libvirt.storage.dmcrypt.LOG')
@mock.patch('nova.utils.execute')
@mock.patch('nova.privsep.libvirt.dmcrypt_delete_volume')
def test_delete_missing_volume(self, mock_execute, mock_log):
mock_execute.side_effect = \
processutils.ProcessExecutionError(exit_code=4)

View File

@ -13,14 +13,13 @@
# License for the specific language governing permissions and limitations
# under the License.
import binascii
import os
from oslo_concurrency import processutils
from oslo_log import log as logging
from oslo_utils import excutils
from nova import utils
import nova.privsep.libvirt
LOG = logging.getLogger(__name__)
@ -54,16 +53,9 @@ def create_volume(target, device, cipher, key_size, key):
:param key_size: encryption key size
:param key: encoded encryption key bytestring
"""
cmd = ('cryptsetup',
'create',
target,
device,
'--cipher=' + cipher,
'--key-size=' + str(key_size),
'--key-file=-')
key = binascii.hexlify(key).decode('utf-8')
try:
utils.execute(*cmd, process_input=key, run_as_root=True)
nova.privsep.libvirt.dmcrypt_create_volume(
target, device, cipher, key_size, key)
except processutils.ProcessExecutionError as e:
with excutils.save_and_reraise_exception():
LOG.error("Could not start encryption for disk %(device)s: "
@ -76,7 +68,7 @@ def delete_volume(target):
:param target: name of the mapped logical device
"""
try:
utils.execute('cryptsetup', 'remove', target, run_as_root=True)
nova.privsep.libvirt.dmcrypt_delete_volume(target)
except processutils.ProcessExecutionError as e:
# cryptsetup returns 4 when attempting to destroy a non-existent
# dm-crypt device. It indicates that the device is invalid, which

View File

@ -5,4 +5,4 @@ upgrade:
rootwrap configuration.
- |
The following commands are no longer required to be listed in your rootwrap
configuration: cat; chown; readlink; tee; touch.
configuration: cat; chown; cryptsetup; readlink; tee; touch.