Merge "Add keymaster to fetch root secret from KMIP service"
This commit is contained in:
commit
00373dad61
@ -101,7 +101,8 @@ Alternatives to specifying the encryption root secret directly in the
|
||||
`proxy-server.conf` file are storing it in a separate file, or storing it in
|
||||
an :ref:`external key management system
|
||||
<encryption_root_secret_in_external_kms>` such as `Barbican
|
||||
<https://docs.openstack.org/barbican>`_.
|
||||
<https://docs.openstack.org/barbican>`_ or a
|
||||
`KMIP <https://www.oasis-open.org/committees/kmip/>`_ service.
|
||||
|
||||
.. note::
|
||||
|
||||
@ -184,14 +185,22 @@ re-encrypted when copied.
|
||||
Encryption Root Secret in External Key Management System
|
||||
--------------------------------------------------------
|
||||
|
||||
The benefits of using
|
||||
a dedicated system for storing the encryption root secret include the
|
||||
auditing and access control infrastructure that are already in place in such a
|
||||
system, and the fact that an encryption root secret stored in a key management
|
||||
system (KMS) may be backed by a hardware security module (HSM) for additional
|
||||
security. Another significant benefit of storing the root encryption secret in
|
||||
an external KMS is that it is in this case never stored on a disk in the Swift
|
||||
cluster.
|
||||
The benefits of using a dedicated system for storing the encryption root secret
|
||||
include the auditing and access control infrastructure that are already in
|
||||
place in such a system, and the fact that an encryption root secret stored in a
|
||||
key management system (KMS) may be backed by a hardware security module (HSM)
|
||||
for additional security. Another significant benefit of storing the root
|
||||
encryption secret in an external KMS is that it is in this case never stored on
|
||||
a disk in the Swift cluster.
|
||||
|
||||
Swift supports fetching encryption root secrets from a `Barbican
|
||||
<https://docs.openstack.org/barbican>`_ service or a KMIP_ service using the
|
||||
``kms_keymaster`` or ``kmip_keymaster`` middleware respectively.
|
||||
|
||||
.. _KMIP: https://www.oasis-open.org/committees/kmip/
|
||||
|
||||
Encryption Root Secret in a Barbican KMS
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Make sure the required dependencies are installed for retrieving an encryption
|
||||
root secret from an external KMS. This can be done when installing Swift (add
|
||||
@ -308,6 +317,72 @@ For further details on the configuration options, see the
|
||||
`[filter:kms_keymaster]` section in the `proxy-server.conf-sample` file, and
|
||||
the `keymaster.conf-sample` file.
|
||||
|
||||
|
||||
Encryption Root Secret in a KMIP service
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
This middleware enables Swift to fetch a root secret from a KMIP_ service. The
|
||||
root secret is expected to have been previously created in the KMIP_ service
|
||||
and is referenced by its unique identifier. The secret should be an AES-256
|
||||
symmetric key.
|
||||
|
||||
To use this middleware Swift must be installed with the extra required
|
||||
dependencies::
|
||||
|
||||
sudo pip install .[kmip_keymaster]
|
||||
|
||||
Add the ``-e`` flag to install as a development version.
|
||||
|
||||
Edit the swift `proxy-server.conf` file to insert the middleware in the wsgi
|
||||
pipeline, replacing any other keymaster middleware::
|
||||
|
||||
[pipeline:main]
|
||||
pipeline = catch_errors gatekeeper healthcheck proxy-logging \
|
||||
<other middleware> kmip_keymaster encryption proxy-logging proxy-server
|
||||
|
||||
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>
|
||||
host = <KMIP server host>
|
||||
port = <KMIP server port>
|
||||
certfile = /path/to/client/cert.pem
|
||||
keyfile = /path/to/client/key.pem
|
||||
ca_certs = /path/to/server/cert.pem
|
||||
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>`_.
|
||||
|
||||
The value of the ``key_id`` option should be the unique identifier for a secret
|
||||
that will be retrieved from the KMIP_ service.
|
||||
|
||||
The keymaster configuration can alternatively be defined in a separate config
|
||||
file by using the ``keymaster_config_path`` option::
|
||||
|
||||
[filter:kmip_keymaster]
|
||||
use = egg:swift#kmip_keymaster
|
||||
keymaster_config_path = /etc/swift/kmip_keymaster.conf
|
||||
|
||||
In this case, the ``filter:kmip_keymaster`` section should contain no other
|
||||
options than ``use`` and ``keymaster_config_path``. All other options should be
|
||||
defined in the separate config file in a section named ``kmip_keymaster``. For
|
||||
example::
|
||||
|
||||
[kmip_keymaster]
|
||||
key_id = 1234567890
|
||||
host = 127.0.0.1
|
||||
port = 5696
|
||||
certfile = /etc/swift/kmip_client.crt
|
||||
keyfile = /etc/swift/kmip_client.key
|
||||
ca_certs = /etc/swift/kmip_server.crt
|
||||
username = swift
|
||||
password = swift_password
|
||||
|
||||
|
||||
Upgrade Considerations
|
||||
----------------------
|
||||
|
||||
|
@ -74,3 +74,23 @@
|
||||
# reauthenticate = changeme
|
||||
# domain_id = changeme
|
||||
# domain_name = changeme
|
||||
|
||||
[kmip_keymaster]
|
||||
# The kmip_keymaster section is used to configure a keymaster that fetches an
|
||||
# encryption root secret from a KMIP service.
|
||||
|
||||
# The value of the ``key_id`` option should be the unique identifier for a
|
||||
# secret that will be retrieved from the KMIP service. The secret should be an
|
||||
# AES-256 symmetric key.
|
||||
# key_id = <unique id of secret to be fetched from the KMIP service>
|
||||
|
||||
# The remaining options are used to configure a PyKMIP client and are shown
|
||||
# below for information. The authoritative definition of these options can be
|
||||
# found at: https://pykmip.readthedocs.io/en/latest/client.html.
|
||||
# host = <KMIP server host>
|
||||
# port = <KMIP server port>
|
||||
# certfile = /path/to/client/cert.pem
|
||||
# keyfile = /path/to/client/key.pem
|
||||
# ca_certs = /path/to/server/cert.pem
|
||||
# username = <KMIP username>
|
||||
# password = <KMIP password>
|
||||
|
@ -1074,6 +1074,25 @@ use = egg:swift#kms_keymaster
|
||||
# options.
|
||||
# keymaster_config_path =
|
||||
|
||||
# kmip_keymaster middleware may be used to fetch an encryption root secret from
|
||||
# a KMIP service. It should replace, in the same position, any other keymaster
|
||||
# middleware in the proxy-server pipeline, so that the middleware order is as
|
||||
# shown in this example:
|
||||
# <other middleware> kmip_keymaster encryption proxy-logging proxy-server
|
||||
[filter:kmip_keymaster]
|
||||
use = egg:swift#kmip_keymaster
|
||||
|
||||
# Sets the path from which the keymaster config options should be read. This
|
||||
# allows multiple processes which need to be encryption-aware (for example,
|
||||
# proxy-server and container-sync) to share the same config file, ensuring
|
||||
# that the encryption keys used are the same. As an added benefit the
|
||||
# keymaster configuration file can have different permissions than the
|
||||
# `proxy-server.conf` file. The format expected is similar
|
||||
# to other config files, with a single [kmip_keymaster] section. See the
|
||||
# keymaster.conf-sample file for details on the kmip_keymaster configuration
|
||||
# options.
|
||||
# keymaster_config_path =
|
||||
|
||||
[filter:encryption]
|
||||
use = egg:swift#encryption
|
||||
|
||||
|
@ -69,6 +69,9 @@ kms_keymaster =
|
||||
oslo.config>=4.0.0,!=4.3.0,!=4.4.0 # Apache-2.0
|
||||
castellan>=0.13.0 # Apache-2.0
|
||||
|
||||
kmip_keymaster =
|
||||
pykmip>=0.7.0 # Apache-2.0
|
||||
|
||||
keystone =
|
||||
keystonemiddleware>=4.17.0
|
||||
|
||||
@ -114,6 +117,7 @@ paste.filter_factory =
|
||||
keymaster = swift.common.middleware.crypto.keymaster:filter_factory
|
||||
encryption = swift.common.middleware.crypto:filter_factory
|
||||
kms_keymaster = swift.common.middleware.crypto.kms_keymaster:filter_factory
|
||||
kmip_keymaster = swift.common.middleware.crypto.kmip_keymaster:filter_factory
|
||||
listing_formats = swift.common.middleware.listing_formats:filter_factory
|
||||
symlink = swift.common.middleware.symlink:filter_factory
|
||||
s3api = swift.common.middleware.s3api.s3api:filter_factory
|
||||
|
131
swift/common/middleware/crypto/kmip_keymaster.py
Normal file
131
swift/common/middleware/crypto/kmip_keymaster.py
Normal file
@ -0,0 +1,131 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2018 OpenStack Foundation
|
||||
#
|
||||
# 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.
|
||||
import logging
|
||||
import os
|
||||
|
||||
from swift.common.middleware.crypto import keymaster
|
||||
from swift.common.utils import readconf, get_logger
|
||||
|
||||
from kmip.pie.client import ProxyKmipClient
|
||||
|
||||
"""
|
||||
This middleware enables Swift to fetch a root secret from a KMIP service.
|
||||
The root secret is expected to have been previously created in the KMIP service
|
||||
and is referenced by its unique identifier. The secret should be an AES-256
|
||||
symmetric key.
|
||||
|
||||
To use this middleware, edit the swift proxy-server.conf to insert the
|
||||
middleware in the wsgi pipeline, replacing any other keymaster middleware::
|
||||
|
||||
[pipeline:main]
|
||||
pipeline = catch_errors gatekeeper healthcheck proxy-logging \
|
||||
<other middleware> kmip_keymaster encryption proxy-logging proxy-server
|
||||
|
||||
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>
|
||||
host = <KMIP server host>
|
||||
port = <KMIP server port>
|
||||
certfile = /path/to/client/cert.pem
|
||||
keyfile = /path/to/client/key.pem
|
||||
ca_certs = /path/to/server/cert.pem
|
||||
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`_
|
||||
|
||||
The value of the ``key_id`` option should be the unique identifier for a secret
|
||||
that will be retrieved from the KMIP service.
|
||||
|
||||
The keymaster configuration can alternatively be defined in a separate config
|
||||
file by using the ``keymaster_config_path`` option::
|
||||
|
||||
[filter:kmip_keymaster]
|
||||
use = egg:swift#kmip_keymaster
|
||||
keymaster_config_path=/etc/swift/kmip_keymaster.conf
|
||||
|
||||
In this case, the ``filter:kmip_keymaster`` section should contain no other
|
||||
options than ``use`` and ``keymaster_config_path``. All other options should be
|
||||
defined in the separate config file in a section named ``kmip_keymaster``. For
|
||||
example::
|
||||
|
||||
[kmip_keymaster]
|
||||
key_id = 1234567890
|
||||
host = 127.0.0.1
|
||||
port = 5696
|
||||
certfile = /etc/swift/kmip_client.crt
|
||||
keyfile = /etc/swift/kmip_client.key
|
||||
ca_certs = /etc/swift/kmip_server.crt
|
||||
username = swift
|
||||
password = swift_password
|
||||
"""
|
||||
|
||||
|
||||
class KmipKeyMaster(keymaster.KeyMaster):
|
||||
def _get_root_secret(self, conf):
|
||||
self.logger = get_logger(conf, log_route='kmip_keymaster')
|
||||
if self.keymaster_config_path:
|
||||
keymaster_opts = ['host', 'port', 'certfile', 'keyfile',
|
||||
'ca_certs', 'username', 'password', 'key_id']
|
||||
section = 'kmip_keymaster'
|
||||
if any(opt in conf for opt in keymaster_opts):
|
||||
raise ValueError('keymaster_config_path is set, but there '
|
||||
'are other config options specified: %s' %
|
||||
", ".join(list(
|
||||
set(keymaster_opts).intersection(conf))))
|
||||
conf = readconf(self.keymaster_config_path, section)
|
||||
else:
|
||||
section = conf['__name__']
|
||||
|
||||
if os.path.isdir(conf['__file__']):
|
||||
raise ValueError(
|
||||
'KmipKeyMaster config cannot be read from conf dir %s. Use '
|
||||
'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)
|
||||
|
||||
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
|
||||
|
||||
|
||||
def filter_factory(global_conf, **local_conf):
|
||||
conf = global_conf.copy()
|
||||
conf.update(local_conf)
|
||||
|
||||
def keymaster_filter(app):
|
||||
return KmipKeyMaster(app, conf)
|
||||
|
||||
return keymaster_filter
|
191
test/unit/common/middleware/crypto/test_kmip_keymaster.py
Normal file
191
test/unit/common/middleware/crypto/test_kmip_keymaster.py
Normal file
@ -0,0 +1,191 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2018 OpenStack Foundation
|
||||
#
|
||||
# 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.
|
||||
|
||||
import mock
|
||||
import os
|
||||
import unittest
|
||||
from tempfile import mkdtemp
|
||||
from textwrap import dedent
|
||||
from shutil import rmtree
|
||||
import sys
|
||||
sys.modules['kmip'] = mock.Mock()
|
||||
sys.modules['kmip.pie'] = mock.Mock()
|
||||
sys.modules['kmip.pie.client'] = mock.Mock()
|
||||
|
||||
from swift.common.middleware.crypto.kmip_keymaster import KmipKeyMaster
|
||||
|
||||
|
||||
class MockProxyKmipClient(object):
|
||||
def __init__(self, secret):
|
||||
self.secret = secret
|
||||
self.uid = None
|
||||
|
||||
def get(self, uid):
|
||||
self.uid = uid
|
||||
return self.secret
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
pass
|
||||
|
||||
|
||||
def create_secret(algorithm_name, length, value):
|
||||
algorithm = mock.MagicMock()
|
||||
algorithm.name = algorithm_name
|
||||
secret = mock.MagicMock(cryptographic_algorithm=algorithm,
|
||||
cryptographic_length=length,
|
||||
value=value)
|
||||
return secret
|
||||
|
||||
|
||||
def create_mock_client(secret, calls):
|
||||
def mock_client(*args, **kwargs):
|
||||
client = MockProxyKmipClient(secret)
|
||||
calls.append({'args': args, 'kwargs': kwargs, 'client': client})
|
||||
return client
|
||||
return mock_client
|
||||
|
||||
|
||||
class TestKmipKeymaster(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.tempdir = mkdtemp()
|
||||
|
||||
def tearDown(self):
|
||||
rmtree(self.tempdir)
|
||||
|
||||
def test_config_in_filter_section(self):
|
||||
conf = {'__file__': '/etc/swift/proxy-server.conf',
|
||||
'__name__': 'filter:kmip_keymaster',
|
||||
'key_id': '1234'}
|
||||
secret = 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)):
|
||||
km = KmipKeyMaster(None, conf)
|
||||
|
||||
self.assertEqual(secret.value, km.root_secret)
|
||||
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)
|
||||
|
||||
def test_config_in_separate_file(self):
|
||||
km_conf = """
|
||||
[kmip_keymaster]
|
||||
key_id = 4321
|
||||
"""
|
||||
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}
|
||||
secret = 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)):
|
||||
km = KmipKeyMaster(None, conf)
|
||||
self.assertEqual(secret.value, km.root_secret)
|
||||
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)
|
||||
|
||||
def test_proxy_server_conf_dir(self):
|
||||
proxy_server_conf_dir = os.path.join(self.tempdir, 'proxy_server.d')
|
||||
os.mkdir(proxy_server_conf_dir)
|
||||
|
||||
# KmipClient can't read conf from a dir, so check that is caught early
|
||||
conf = {'__file__': proxy_server_conf_dir,
|
||||
'__name__': 'filter:kmip_keymaster',
|
||||
'key_id': '789'}
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
KmipKeyMaster(None, conf)
|
||||
self.assertIn('config cannot be read from conf dir', str(cm.exception))
|
||||
|
||||
# ...but a conf file in a conf dir could point back to itself for the
|
||||
# KmipClient config
|
||||
km_config_file = os.path.join(proxy_server_conf_dir, '40.conf')
|
||||
km_conf = """
|
||||
[filter:kmip_keymaster]
|
||||
keymaster_config_file = %s
|
||||
|
||||
[kmip_keymaster]
|
||||
key_id = 789
|
||||
""" % km_config_file
|
||||
|
||||
with open(km_config_file, 'wb') as fd:
|
||||
fd.write(dedent(km_conf))
|
||||
|
||||
conf = {'__file__': proxy_server_conf_dir,
|
||||
'__name__': 'filter:kmip_keymaster',
|
||||
'keymaster_config_path': km_config_file}
|
||||
secret = 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)):
|
||||
km = KmipKeyMaster(None, conf)
|
||||
self.assertEqual(secret.value, km.root_secret)
|
||||
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)
|
||||
|
||||
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)
|
||||
calls = []
|
||||
klass = 'swift.common.middleware.crypto.kmip_keymaster.ProxyKmipClient'
|
||||
with mock.patch(klass, create_mock_client(secret, 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)
|
||||
|
||||
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)
|
||||
calls = []
|
||||
klass = 'swift.common.middleware.crypto.kmip_keymaster.ProxyKmipClient'
|
||||
with mock.patch(klass, create_mock_client(secret, 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)
|
||||
|
||||
def test_missing_key_id(self):
|
||||
conf = {'__file__': '/etc/swift/proxy-server.conf',
|
||||
'__name__': 'filter:kmip_keymaster'}
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
KmipKeyMaster(None, conf)
|
||||
self.assertIn('key_id option is required', str(cm.exception))
|
Loading…
Reference in New Issue
Block a user