Codebase list python-castellan / a972da3
Vault based key manager * Uses https://www.vaultproject.io/ to store/fetch secrets * All we need is the URL and a Token to talk to the vault server * tox target "functional-vault" sets up a server in development mode and runs functional tests * Supports both http:// and https:// url(s) * the https support was tested by setting up a vault server by hand (https://gist.github.com/dims/47674cf2c3b0a953df69246c2ea1ff78) * create_key_pair is the only API that is not implemented Change-Id: I6436e5841c8e77a7262b4d5aa39201b40a985255 Davanum Srinivas 6 years ago
9 changed file(s) with 494 addition(s) and 4 deletion(s). Raw diff Collapse all Expand all
2424 default='barbican',
2525 deprecated_name='api_class',
2626 deprecated_group='key_manager',
27 help='Specify the key manager implementation. Default is '
28 '"barbican".Will support the values earlier set using '
27 help='Specify the key manager implementation. Options are '
28 '"barbican" and "vault". Default is "barbican". Will '
29 'support the values earlier set using '
2930 '[key_manager]/api_class for some time.'),
3031 ]
3132
0 # Licensed under the Apache License, Version 2.0 (the "License"); you may
1 # not use this file except in compliance with the License. You may obtain
2 # a copy of the License at
3 #
4 # http://www.apache.org/licenses/LICENSE-2.0
5 #
6 # Unless required by applicable law or agreed to in writing, software
7 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
8 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
9 # License for the specific language governing permissions and limitations
10 # under the License.
11
12 """
13 Key manager implementation for Vault
14 """
15
16 import binascii
17 import os
18 import time
19 import uuid
20
21 from keystoneauth1 import loading
22 from oslo_config import cfg
23 from oslo_log import log as logging
24 import requests
25 import six
26
27 from castellan.common import exception
28 from castellan.common.objects import opaque_data as op_data
29 from castellan.common.objects import passphrase
30 from castellan.common.objects import private_key as pri_key
31 from castellan.common.objects import public_key as pub_key
32 from castellan.common.objects import symmetric_key as sym_key
33 from castellan.common.objects import x_509
34 from castellan.i18n import _
35 from castellan.key_manager import key_manager
36
37 DEFAULT_VAULT_URL = "http://127.0.0.1:8200"
38
39 vault_opts = [
40 cfg.StrOpt('root_token_id',
41 help='root token for vault'),
42 cfg.StrOpt('vault_url',
43 default=DEFAULT_VAULT_URL,
44 help='Use this endpoint to connect to Vault, for example: '
45 '"%s"' % DEFAULT_VAULT_URL),
46 cfg.StrOpt('ssl_ca_crt_file',
47 help='Absolute path to ca cert file'),
48 cfg.BoolOpt('use_ssl',
49 default=False,
50 help=_('SSL Enabled/Disabled')),
51 ]
52
53 VAULT_OPT_GROUP = 'vault'
54
55 _EXCEPTIONS_BY_CODE = [
56 requests.codes['internal_server_error'],
57 requests.codes['service_unavailable'],
58 requests.codes['request_timeout'],
59 requests.codes['gateway_timeout'],
60 requests.codes['precondition_failed'],
61 ]
62
63 LOG = logging.getLogger(__name__)
64
65
66 class VaultKeyManager(key_manager.KeyManager):
67 """Key Manager Interface that wraps the Vault REST API."""
68
69 _secret_type_dict = {
70 op_data.OpaqueData: 'opaque',
71 passphrase.Passphrase: 'passphrase',
72 pri_key.PrivateKey: 'private',
73 pub_key.PublicKey: 'public',
74 sym_key.SymmetricKey: 'symmetric',
75 x_509.X509: 'certificate'}
76
77 def __init__(self, configuration):
78 self._conf = configuration
79 self._conf.register_opts(vault_opts, group=VAULT_OPT_GROUP)
80 loading.register_session_conf_options(self._conf, VAULT_OPT_GROUP)
81 self._session = requests.Session()
82 self._root_token_id = self._conf.vault.root_token_id
83 self._vault_url = self._conf.vault.vault_url
84 if self._vault_url.startswith("https://"):
85 self._verify_server = self._conf.vault.ssl_ca_crt_file or True
86 else:
87 self._verify_server = False
88
89 def _get_url(self):
90 if not self._vault_url.endswith('/'):
91 self._vault_url += '/'
92 return self._vault_url
93
94 def create_key_pair(self, context, algorithm, length,
95 expiration=None, name=None):
96 """Creates an asymmetric key pair."""
97 raise NotImplementedError(
98 "VaultKeyManager does not support asymmetric keys")
99
100 def _store_key_value(self, key_id, value):
101
102 type_value = self._secret_type_dict.get(type(value))
103 if type_value is None:
104 raise exception.KeyManagerError(
105 "Unknown type for value : %r" % value)
106
107 headers = {'X-Vault-Token': self._root_token_id}
108 try:
109 resource_url = self._get_url() + 'v1/secret/' + key_id
110 record = {
111 'type': type_value,
112 'value': binascii.hexlify(value.get_encoded()).decode('utf-8'),
113 'algorithm': (value.algorithm if hasattr(value, 'algorithm')
114 else None),
115 'bit_length': (value.bit_length if hasattr(value, 'bit_length')
116 else None),
117 'name': value.name,
118 'created': value.created
119 }
120 resp = self._session.post(resource_url,
121 verify=self._verify_server,
122 json=record,
123 headers=headers)
124 except requests.exceptions.Timeout as ex:
125 raise exception.KeyManagerError(six.text_type(ex))
126 except requests.exceptions.ConnectionError as ex:
127 raise exception.KeyManagerError(six.text_type(ex))
128 except Exception as ex:
129 raise exception.KeyManagerError(six.text_type(ex))
130
131 if resp.status_code in _EXCEPTIONS_BY_CODE:
132 raise exception.KeyManagerError(resp.reason)
133 if resp.status_code == requests.codes['forbidden']:
134 raise exception.Forbidden()
135
136 return key_id
137
138 def create_key(self, context, algorithm, length, name=None, **kwargs):
139 """Creates a symmetric key."""
140
141 # Confirm context is provided, if not raise forbidden
142 if not context:
143 msg = _("User is not authorized to use key manager.")
144 raise exception.Forbidden(msg)
145
146 key_id = uuid.uuid4().hex
147 key_value = os.urandom(length or 32)
148 key = sym_key.SymmetricKey(algorithm,
149 length or 32,
150 key_value,
151 key_id,
152 name or int(time.time()))
153 return self._store_key_value(key_id, key)
154
155 def store(self, context, key_value, **kwargs):
156 """Stores (i.e., registers) a key with the key manager."""
157
158 # Confirm context is provided, if not raise forbidden
159 if not context:
160 msg = _("User is not authorized to use key manager.")
161 raise exception.Forbidden(msg)
162
163 key_id = uuid.uuid4().hex
164 return self._store_key_value(key_id, key_value)
165
166 def get(self, context, key_id, metadata_only=False):
167 """Retrieves the key identified by the specified id."""
168
169 # Confirm context is provided, if not raise forbidden
170 if not context:
171 msg = _("User is not authorized to use key manager.")
172 raise exception.Forbidden(msg)
173
174 if not key_id:
175 raise exception.KeyManagerError('key identifier not provided')
176
177 headers = {'X-Vault-Token': self._root_token_id}
178 try:
179 resource_url = self._get_url() + 'v1/secret/' + key_id
180 resp = self._session.get(resource_url,
181 verify=self._verify_server,
182 headers=headers)
183 except requests.exceptions.Timeout as ex:
184 raise exception.KeyManagerError(six.text_type(ex))
185 except requests.exceptions.ConnectionError as ex:
186 raise exception.KeyManagerError(six.text_type(ex))
187 except Exception as ex:
188 raise exception.KeyManagerError(six.text_type(ex))
189
190 if resp.status_code in _EXCEPTIONS_BY_CODE:
191 raise exception.KeyManagerError(resp.reason)
192 if resp.status_code == requests.codes['forbidden']:
193 raise exception.Forbidden()
194 if resp.status_code == requests.codes['not_found']:
195 raise exception.ManagedObjectNotFoundError(uuid=key_id)
196
197 record = resp.json()['data']
198 key = None if metadata_only else binascii.unhexlify(record['value'])
199
200 clazz = None
201 for type_clazz, type_name in self._secret_type_dict.items():
202 if type_name == record['type']:
203 clazz = type_clazz
204
205 if clazz is None:
206 raise exception.KeyManagerError(
207 "Unknown type : %r" % record['type'])
208
209 if hasattr(clazz, 'algorithm') and hasattr(clazz, 'bit_length'):
210 return clazz(record['algorithm'],
211 record['bit_length'],
212 key,
213 record['name'],
214 record['created'],
215 key_id)
216 else:
217 return clazz(key,
218 record['name'],
219 record['created'],
220 key_id)
221
222 def delete(self, context, key_id):
223 """Represents deleting the key."""
224
225 # Confirm context is provided, if not raise forbidden
226 if not context:
227 msg = _("User is not authorized to use key manager.")
228 raise exception.Forbidden(msg)
229
230 if not key_id:
231 raise exception.KeyManagerError('key identifier not provided')
232
233 headers = {'X-Vault-Token': self._root_token_id}
234 try:
235 resource_url = self._get_url() + 'v1/secret/' + key_id
236 resp = self._session.delete(resource_url,
237 verify=self._verify_server,
238 headers=headers)
239 except requests.exceptions.Timeout as ex:
240 raise exception.KeyManagerError(six.text_type(ex))
241 except requests.exceptions.ConnectionError as ex:
242 raise exception.KeyManagerError(six.text_type(ex))
243 except Exception as ex:
244 raise exception.KeyManagerError(six.text_type(ex))
245
246 if resp.status_code in _EXCEPTIONS_BY_CODE:
247 raise exception.KeyManagerError(resp.reason)
248 if resp.status_code == requests.codes['forbidden']:
249 raise exception.Forbidden()
250 if resp.status_code == requests.codes['not_found']:
251 raise exception.ManagedObjectNotFoundError(uuid=key_id)
252
253 def list(self, context, object_type=None, metadata_only=False):
254 """Lists the managed objects given the criteria."""
255
256 # Confirm context is provided, if not raise forbidden
257 if not context:
258 msg = _("User is not authorized to use key manager.")
259 raise exception.Forbidden(msg)
260
261 if object_type and object_type not in self._secret_type_dict:
262 msg = _("Invalid secret type: %s") % object_type
263 raise exception.KeyManagerError(reason=msg)
264
265 headers = {'X-Vault-Token': self._root_token_id}
266 try:
267 resource_url = self._get_url() + 'v1/secret/?list=true'
268 resp = self._session.get(resource_url,
269 verify=self._verify_server,
270 headers=headers)
271 keys = resp.json()['data']['keys']
272 except requests.exceptions.Timeout as ex:
273 raise exception.KeyManagerError(six.text_type(ex))
274 except requests.exceptions.ConnectionError as ex:
275 raise exception.KeyManagerError(six.text_type(ex))
276 except Exception as ex:
277 raise exception.KeyManagerError(six.text_type(ex))
278
279 if resp.status_code in _EXCEPTIONS_BY_CODE:
280 raise exception.KeyManagerError(resp.reason)
281 if resp.status_code == requests.codes['forbidden']:
282 raise exception.Forbidden()
283 if resp.status_code == requests.codes['not_found']:
284 keys = []
285
286 objects = []
287 for obj_id in keys:
288 try:
289 obj = self.get(context, obj_id, metadata_only=metadata_only)
290 if object_type is None or isinstance(obj, object_type):
291 objects.append(obj)
292 except exception.ManagedObjectNotFoundError as e:
293 LOG.warning(_("Error occurred while retrieving object "
294 "metadata, not adding it to the list: %s"), e)
295 pass
296 return objects
1919 from castellan.key_manager import barbican_key_manager as bkm
2020 except ImportError:
2121 bkm = None
22
23 try:
24 from castellan.key_manager import vault_key_manager as vkm
25 except ImportError:
26 vkm = None
27
2228 from castellan.common import utils
2329
2430 _DEFAULT_LOG_LEVELS = ['castellan=WARN']
3238 def set_defaults(conf, backend=None, barbican_endpoint=None,
3339 barbican_api_version=None, auth_endpoint=None,
3440 retry_delay=None, number_of_retries=None, verify_ssl=None,
35 api_class=None):
41 api_class=None, vault_root_token_id=None, vault_url=None,
42 vault_ssl_ca_crt_file=None, vault_use_ssl=None):
3643 """Set defaults for configuration values.
3744
3845 Overrides the default options values.
4451 :param retry_delay: Use this attribute to set retry delay.
4552 :param number_of_retries: Use this attribute to set number of retries.
4653 :param verify_ssl: Use this to specify if ssl should be verified.
54 :param vault_root_token_id: Use this for the root token id for vault.
55 :param vault_url: Use this for the url for vault.
56 :param vault_use_ssl: Use this to force vault driver to use ssl.
57 :param vault_ssl_ca_crt_file: Use this for the CA file for vault.
4758 """
4859 conf.register_opts(km.key_manager_opts, group='key_manager')
4960 if bkm:
5061 conf.register_opts(bkm.barbican_opts, group=bkm.BARBICAN_OPT_GROUP)
62 if vkm:
63 conf.register_opts(vkm.vault_opts, group=vkm.VAULT_OPT_GROUP)
5164
5265 # Use the new backend option if set or fall back to the older api_class
5366 default_backend = backend or api_class
7386 if verify_ssl is not None:
7487 conf.set_default('verify_ssl', verify_ssl,
7588 group=bkm.BARBICAN_OPT_GROUP)
89
90 if vkm is not None:
91 if vault_root_token_id is not None:
92 conf.set_default('root_token_id', vault_root_token_id,
93 group=vkm.VAULT_OPT_GROUP)
94 if vault_url is not None:
95 conf.set_default('vault_url', vault_url,
96 group=vkm.VAULT_OPT_GROUP)
97 if vault_ssl_ca_crt_file is not None:
98 conf.set_default('ssl_ca_crt_file', vault_ssl_ca_crt_file,
99 group=vkm.VAULT_OPT_GROUP)
100 if vault_use_ssl is not None:
101 conf.set_default('use_ssl', vault_use_ssl,
102 group=vkm.VAULT_OPT_GROUP)
76103
77104
78105 def enable_logging(conf=None, app_name='castellan'):
108135
109136 if bkm is not None:
110137 opts.append((bkm.BARBICAN_OPT_GROUP, bkm.barbican_opts))
138 if vkm is not None:
139 opts.append((vkm.VAULT_OPT_GROUP, vkm.vault_opts))
111140 return opts
2525 from oslo_context import context
2626 from oslo_utils import uuidutils
2727 from oslotest import base
28 from testtools import testcase
2829
2930 from castellan.common.credentials import keystone_password
3031 from castellan.common.credentials import keystone_token
4950
5051 def setUp(self):
5152 super(BarbicanKeyManagerTestCase, self).setUp()
52 self.ctxt = self.get_context()
53 try:
54 self.ctxt = self.get_context()
55 self.key_mgr._get_barbican_client(self.ctxt)
56 except Exception as e:
57 # When we run functional-vault target, This test class needs
58 # to be skipped as barbican is not running
59 raise testcase.TestSkipped(str(e))
5360
5461 def tearDown(self):
5562 super(BarbicanKeyManagerTestCase, self).tearDown()
0 # Licensed under the Apache License, Version 2.0 (the "License"); you may
1 # not use this file except in compliance with the License. You may obtain
2 # a copy of the License at
3 #
4 # http://www.apache.org/licenses/LICENSE-2.0
5 #
6 # Unless required by applicable law or agreed to in writing, software
7 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
8 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
9 # License for the specific language governing permissions and limitations
10 # under the License.
11
12 """
13 Functional test cases for the Vault key manager.
14
15 Note: This requires local running instance of Vault.
16 """
17 import abc
18 import os
19
20 from oslo_config import cfg
21 from oslo_context import context
22 from oslo_utils import uuidutils
23 from oslotest import base
24 from testtools import testcase
25
26 from castellan.common import exception
27 from castellan.key_manager import vault_key_manager
28 from castellan.tests.functional import config
29 from castellan.tests.functional.key_manager import test_key_manager
30
31 CONF = config.get_config()
32
33
34 class VaultKeyManagerTestCase(test_key_manager.KeyManagerTestCase):
35 def _create_key_manager(self):
36 key_mgr = vault_key_manager.VaultKeyManager(cfg.CONF)
37
38 if ('VAULT_TEST_URL' not in os.environ or
39 'VAULT_TEST_ROOT_TOKEN' not in os.environ):
40 raise testcase.TestSkipped('Missing Vault setup information')
41
42 key_mgr._root_token_id = os.environ['VAULT_TEST_ROOT_TOKEN']
43 key_mgr._vault_url = os.environ['VAULT_TEST_URL']
44 return key_mgr
45
46 @abc.abstractmethod
47 def get_context(self):
48 """Retrieves Context for Authentication"""
49 return
50
51 def setUp(self):
52 super(VaultKeyManagerTestCase, self).setUp()
53 self.ctxt = self.get_context()
54
55 def tearDown(self):
56 super(VaultKeyManagerTestCase, self).tearDown()
57
58 def test_create_key_pair(self):
59 self.assertRaises(NotImplementedError,
60 self.key_mgr.create_key_pair, None, None, None)
61
62 def test_create_null_context(self):
63 self.assertRaises(exception.Forbidden,
64 self.key_mgr.create_key, None, 'AES', 256)
65
66 def test_create_key_pair_null_context(self):
67 self.assertRaises(NotImplementedError,
68 self.key_mgr.create_key_pair, None, 'RSA', 2048)
69
70 def test_delete_null_context(self):
71 key_uuid = self._get_valid_object_uuid(
72 test_key_manager._get_test_symmetric_key())
73 self.addCleanup(self.key_mgr.delete, self.ctxt, key_uuid)
74 self.assertRaises(exception.Forbidden,
75 self.key_mgr.delete, None, key_uuid)
76
77 def test_delete_null_object(self):
78 self.assertRaises(exception.KeyManagerError,
79 self.key_mgr.delete, self.ctxt, None)
80
81 def test_get_null_context(self):
82 key_uuid = self._get_valid_object_uuid(
83 test_key_manager._get_test_symmetric_key())
84 self.addCleanup(self.key_mgr.delete, self.ctxt, key_uuid)
85 self.assertRaises(exception.Forbidden,
86 self.key_mgr.get, None, key_uuid)
87
88 def test_get_null_object(self):
89 self.assertRaises(exception.KeyManagerError,
90 self.key_mgr.get, self.ctxt, None)
91
92 def test_get_unknown_key(self):
93 bad_key_uuid = uuidutils.generate_uuid()
94 self.assertRaises(exception.ManagedObjectNotFoundError,
95 self.key_mgr.get, self.ctxt, bad_key_uuid)
96
97 def test_store_null_context(self):
98 key = test_key_manager._get_test_symmetric_key()
99
100 self.assertRaises(exception.Forbidden,
101 self.key_mgr.store, None, key)
102
103
104 class VaultKeyManagerOSLOContextTestCase(VaultKeyManagerTestCase,
105 base.BaseTestCase):
106 def get_context(self):
107 return context.get_admin_context()
0 ---
1 features:
2 - |
3 Added a new provider for Vault (https://www.vaultproject.io/)
2828
2929 castellan.drivers =
3030 barbican = castellan.key_manager.barbican_key_manager:BarbicanKeyManager
31 vault = castellan.key_manager.vault_key_manager:VaultKeyManager
3132
3233 [build_sphinx]
3334 source-dir = doc/source
0 #!/bin/bash
1 set -eux
2 if [ -z "$(which vault)" ]; then
3 VAULT_VERSION=0.7.3
4 SUFFIX=zip
5 case `uname -s` in
6 Darwin)
7 OS=darwin
8 ;;
9 Linux)
10 OS=linux
11 ;;
12 *)
13 echo "Unsupported OS"
14 exit 1
15 esac
16 case `uname -m` in
17 x86_64)
18 MACHINE=amd64
19 ;;
20 *)
21 echo "Unsupported machine"
22 exit 1
23 esac
24 TARBALL_NAME=vault_${VAULT_VERSION}_${OS}_${MACHINE}
25 test ! -d "$TARBALL_NAME" && mkdir ${TARBALL_NAME} && wget https://releases.hashicorp.com/vault/${VAULT_VERSION}/${TARBALL_NAME}.${SUFFIX} && unzip -d ${TARBALL_NAME} ${TARBALL_NAME}.${SUFFIX} && rm ${TARBALL_NAME}.${SUFFIX}
26 export VAULT_CONFIG_PATH=$(pwd)/$TARBALL_NAME/vault.json
27 export PATH=$PATH:$(pwd)/$TARBALL_NAME
28 fi
29
30 $*
5656 -r{toxinidir}/test-requirements.txt
5757 commands = python setup.py testr --slowest --testr-args='{posargs}'
5858
59 [testenv:functional-vault]
60 passenv = HOME
61 usedevelop = True
62 install_command = pip install -U {opts} {packages}
63 setenv =
64 VIRTUAL_ENV={envdir}
65 OS_TEST_PATH=./castellan/tests/functional
66 deps = -r{toxinidir}/requirements.txt
67 -r{toxinidir}/test-requirements.txt
68 commands =
69 {toxinidir}/tools/setup-vault-env.sh pifpaf -e VAULT_TEST run vault -- python setup.py testr --slowest --testr-args='{posargs}'
70
5971 [testenv:genconfig]
6072 commands =
6173 oslo-config-generator --config-file=etc/castellan/functional-config-generator.conf