Merge "Multi-key KMIP keymaster"

This commit is contained in:
Zuul 2018-08-21 15:15:05 +00:00 committed by Gerrit Code Review
commit b32578b5d4
2 changed files with 171 additions and 61 deletions

View File

@ -38,6 +38,8 @@ and add a new filter section::
[filter:kmip_keymaster]
use = egg:swift#kmip_keymaster
key_id = <unique id of secret to be fetched from the KMIP service>
key_id_<secret_id> = <unique id of additional secret to be fetched>
active_root_secret_id = <secret_id to be used for new encryptions>
host = <KMIP server host>
port = <KMIP server port>
certfile = /path/to/client/cert.pem
@ -46,12 +48,27 @@ and add a new filter section::
username = <KMIP username>
password = <KMIP password>
Apart from ``use`` and ``key_id`` the options are as defined for a PyKMIP
client. The authoritative definition of these options can be found at
`https://pykmip.readthedocs.io/en/latest/client.html`_
Apart from ``use``, ``key_id*``, ``active_root_secret_id`` the options are
as defined for a PyKMIP client. The authoritative definition of these options
can be found at `https://pykmip.readthedocs.io/en/latest/client.html`_
The value of the ``key_id`` option should be the unique identifier for a secret
that will be retrieved from the KMIP service.
The value of each ``key_id*`` option should be a unique identifier for a secret
to be retrieved from the KMIP service. Any of these secrets may be used for
*decryption*.
The value of the ``active_root_secret_id`` option should be the ``secret_id``
for the secret that should be used for all new *encryption*. If not specified,
the ``key_id`` secret will be used.
.. note::
To ensure there is no loss of data availability, deploying a new key to
your cluster requires a two-stage config change. First, add the new key
to the ``key_id_<secret_id>`` option and restart the proxy-server. Do this
for all proxies. Next, set the ``active_root_secret_id`` option to the
new secret id and restart the proxy. Again, do this for all proxies. This
process ensures that all proxies will have the new key available for
*decryption* before any proxy uses it for *encryption*.
The keymaster configuration can alternatively be defined in a separate config
file by using the ``keymaster_config_path`` option::
@ -67,6 +84,9 @@ example::
[kmip_keymaster]
key_id = 1234567890
key_id_foo = 2468024680
key_id_bar = 1357913579
active_root_secret_id = foo
host = 127.0.0.1
port = 5696
certfile = /etc/swift/kmip_client.crt
@ -81,7 +101,7 @@ class KmipKeyMaster(keymaster.KeyMaster):
log_route = 'kmip_keymaster'
keymaster_opts = ('host', 'port', 'certfile', 'keyfile',
'ca_certs', 'username', 'password',
'active_root_secret_id', 'key_id')
'active_root_secret_id', 'key_id*')
keymaster_conf_section = 'kmip_keymaster'
def _get_root_secret(self, conf):
@ -96,25 +116,36 @@ class KmipKeyMaster(keymaster.KeyMaster):
'keymaster_config_path option in the proxy server config to '
'specify a config file.')
key_id = conf.get('key_id')
if not key_id:
raise ValueError('key_id option is required')
kmip_logger = logging.getLogger('kmip')
for handler in self.logger.logger.handlers:
kmip_logger.addHandler(handler)
multikey_opts = self._load_multikey_opts(conf, 'key_id')
if not multikey_opts:
raise ValueError('key_id option is required')
kmip_to_secret = {}
root_secrets = {}
with ProxyKmipClient(
config=section,
config_file=conf['__file__']
) as client:
secret = client.get(key_id)
if (secret.cryptographic_algorithm.name,
secret.cryptographic_length) != ('AES', 256):
raise ValueError('Expected an AES-256 key, not %s-%d' % (
secret.cryptographic_algorithm.name,
secret.cryptographic_length))
return secret.value
for opt, secret_id, kmip_id in multikey_opts:
if kmip_id in kmip_to_secret:
# Save some round trips if there are multiple
# secret_ids for a single kmip_id
root_secrets[secret_id] = root_secrets[
kmip_to_secret[kmip_id]]
continue
secret = client.get(kmip_id)
algo = secret.cryptographic_algorithm.name
length = secret.cryptographic_length
if (algo, length) != ('AES', 256):
raise ValueError(
'Expected key %s to be an AES-256 key, not %s-%d' % (
kmip_id, algo, length))
root_secrets[secret_id] = secret.value
kmip_to_secret.setdefault(kmip_id, secret_id)
return root_secrets
def filter_factory(global_conf, **local_conf):

View File

