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:
parent
6510f8639a
commit
bc7f7a4c36
@ -29,6 +29,7 @@ import uuid
|
||||
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 @@ DEFAULT_VAULT_URL = "http://127.0.0.1:8200"
|
||||
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 @@ class VaultKeyManager(key_manager.KeyManager):
|
||||
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 @@ class VaultKeyManager(key_manager.KeyManager):
|
||||
|
||||
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)
|
||||
|
@ -39,7 +39,9 @@ _DEFAULT_LOGGING_CONTEXT_FORMAT = ('%(asctime)s.%(msecs)03d %(process)d '
|
||||
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 @@ def set_defaults(conf, backend=None, barbican_endpoint=None,
|
||||
: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.
|
||||
@ -98,6 +103,12 @@ def set_defaults(conf, backend=None, barbican_endpoint=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,
|
||||
group=vkm.VAULT_OPT_GROUP)
|
||||
|
@ -17,11 +17,13 @@ Note: This requires local running instance of Vault.
|
||||
"""
|
||||
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 @@ class VaultKeyManagerOSLOContextTestCase(VaultKeyManagerTestCase,
|
||||
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)
|
||||
|
@ -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)
|
Loading…
Reference in New Issue
Block a user