vault: add AppRole support

Add support for use of AppRole's for authentication to Vault; this
feature provides a more application centric approach to managing
long term access to Vault.

The functional tests exercise this integration with a restricted
policy which only allows access to the default 'secret' backend.

Change-Id: I59dfe31adb72712c53d49f66d9ac894e43e8bbad
Closes-Bug: 1796851
This commit is contained in:
James Page 2018-10-10 10:07:11 +01:00
parent 6510f8639a
commit bc7f7a4c36
4 changed files with 183 additions and 2 deletions

View File

@ -29,6 +29,7 @@ import uuid
from keystoneauth1 import loading from keystoneauth1 import loading
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from oslo_utils import timeutils
import requests import requests
import six import six
@ -47,6 +48,10 @@ DEFAULT_VAULT_URL = "http://127.0.0.1:8200"
vault_opts = [ vault_opts = [
cfg.StrOpt('root_token_id', cfg.StrOpt('root_token_id',
help='root token for vault'), 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', cfg.StrOpt('vault_url',
default=DEFAULT_VAULT_URL, default=DEFAULT_VAULT_URL,
help='Use this endpoint to connect to Vault, for example: ' help='Use this endpoint to connect to Vault, for example: '
@ -88,6 +93,11 @@ class VaultKeyManager(key_manager.KeyManager):
loading.register_session_conf_options(self._conf, VAULT_OPT_GROUP) loading.register_session_conf_options(self._conf, VAULT_OPT_GROUP)
self._session = requests.Session() self._session = requests.Session()
self._root_token_id = self._conf.vault.root_token_id 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 self._vault_url = self._conf.vault.vault_url
if self._vault_url.startswith("https://"): if self._vault_url.startswith("https://"):
self._verify_server = self._conf.vault.ssl_ca_crt_file or True self._verify_server = self._conf.vault.ssl_ca_crt_file or True
@ -124,9 +134,58 @@ class VaultKeyManager(key_manager.KeyManager):
key_id if key_id else '?list=true') 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): def _do_http_request(self, method, resource, json=None):
verify = self._verify_server verify = self._verify_server
headers = {'X-Vault-Token': self._root_token_id} headers = self._build_auth_headers()
try: try:
resp = method(resource, headers=headers, json=json, verify=verify) resp = method(resource, headers=headers, json=json, verify=verify)

View File

@ -39,7 +39,9 @@ _DEFAULT_LOGGING_CONTEXT_FORMAT = ('%(asctime)s.%(msecs)03d %(process)d '
def set_defaults(conf, backend=None, barbican_endpoint=None, def set_defaults(conf, backend=None, barbican_endpoint=None,
barbican_api_version=None, auth_endpoint=None, barbican_api_version=None, auth_endpoint=None,
retry_delay=None, number_of_retries=None, verify_ssl=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, vault_ssl_ca_crt_file=None, vault_use_ssl=None,
barbican_endpoint_type=None): barbican_endpoint_type=None):
"""Set defaults for configuration values. """Set defaults for configuration values.
@ -54,6 +56,9 @@ def set_defaults(conf, backend=None, barbican_endpoint=None,
:param number_of_retries: Use this attribute to set number of retries. :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 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_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_url: Use this for the url for vault.
:param vault_use_ssl: Use this to force vault driver to use ssl. :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. :param vault_ssl_ca_crt_file: Use this for the CA file for vault.
@ -98,6 +103,12 @@ def set_defaults(conf, backend=None, barbican_endpoint=None,
if vault_root_token_id is not None: if vault_root_token_id is not None:
conf.set_default('root_token_id', vault_root_token_id, conf.set_default('root_token_id', vault_root_token_id,
group=vkm.VAULT_OPT_GROUP) 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: if vault_url is not None:
conf.set_default('vault_url', vault_url, conf.set_default('vault_url', vault_url,
group=vkm.VAULT_OPT_GROUP) group=vkm.VAULT_OPT_GROUP)

View File

@ -17,11 +17,13 @@ Note: This requires local running instance of Vault.
""" """
import abc import abc
import os import os
import uuid
from oslo_config import cfg from oslo_config import cfg
from oslo_context import context from oslo_context import context
from oslo_utils import uuidutils from oslo_utils import uuidutils
from oslotest import base from oslotest import base
import requests
from testtools import testcase from testtools import testcase
from castellan.common import exception from castellan.common import exception
@ -109,3 +111,105 @@ class VaultKeyManagerOSLOContextTestCase(VaultKeyManagerTestCase,
base.BaseTestCase): base.BaseTestCase):
def get_context(self): def get_context(self):
return context.get_admin_context() 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)

View File

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