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
This commit is contained in:
Joel Coffman 2013-09-10 17:01:08 -04:00
parent b05eef9201
commit cf5645fdee
10 changed files with 415 additions and 96 deletions

View File

@ -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=<None>
# 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=<None>
[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=<None>
#
# 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=<None>
# 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=<None>
#fixed_key=<None>
[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=<None>
# 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=<None>
# 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=<None>
# 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=<None>
[image_file_url]

View File

@ -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")

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)