diff --git a/castellan/key_manager/vault_key_manager.py b/castellan/key_manager/vault_key_manager.py index 40f28cd..b18dac9 100644 --- a/castellan/key_manager/vault_key_manager.py +++ b/castellan/key_manager/vault_key_manager.py @@ -29,6 +29,7 @@ from keystoneauth1 import loading from oslo_config import cfg from oslo_log import log as logging +from oslo_utils import timeutils import requests import six @@ -47,6 +48,10 @@ vault_opts = [ cfg.StrOpt('root_token_id', help='root token for vault'), + cfg.StrOpt('approle_role_id', + help='AppRole role_id for authentication with vault'), + cfg.StrOpt('approle_secret_id', + help='AppRole secret_id for authentication with vault'), cfg.StrOpt('vault_url', default=DEFAULT_VAULT_URL, help='Use this endpoint to connect to Vault, for example: ' @@ -88,6 +93,11 @@ loading.register_session_conf_options(self._conf, VAULT_OPT_GROUP) self._session = requests.Session() self._root_token_id = self._conf.vault.root_token_id + self._approle_role_id = self._conf.vault.approle_role_id + self._approle_secret_id = self._conf.vault.approle_secret_id + self._cached_approle_token_id = None + self._approle_token_ttl = None + self._approle_token_issue = None self._vault_url = self._conf.vault.vault_url if self._vault_url.startswith("https://"): self._verify_server = self._conf.vault.ssl_ca_crt_file or True @@ -124,9 +134,58 @@ key_id if key_id else '?list=true') + @property + def _approle_token_id(self): + if (all((self._approle_token_issue, self._approle_token_ttl)) and + timeutils.is_older_than(self._approle_token_issue, + self._approle_token_ttl)): + self._cached_approle_token_id = None + return self._cached_approle_token_id + + def _build_auth_headers(self): + if self._root_token_id: + return {'X-Vault-Token': self._root_token_id} + + if self._approle_token_id: + return {'X-Vault-Token': self._approle_token_id} + + if self._approle_role_id: + params = { + 'role_id': self._approle_role_id + } + if self._approle_secret_id: + params['secret_id'] = self._approle_secret_id + approle_login_url = '{}v1/auth/approle/login'.format( + self._get_url() + ) + token_issue_utc = timeutils.utcnow() + try: + resp = self._session.post(url=approle_login_url, + json=params, + verify=self._verify_server) + except requests.exceptions.Timeout as ex: + raise exception.KeyManagerError(six.text_type(ex)) + except requests.exceptions.ConnectionError as ex: + raise exception.KeyManagerError(six.text_type(ex)) + except Exception as ex: + raise exception.KeyManagerError(six.text_type(ex)) + + if resp.status_code in _EXCEPTIONS_BY_CODE: + raise exception.KeyManagerError(resp.reason) + if resp.status_code == requests.codes['forbidden']: + raise exception.Forbidden() + + resp = resp.json() + self._cached_approle_token_id = resp['auth']['client_token'] + self._approle_token_issue = token_issue_utc + self._approle_token_ttl = resp['auth']['lease_duration'] + return {'X-Vault-Token': self._approle_token_id} + + return {} + def _do_http_request(self, method, resource, json=None): verify = self._verify_server - headers = {'X-Vault-Token': self._root_token_id} + headers = self._build_auth_headers() try: resp = method(resource, headers=headers, json=json, verify=verify) diff --git a/castellan/options.py b/castellan/options.py index e748fd9..3aaa138 100644 --- a/castellan/options.py +++ b/castellan/options.py @@ -39,7 +39,9 @@ def set_defaults(conf, backend=None, barbican_endpoint=None, barbican_api_version=None, auth_endpoint=None, retry_delay=None, number_of_retries=None, verify_ssl=None, - api_class=None, vault_root_token_id=None, vault_url=None, + api_class=None, vault_root_token_id=None, + vault_approle_role_id=None, vault_approle_secret_id=None, + vault_url=None, vault_ssl_ca_crt_file=None, vault_use_ssl=None, barbican_endpoint_type=None): """Set defaults for configuration values. @@ -54,6 +56,9 @@ :param number_of_retries: Use this attribute to set number of retries. :param verify_ssl: Use this to specify if ssl should be verified. :param vault_root_token_id: Use this for the root token id for vault. + :param vault_approle_role_id: Use this for the approle role_id for vault. + :param vault_approle_secret_id: Use this for the approle secret_id + for vault. :param vault_url: Use this for the url for vault. :param vault_use_ssl: Use this to force vault driver to use ssl. :param vault_ssl_ca_crt_file: Use this for the CA file for vault. @@ -97,6 +102,12 @@ if vkm is not None: if vault_root_token_id is not None: conf.set_default('root_token_id', vault_root_token_id, + group=vkm.VAULT_OPT_GROUP) + if vault_approle_role_id is not None: + conf.set_default('approle_role_id', vault_approle_role_id, + group=vkm.VAULT_OPT_GROUP) + if vault_approle_secret_id is not None: + conf.set_default('approle_secret_id', vault_approle_secret_id, group=vkm.VAULT_OPT_GROUP) if vault_url is not None: conf.set_default('vault_url', vault_url, diff --git a/castellan/tests/functional/key_manager/test_vault_key_manager.py b/castellan/tests/functional/key_manager/test_vault_key_manager.py index e51bd39..c145164 100644 --- a/castellan/tests/functional/key_manager/test_vault_key_manager.py +++ b/castellan/tests/functional/key_manager/test_vault_key_manager.py @@ -17,11 +17,13 @@ """ import abc import os +import uuid from oslo_config import cfg from oslo_context import context from oslo_utils import uuidutils from oslotest import base +import requests from testtools import testcase from castellan.common import exception @@ -109,3 +111,105 @@ base.BaseTestCase): def get_context(self): return context.get_admin_context() + + +TEST_POLICY = ''' +path "{backend}/*" {{ + capabilities = ["create", "read", "update", "delete", "list"] +}} + +path "sys/internal/ui/mounts/{backend}" {{ + capabilities = ["read"] +}} +''' + +AUTH_ENDPOINT = 'v1/sys/auth/{auth_type}' +POLICY_ENDPOINT = 'v1/sys/policy/{policy_name}' +APPROLE_ENDPOINT = 'v1/auth/approle/role/{role_name}' + + +class VaultKeyManagerAppRoleTestCase(VaultKeyManagerOSLOContextTestCase): + + def _create_key_manager(self): + key_mgr = vault_key_manager.VaultKeyManager(cfg.CONF) + + if ('VAULT_TEST_URL' not in os.environ or + 'VAULT_TEST_ROOT_TOKEN' not in os.environ): + raise testcase.TestSkipped('Missing Vault setup information') + + self.root_token_id = os.environ['VAULT_TEST_ROOT_TOKEN'] + self.vault_url = os.environ['VAULT_TEST_URL'] + + test_uuid = str(uuid.uuid4()) + vault_policy = 'policy-{}'.format(test_uuid) + vault_approle = 'approle-{}'.format(test_uuid) + + self.session = requests.Session() + self.session.headers.update({'X-Vault-Token': self.root_token_id}) + + self._enable_approle() + self._create_policy(vault_policy) + self._create_approle(vault_approle, vault_policy) + + key_mgr._approle_role_id, key_mgr._approle_secret_id = ( + self._retrieve_approle(vault_approle) + ) + key_mgr._vault_url = self.vault_url + return key_mgr + + def _enable_approle(self): + params = { + 'type': 'approle' + } + self.session.post( + '{}/{}'.format( + self.vault_url, + AUTH_ENDPOINT.format(auth_type='approle') + ), + json=params, + ) + + def _create_policy(self, vault_policy): + params = { + 'rules': TEST_POLICY.format(backend='secret'), + } + self.session.put( + '{}/{}'.format( + self.vault_url, + POLICY_ENDPOINT.format(policy_name=vault_policy) + ), + json=params, + ) + + def _create_approle(self, vault_approle, vault_policy): + params = { + 'token_ttl': '60s', + 'token_max_ttl': '60s', + 'policies': [vault_policy], + 'bind_secret_id': 'true', + 'bound_cidr_list': '127.0.0.1/32' + } + self.session.post( + '{}/{}'.format( + self.vault_url, + APPROLE_ENDPOINT.format(role_name=vault_approle) + ), + json=params, + ) + + def _retrieve_approle(self, vault_approle): + approle_role_id = ( + self.session.get( + '{}/v1/auth/approle/role/{}/role-id'.format( + self.vault_url, + vault_approle + )).json()['data']['role_id'] + ) + approle_secret_id = ( + self.session.post( + '{}/v1/auth/approle/role/{}/secret-id'.format( + self.vault_url, + vault_approle + )).json()['data']['secret_id'] + ) + return (approle_role_id, approle_secret_id) diff --git a/releasenotes/notes/vault-approle-support-5ea04daea07a152f.yaml b/releasenotes/notes/vault-approle-support-5ea04daea07a152f.yaml new file mode 100644 index 0000000..84727be --- /dev/null +++ b/releasenotes/notes/vault-approle-support-5ea04daea07a152f.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Added support for AppRole based authentication to the Vault + key manager configured using new approle_role_id and + optional approle_secret_id options. + (https://www.vaultproject.io/docs/auth/approle.html)