@ -29,13 +29,14 @@ from swift.common.middleware.crypto.kmip_keymaster import KmipKeyMaster
class MockProxyKmipClient(object):
def __init__(self, secret):
self.secret = secret
self.uid = None
def __init__(self, secrets, calls, kwargs):
calls.append(('__init__', kwargs))
self.secrets = secrets
self.calls = calls
def get(self, uid):
self.uid = uid
return self.secret
self.calls.append(('get', uid))
return self.secrets[uid]
def __enter__(self):
return self
@ -53,11 +54,11 @@ def create_secret(algorithm_name, length, value):
return secret
def create_mock_client(secret, calls):
def create_mock_client(secrets, calls):
def mock_client(*args, **kwargs):
client = MockProxyKmipClient(secret)
calls.append({'args': args, 'kwargs': kwargs, 'client': client})
return client
if args:
raise Exception('unexpected args provided: %r' % (args,))
return MockProxyKmipClient(secrets, calls, kwargs)
return mock_client
@ -73,18 +74,62 @@ class TestKmipKeymaster(unittest.TestCase):
conf = {'__file__': '/etc/swift/proxy-server.conf',
'__name__': 'filter:kmip_keymaster',
'key_id': '1234'}
secret = create_secret('AES', 256, b'x' * 32)
secrets = {'1234': create_secret('AES', 256, b'x' * 32)}
calls = []
klass = 'swift.common.middleware.crypto.kmip_keymaster.ProxyKmipClient'
with mock.patch(klass, create_mock_client(secret, calls)):
with mock.patch(klass, create_mock_client(secrets, calls)):
km = KmipKeyMaster(None, conf)
self.assertEqual(secret.value, km.root_secret)
self.assertEqual({None: b'x' * 32}, km._root_secrets)
self.assertEqual(None, km.active_secret_id)
self.assertIsNone(km.keymaster_config_path)
self.assertEqual({'config_file': '/etc/swift/proxy-server.conf',
'config': 'filter:kmip_keymaster'},
calls[0]['kwargs'])
self.assertEqual('1234', calls[0]['client'].uid)
self.assertEqual(calls, [
('__init__', {'config_file': '/etc/swift/proxy-server.conf',
'config': 'filter:kmip_keymaster'}),
('get', '1234'),
])
def test_multikey_config_in_filter_section(self):
conf = {'__file__': '/etc/swift/proxy-server.conf',
'__name__': 'filter:kmip_keymaster',
'key_id': '1234',
'key_id_xyzzy': 'foobar',
'key_id_alt_secret_id': 'foobar',
'active_root_secret_id': 'xyzzy'}
secrets = {'1234': create_secret('AES', 256, b'x' * 32),
'foobar': create_secret('AES', 256, b'y' * 32)}
calls = []
klass = 'swift.common.middleware.crypto.kmip_keymaster.ProxyKmipClient'
with mock.patch(klass, create_mock_client(secrets, calls)):
km = KmipKeyMaster(None, conf)
self.assertEqual({None: b'x' * 32, 'xyzzy': b'y' * 32,
'alt_secret_id': b'y' * 32},
km._root_secrets)
self.assertEqual('xyzzy', km.active_secret_id)
self.assertIsNone(km.keymaster_config_path)
self.assertEqual(calls, [
('__init__', {'config_file': '/etc/swift/proxy-server.conf',
'config': 'filter:kmip_keymaster'}),
('get', '1234'),
('get', 'foobar'),
])
def test_bad_active_key(self):
conf = {'__file__': '/etc/swift/proxy-server.conf',
'__name__': 'filter:kmip_keymaster',
'key_id': '1234',
'key_id_xyzzy': 'foobar',
'active_root_secret_id': 'unknown'}
secrets = {'1234': create_secret('AES', 256, b'x' * 32),
'foobar': create_secret('AES', 256, b'y' * 32)}
calls = []
klass = 'swift.common.middleware.crypto.kmip_keymaster.ProxyKmipClient'
with mock.patch(klass, create_mock_client(secrets, calls)), \
self.assertRaises(ValueError) as raised:
KmipKeyMaster(None, conf)
self.assertEqual('No secret loaded for active_root_secret_id unknown',
str(raised.exception))
def test_config_in_separate_file(self):
km_conf = """
@ -98,17 +143,48 @@ class TestKmipKeymaster(unittest.TestCase):
conf = {'__file__': '/etc/swift/proxy-server.conf',
'__name__': 'filter:kmip_keymaster',
'keymaster_config_path': km_config_file}
secret = create_secret('AES', 256, b'x' * 32)
secrets = {'4321': create_secret('AES', 256, b'x' * 32)}
calls = []
klass = 'swift.common.middleware.crypto.kmip_keymaster.ProxyKmipClient'
with mock.patch(klass, create_mock_client(secret, calls)):
with mock.patch(klass, create_mock_client(secrets, calls)):
km = KmipKeyMaster(None, conf)
self.assertEqual(secret.value, km.root_secret)
self.assertEqual({None: b'x' * 32}, km._root_secrets)
self.assertEqual(None, km.active_secret_id)
self.assertEqual(km_config_file, km.keymaster_config_path)
self.assertEqual({'config_file': km_config_file,
'config': 'kmip_keymaster'},
calls[0]['kwargs'])
self.assertEqual('4321', calls[0]['client'].uid)
self.assertEqual(calls, [
('__init__', {'config_file': km_config_file,
'config': 'kmip_keymaster'}),
('get', '4321')])
def test_multikey_config_in_separate_file(self):
km_conf = """
[kmip_keymaster]
key_id = 4321
key_id_secret_id = another id
active_root_secret_id = secret_id
"""
km_config_file = os.path.join(self.tempdir, 'km.conf')
with open(km_config_file, 'wb') as fd:
fd.write(dedent(km_conf))
conf = {'__file__': '/etc/swift/proxy-server.conf',
'__name__': 'filter:kmip_keymaster',
'keymaster_config_path': km_config_file}
secrets = {'4321': create_secret('AES', 256, b'x' * 32),
'another id': create_secret('AES', 256, b'y' * 32)}
calls = []
klass = 'swift.common.middleware.crypto.kmip_keymaster.ProxyKmipClient'
with mock.patch(klass, create_mock_client(secrets, calls)):
km = KmipKeyMaster(None, conf)
self.assertEqual({None: b'x' * 32, 'secret_id': b'y' * 32},
km._root_secrets)
self.assertEqual('secret_id', km.active_secret_id)
self.assertEqual(km_config_file, km.keymaster_config_path)
self.assertEqual(calls, [
('__init__', {'config_file': km_config_file,
'config': 'kmip_keymaster'}),
('get', '4321'),
('get', 'another id')])
def test_proxy_server_conf_dir(self):
proxy_server_conf_dir = os.path.join(self.tempdir, 'proxy_server.d')
@ -139,49 +215,52 @@ class TestKmipKeymaster(unittest.TestCase):
conf = {'__file__': proxy_server_conf_dir,
'__name__': 'filter:kmip_keymaster',
'keymaster_config_path': km_config_file}
secret = create_secret('AES', 256, b'x' * 32)
secrets = {'789': create_secret('AES', 256, b'x' * 32)}
calls = []
klass = 'swift.common.middleware.crypto.kmip_keymaster.ProxyKmipClient'
with mock.patch(klass, create_mock_client(secret, calls)):
with mock.patch(klass, create_mock_client(secrets, calls)):
km = KmipKeyMaster(None, conf)
self.assertEqual(secret.value, km.root_secret)
self.assertEqual({None: b'x' * 32}, km._root_secrets)
self.assertEqual(None, km.active_secret_id)
self.assertEqual(km_config_file, km.keymaster_config_path)
self.assertEqual({'config_file': km_config_file,
'config': 'kmip_keymaster'},
calls[0]['kwargs'])
self.assertEqual('789', calls[0]['client'].uid)
self.assertEqual(calls, [
('__init__', {'config_file': km_config_file,
'config': 'kmip_keymaster'}),
('get', '789')])
def test_bad_key_length(self):
conf = {'__file__': '/etc/swift/proxy-server.conf',
'__name__': 'filter:kmip_keymaster',
'key_id': '1234'}
secret = create_secret('AES', 128, b'x' * 16)
secrets = {'1234': create_secret('AES', 128, b'x' * 16)}
calls = []
klass = 'swift.common.middleware.crypto.kmip_keymaster.ProxyKmipClient'
with mock.patch(klass, create_mock_client(secret, calls)):
with mock.patch(klass, create_mock_client(secrets, calls)):
with self.assertRaises(ValueError) as cm:
KmipKeyMaster(None, conf)
self.assertIn('Expected an AES-256 key', str(cm.exception))
self.assertEqual({'config_file': '/etc/swift/proxy-server.conf',
'config': 'filter:kmip_keymaster'},
calls[0]['kwargs'])
self.assertEqual('1234', calls[0]['client'].uid)
self.assertIn('Expected key 1234 to be an AES-256 key',
str(cm.exception))
self.assertEqual(calls, [
('__init__', {'config_file': '/etc/swift/proxy-server.conf',
'config': 'filter:kmip_keymaster'}),
('get', '1234')])
def test_bad_key_algorithm(self):
conf = {'__file__': '/etc/swift/proxy-server.conf',
'__name__': 'filter:kmip_keymaster',
'key_id': '1234'}
secret = create_secret('notAES', 256, b'x' * 32)
secrets = {'1234': create_secret('notAES', 256, b'x' * 32)}
calls = []
klass = 'swift.common.middleware.crypto.kmip_keymaster.ProxyKmipClient'
with mock.patch(klass, create_mock_client(secret, calls)):
with mock.patch(klass, create_mock_client(secrets, calls)):
with self.assertRaises(ValueError) as cm:
KmipKeyMaster(None, conf)
self.assertIn('Expected an AES-256 key', str(cm.exception))
self.assertEqual({'config_file': '/etc/swift/proxy-server.conf',
'config': 'filter:kmip_keymaster'},
calls[0]['kwargs'])
self.assertEqual('1234', calls[0]['client'].uid)
self.assertIn('Expected key 1234 to be an AES-256 key',
str(cm.exception))
self.assertEqual(calls, [
('__init__', {'config_file': '/etc/swift/proxy-server.conf',
'config': 'filter:kmip_keymaster'}),
('get', '1234')])
def test_missing_key_id(self):
conf = {'__file__': '/etc/swift/proxy-server.conf',