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