From cf5645fdee2dfb9fc0e34b0355fa8c1e97758ced Mon Sep 17 00:00:00 2001 From: Joel Coffman Date: Tue, 10 Sep 2013 17:01:08 -0400 Subject: [PATCH] Add key manager implementation with static key Per feedback received on other patch sets, an example key manager driver is required to support ephemeral storage encryption and Cinder volume encryption. The ConfKeyManager class reads its key from the project's configuration file and provides this key for *all* requests. As such, this key manager is insecure but allows the aforementioned encryption features to be used without further integration effort. To clarify the above statements, the configuration-based key manager uses a single, fixed key. When used to encrypt data (e.g., by the Cinder volume encryption feature), the encryption provides limited protection for the confidentiality of data. For example, data cannot be read from a lost or stolen disk, and a volume's contents cannot be reconstructed if an attacker intercepts the iSCSI traffic between the compute and storage host. If the key is ever compromised, then any data encrypted with the key can be decrypted. Implements blueprint encrypt-cinder-volumes SecurityImpact Change-Id: Ia6f4c69e699e68065c0f767e769cd0a6f5cc623b --- etc/nova/nova.conf.sample | 157 ++++++++++++----------- nova/exception.py | 4 + nova/keymgr/__init__.py | 12 +- nova/keymgr/conf_key_mgr.py | 65 ++++++++++ nova/keymgr/key.py | 12 ++ nova/tests/keymgr/mock_key_mgr.py | 52 +++++--- nova/tests/keymgr/single_key_mgr.py | 73 +++++++++++ nova/tests/keymgr/test_conf_key_mgr.py | 48 +++++++ nova/tests/keymgr/test_key.py | 12 ++ nova/tests/keymgr/test_single_key_mgr.py | 76 +++++++++++ 10 files changed, 415 insertions(+), 96 deletions(-) create mode 100644 nova/keymgr/conf_key_mgr.py create mode 100644 nova/tests/keymgr/single_key_mgr.py create mode 100644 nova/tests/keymgr/test_conf_key_mgr.py create mode 100644 nova/tests/keymgr/test_single_key_mgr.py diff --git a/etc/nova/nova.conf.sample b/etc/nova/nova.conf.sample index 0293f6a91c36..199cb7150bc2 100644 --- a/etc/nova/nova.conf.sample +++ b/etc/nova/nova.conf.sample @@ -971,15 +971,6 @@ #ipv6_backend=rfc2462 -# -# Options defined in nova.keymgr -# - -# The full class name of the key manager API class (string -# value) -#keymgr_api_class=nova.keymgr.not_implemented_key_mgr.NotImplementedKeyManager - - # # Options defined in nova.network # @@ -2622,6 +2613,28 @@ #volume_attach_retry_interval=5 +[zookeeper] + +# +# Options defined in nova.servicegroup.drivers.zk +# + +# The ZooKeeper addresses for servicegroup service in the +# format of host1:port,host2:port,host3:port (string value) +#address= + +# recv_timeout parameter for the zk session (integer value) +#recv_timeout=4000 + +# The prefix used in ZooKeeper to store ephemeral nodes +# (string value) +#sg_prefix=/servicegroups + +# Number of seconds to wait until retrying to join the session +# (integer value) +#sg_retry_interval=5 + + [osapi_v3] # @@ -2661,67 +2674,24 @@ #workers= -[database] +[keymgr] # -# Options defined in nova.openstack.common.db.api +# Options defined in nova.keymgr # -# The backend to use for db (string value) -#backend=sqlalchemy - -# Enable the experimental use of thread pooling for all DB API -# calls (boolean value) -#use_tpool=false - - -# -# Options defined in nova.openstack.common.db.sqlalchemy.session -# - -# The SQLAlchemy connection string used to connect to the -# database (string value) -#connection=sqlite:////nova/openstack/common/db/$sqlite_db - -# The SQLAlchemy connection string used to connect to the -# slave database (string value) -#slave_connection= - -# timeout before idle sql connections are reaped (integer +# The full class name of the key manager API class (string # value) -#idle_timeout=3600 +#api_class=nova.keymgr.conf_key_mgr.ConfKeyManager -# Minimum number of SQL connections to keep open in a pool -# (integer value) -#min_pool_size=1 -# Maximum number of SQL connections to keep open in a pool -# (integer value) -#max_pool_size= +# +# Options defined in nova.keymgr.conf_key_mgr +# -# maximum db connection retries during startup. (setting -1 -# implies an infinite retry count) (integer value) -#max_retries=10 - -# interval between retries of opening a sql connection -# (integer value) -#retry_interval=10 - -# If set, use this value for max_overflow with sqlalchemy -# (integer value) -#max_overflow= - -# Verbosity of SQL debugging information. 0=None, -# 100=Everything (integer value) -#connection_debug=0 - -# Add python stack traces to SQL as comment strings (boolean +# Fixed key returned by key manager, specified in hex (string # value) -#connection_trace=false - -# If set, use this value for pool_timeout with sqlalchemy -# (integer value) -#pool_timeout= +#fixed_key= [cells] @@ -2862,26 +2832,67 @@ #ram_weight_multiplier=10.0 -[zookeeper] +[database] # -# Options defined in nova.servicegroup.drivers.zk +# Options defined in nova.openstack.common.db.api # -# The ZooKeeper addresses for servicegroup service in the -# format of host1:port,host2:port,host3:port (string value) -#address= +# The backend to use for db (string value) +#backend=sqlalchemy -# recv_timeout parameter for the zk session (integer value) -#recv_timeout=4000 +# Enable the experimental use of thread pooling for all DB API +# calls (boolean value) +#use_tpool=false -# The prefix used in ZooKeeper to store ephemeral nodes -# (string value) -#sg_prefix=/servicegroups -# Number of seconds to wait until retrying to join the session +# +# Options defined in nova.openstack.common.db.sqlalchemy.session +# + +# The SQLAlchemy connection string used to connect to the +# database (string value) +#connection=sqlite:////nova/openstack/common/db/$sqlite_db + +# The SQLAlchemy connection string used to connect to the +# slave database (string value) +#slave_connection= + +# timeout before idle sql connections are reaped (integer +# value) +#idle_timeout=3600 + +# Minimum number of SQL connections to keep open in a pool # (integer value) -#sg_retry_interval=5 +#min_pool_size=1 + +# Maximum number of SQL connections to keep open in a pool +# (integer value) +#max_pool_size= + +# maximum db connection retries during startup. (setting -1 +# implies an infinite retry count) (integer value) +#max_retries=10 + +# interval between retries of opening a sql connection +# (integer value) +#retry_interval=10 + +# If set, use this value for max_overflow with sqlalchemy +# (integer value) +#max_overflow= + +# Verbosity of SQL debugging information. 0=None, +# 100=Everything (integer value) +#connection_debug=0 + +# Add python stack traces to SQL as comment strings (boolean +# value) +#connection_trace=false + +# If set, use this value for pool_timeout with sqlalchemy +# (integer value) +#pool_timeout= [image_file_url] diff --git a/nova/exception.py b/nova/exception.py index f8709854818d..dc530a3f07ae 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -1481,3 +1481,7 @@ class PciDeviceDetachFailed(NovaException): class PciDeviceUnsupportedHypervisor(NovaException): msg_fmt = _("%(type)s hypervisor does not support PCI devices") + + +class KeyManagerError(NovaException): + msg_fmt = _("key manager error: %(reason)s") diff --git a/nova/keymgr/__init__.py b/nova/keymgr/__init__.py index 101eb57cfd26..83275bf296ce 100644 --- a/nova/keymgr/__init__.py +++ b/nova/keymgr/__init__.py @@ -14,25 +14,25 @@ # License for the specific language governing permissions and limitations # under the License. + from oslo.config import cfg from nova.openstack.common import importutils from nova.openstack.common import log as logging + keymgr_opts = [ - cfg.StrOpt('keymgr_api_class', - default='nova.keymgr.' - 'not_implemented_key_mgr.NotImplementedKeyManager', + cfg.StrOpt('api_class', + default='nova.keymgr.conf_key_mgr.ConfKeyManager', help='The full class name of the key manager API class'), ] CONF = cfg.CONF -CONF.register_opts(keymgr_opts) +CONF.register_opts(keymgr_opts, group='keymgr') LOG = logging.getLogger(__name__) def API(): - keymgr_api_class = CONF.keymgr_api_class - cls = importutils.import_class(keymgr_api_class) + cls = importutils.import_class(CONF.keymgr.api_class) return cls() diff --git a/nova/keymgr/conf_key_mgr.py b/nova/keymgr/conf_key_mgr.py new file mode 100644 index 000000000000..1240cb487b81 --- /dev/null +++ b/nova/keymgr/conf_key_mgr.py @@ -0,0 +1,65 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +An implementation of a key manager that reads its key from the project's +configuration options. + +This key manager implementation provides limited security, assuming that the +key remains secret. Using the volume encryption feature as an example, +encryption provides protection against a lost or stolen disk, assuming that +the configuration file that contains the key is not stored on the disk. +Encryption also protects the confidentiality of data as it is transmitted via +iSCSI from the compute host to the storage host (again assuming that an +attacker who intercepts the data does not know the secret key). + +Because this implementation uses a single, fixed key, it proffers no +protection once that key is compromised. In particular, different volumes +encrypted with a key provided by this key manager actually share the same +encryption key so *any* volume can be decrypted once the fixed key is known. +""" + +from oslo.config import cfg + +from nova.openstack.common.gettextutils import _ +from nova.tests.keymgr import single_key_mgr + +key_mgr_opts = [ + cfg.StrOpt('fixed_key', + help='Fixed key returned by key manager, specified in hex'), +] + +CONF = cfg.CONF +CONF.register_opts(key_mgr_opts, group='keymgr') + + +class ConfKeyManager(single_key_mgr.SingleKeyManager): + """ + This key manager implementation supports all the methods specified by the + key manager interface. This implementation creates a single key in response + to all invocations of create_key. Side effects (e.g., raising exceptions) + for each method are handled as specified by the key manager interface. + """ + + def __init__(self): + super(ConfKeyManager, self).__init__() + + if CONF.keymgr.fixed_key is None: + raise ValueError(_('keymgr.fixed_key not defined')) + self._hex_key = CONF.keymgr.fixed_key + + def _get_hex_key(self): + return self._hex_key diff --git a/nova/keymgr/key.py b/nova/keymgr/key.py index 3e3f1394122c..644cf34c4d52 100644 --- a/nova/keymgr/key.py +++ b/nova/keymgr/key.py @@ -79,3 +79,15 @@ class SymmetricKey(Key): def get_encoded(self): """Returns the key in its encoded format.""" return self.key + + def __eq__(self, other): + if isinstance(other, SymmetricKey): + return (self.alg == other.alg and + self.key == other.key) + return NotImplemented + + def __ne__(self, other): + result = self.__eq__(other) + if result is NotImplemented: + return result + return not result diff --git a/nova/tests/keymgr/mock_key_mgr.py b/nova/tests/keymgr/mock_key_mgr.py index 4489fe567a34..5a0f8c21d65a 100644 --- a/nova/tests/keymgr/mock_key_mgr.py +++ b/nova/tests/keymgr/mock_key_mgr.py @@ -15,8 +15,16 @@ # under the License. """ -A mock implementation of a key manager. This module should NOT be used for -anything but integration testing. +A mock implementation of a key manager that stores keys in a dictionary. + +This key manager implementation is primarily intended for testing. In +particular, it does not store keys persistently. Lack of a centralized key +store also makes this implementation unsuitable for use among different +services. + +Note: Instantiating this class multiple times will create separate key stores. +Keys created in one instance will not be accessible from other instances of +this class. """ import array @@ -24,6 +32,7 @@ import array from nova import exception from nova.keymgr import key from nova.keymgr import key_mgr +from nova.openstack.common.gettextutils import _ from nova.openstack.common import log as logging from nova.openstack.common import uuidutils from nova import utils @@ -40,13 +49,27 @@ class MockKeyManager(key_mgr.KeyManager): services. Side effects (e.g., raising exceptions) for each method are handled as specified by the key manager interface. - This class should NOT be used for anything but integration testing because - keys are not stored persistently. + This key manager is not suitable for use in production deployments. """ def __init__(self): + LOG.warn(_('This key manager is not suitable for use in production' + ' deployments')) + self.keys = {} + def _generate_hex_key(self, **kwargs): + key_length = kwargs.get('key_length', 256) + # hex digit => 4 bits + hex_encoded = utils.generate_password(length=key_length / 4, + symbolgroups='0123456789ABCDEF') + return hex_encoded + + def _generate_key(self, **kwargs): + _hex = self._generate_hex_key(**kwargs) + return key.SymmetricKey('AES', + array.array('B', _hex.decode('hex')).tolist()) + def create_key(self, ctxt, **kwargs): """Creates a key. @@ -56,16 +79,15 @@ class MockKeyManager(key_mgr.KeyManager): if ctxt is None: raise exception.NotAuthorized() - # generate the key - key_length = kwargs.get('key_length', 256) - # hex digit => 4 bits - hex_string = utils.generate_password(length=key_length / 4, - symbolgroups='0123456789ABCDEF') + key = self._generate_key(**kwargs) + return self.store_key(ctxt, key) - _bytes = array.array('B', hex_string.decode('hex')).tolist() - _key = key.SymmetricKey('AES', _bytes) + def _generate_key_id(self): + key_id = uuidutils.generate_uuid() + while key_id in self.keys: + key_id = uuidutils.generate_uuid() - return self.store_key(ctxt, _key) + return key_id def _generate_key_id(self): key_id = uuidutils.generate_uuid() @@ -75,11 +97,7 @@ class MockKeyManager(key_mgr.KeyManager): return key_id def store_key(self, ctxt, key, **kwargs): - """Stores (i.e., registers) a key with the key manager. - - This implementation does nothing -- i.e., the specified key is - discarded. - """ + """Stores (i.e., registers) a key with the key manager.""" if ctxt is None: raise exception.NotAuthorized() diff --git a/nova/tests/keymgr/single_key_mgr.py b/nova/tests/keymgr/single_key_mgr.py new file mode 100644 index 000000000000..b5aca56d09c2 --- /dev/null +++ b/nova/tests/keymgr/single_key_mgr.py @@ -0,0 +1,73 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +An implementation of a key manager that returns a single key in response to +all invocations of get_key. +""" + + +from nova import exception +from nova.openstack.common.gettextutils import _ +from nova.openstack.common import log as logging +from nova.tests.keymgr import mock_key_mgr + + +LOG = logging.getLogger(__name__) + + +class SingleKeyManager(mock_key_mgr.MockKeyManager): + """ + This key manager implementation supports all the methods specified by the + key manager interface. This implementation creates a single key in response + to all invocations of create_key. Side effects (e.g., raising exceptions) + for each method are handled as specified by the key manager interface. + """ + + def __init__(self): + LOG.warn(_('This key manager is insecure and is not recommended for ' + 'production deployments')) + super(SingleKeyManager, self).__init__() + + self.key_id = '00000000-0000-0000-0000-000000000000' + self.key = self._generate_key(key_length=256) + + # key should exist by default + self.keys[self.key_id] = self.key + + def _generate_hex_key(self, **kwargs): + key_length = kwargs.get('key_length', 256) + return '0' * (key_length / 4) # hex digit => 4 bits + + def _generate_key_id(self): + return self.key_id + + def store_key(self, ctxt, key, **kwargs): + if key != self.key: + raise exception.KeyManagerError( + reason="cannot store arbitrary keys") + + return super(SingleKeyManager, self).store_key(ctxt, key, **kwargs) + + def delete_key(self, ctxt, key_id, **kwargs): + if ctxt is None: + raise exception.NotAuthorized() + + if key_id != self.key_id: + raise exception.KeyManagerError( + reason="cannot delete non-existent key") + + LOG.warn(_("Not deleting key %s"), key_id) diff --git a/nova/tests/keymgr/test_conf_key_mgr.py b/nova/tests/keymgr/test_conf_key_mgr.py new file mode 100644 index 000000000000..35e9eccfaf87 --- /dev/null +++ b/nova/tests/keymgr/test_conf_key_mgr.py @@ -0,0 +1,48 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Test cases for the conf key manager. +""" + +import array + +from oslo.config import cfg + +from nova.keymgr import conf_key_mgr +from nova.keymgr import key +from nova.tests.keymgr import test_single_key_mgr + + +CONF = cfg.CONF +CONF.import_opt('fixed_key', 'nova.keymgr.conf_key_mgr', group='keymgr') + + +class ConfKeyManagerTestCase(test_single_key_mgr.SingleKeyManagerTestCase): + def __init__(self, *args, **kwargs): + super(ConfKeyManagerTestCase, self).__init__(*args, **kwargs) + + self._hex_key = '0' * 64 + + def _create_key_manager(self): + CONF.set_default('fixed_key', default=self._hex_key, group='keymgr') + return conf_key_mgr.ConfKeyManager() + + def setUp(self): + super(ConfKeyManagerTestCase, self).setUp() + + encoded_key = array.array('B', self._hex_key.decode('hex')).tolist() + self.key = key.SymmetricKey('AES', encoded_key) diff --git a/nova/tests/keymgr/test_key.py b/nova/tests/keymgr/test_key.py index d0d6eea5a61f..27cdb165a821 100644 --- a/nova/tests/keymgr/test_key.py +++ b/nova/tests/keymgr/test_key.py @@ -55,3 +55,15 @@ class SymmetricKeyTestCase(KeyTestCase): def test_get_encoded(self): self.assertEquals(self.key.get_encoded(), self.encoded) + + def test___eq__(self): + self.assertTrue(self.key == self.key) + + self.assertFalse(self.key == None) + self.assertFalse(None == self.key) + + def test___ne__(self): + self.assertFalse(self.key != self.key) + + self.assertTrue(self.key != None) + self.assertTrue(None != self.key) diff --git a/nova/tests/keymgr/test_single_key_mgr.py b/nova/tests/keymgr/test_single_key_mgr.py new file mode 100644 index 000000000000..a44a06d03993 --- /dev/null +++ b/nova/tests/keymgr/test_single_key_mgr.py @@ -0,0 +1,76 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Test cases for the single key manager. +""" + +import array + +from nova import context +from nova import exception +from nova.keymgr import key +from nova.tests.keymgr import single_key_mgr +from nova.tests.keymgr import test_mock_key_mgr + + +class SingleKeyManagerTestCase(test_mock_key_mgr.MockKeyManagerTestCase): + + def _create_key_manager(self): + return single_key_mgr.SingleKeyManager() + + def setUp(self): + super(SingleKeyManagerTestCase, self).setUp() + + self.ctxt = context.RequestContext('fake', 'fake') + + self.key_id = '00000000-0000-0000-0000-000000000000' + encoded = array.array('B', ('0' * 64).decode('hex')).tolist() + self.key = key.SymmetricKey('AES', encoded) + + def test___init__(self): + self.assertEqual(self.key, + self.key_mgr.get_key(self.ctxt, self.key_id)) + + def test_create_key(self): + key_id_1 = self.key_mgr.create_key(self.ctxt) + key_id_2 = self.key_mgr.create_key(self.ctxt) + # ensure that the UUIDs are the same + self.assertEqual(key_id_1, key_id_2) + + def test_create_key_with_length(self): + pass + + def test_store_null_context(self): + self.assertRaises(exception.NotAuthorized, + self.key_mgr.store_key, None, self.key) + + def test_copy_key(self): + key_id = self.key_mgr.create_key(self.ctxt) + key = self.key_mgr.get_key(self.ctxt, key_id) + + copied_key_id = self.key_mgr.copy_key(self.ctxt, key_id) + copied_key = self.key_mgr.get_key(self.ctxt, copied_key_id) + + self.assertEqual(key_id, copied_key_id) + self.assertEqual(key, copied_key) + + def test_delete_key(self): + pass + + def test_delete_unknown_key(self): + self.assertRaises(exception.KeyManagerError, + self.key_mgr.delete_key, self.ctxt, None)