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
James Page
4 years ago
28 | 28 | from keystoneauth1 import loading |
29 | 29 | from oslo_config import cfg |
30 | 30 | from oslo_log import log as logging |
31 | from oslo_utils import timeutils | |
31 | 32 | import requests |
32 | 33 | import six |
33 | 34 | |
46 | 47 | vault_opts = [ |
47 | 48 | cfg.StrOpt('root_token_id', |
48 | 49 | help='root token for vault'), |
50 | cfg.StrOpt('approle_role_id', | |
51 | help='AppRole role_id for authentication with vault'), | |
52 | cfg.StrOpt('approle_secret_id', | |
53 | help='AppRole secret_id for authentication with vault'), | |
49 | 54 | cfg.StrOpt('vault_url', |
50 | 55 | default=DEFAULT_VAULT_URL, |
51 | 56 | help='Use this endpoint to connect to Vault, for example: ' |
87 | 92 | loading.register_session_conf_options(self._conf, VAULT_OPT_GROUP) |
88 | 93 | self._session = requests.Session() |
89 | 94 | self._root_token_id = self._conf.vault.root_token_id |
95 | self._approle_role_id = self._conf.vault.approle_role_id | |
96 | self._approle_secret_id = self._conf.vault.approle_secret_id | |
97 | self._cached_approle_token_id = None | |
98 | self._approle_token_ttl = None | |
99 | self._approle_token_issue = None | |
90 | 100 | self._vault_url = self._conf.vault.vault_url |
91 | 101 | if self._vault_url.startswith("https://"): |
92 | 102 | self._verify_server = self._conf.vault.ssl_ca_crt_file or True |
123 | 133 | |
124 | 134 | key_id if key_id else '?list=true') |
125 | 135 | |
136 | @property | |
137 | def _approle_token_id(self): | |
138 | if (all((self._approle_token_issue, self._approle_token_ttl)) and | |
139 | timeutils.is_older_than(self._approle_token_issue, | |
140 | self._approle_token_ttl)): | |
141 | self._cached_approle_token_id = None | |
142 | return self._cached_approle_token_id | |
143 | ||
144 | def _build_auth_headers(self): | |
145 | if self._root_token_id: | |
146 | return {'X-Vault-Token': self._root_token_id} | |
147 | ||
148 | if self._approle_token_id: | |
149 | return {'X-Vault-Token': self._approle_token_id} | |
150 | ||
151 | if self._approle_role_id: | |
152 | params = { | |
153 | 'role_id': self._approle_role_id | |
154 | } | |
155 | if self._approle_secret_id: | |
156 | params['secret_id'] = self._approle_secret_id | |
157 | approle_login_url = '{}v1/auth/approle/login'.format( | |
158 | self._get_url() | |
159 | ) | |
160 | token_issue_utc = timeutils.utcnow() | |
161 | try: | |
162 | resp = self._session.post(url=approle_login_url, | |
163 | json=params, | |
164 | verify=self._verify_server) | |
165 | except requests.exceptions.Timeout as ex: | |
166 | raise exception.KeyManagerError(six.text_type(ex)) | |
167 | except requests.exceptions.ConnectionError as ex: | |
168 | raise exception.KeyManagerError(six.text_type(ex)) | |
169 | except Exception as ex: | |
170 | raise exception.KeyManagerError(six.text_type(ex)) | |
171 | ||
172 | if resp.status_code in _EXCEPTIONS_BY_CODE: | |
173 | raise exception.KeyManagerError(resp.reason) | |
174 | if resp.status_code == requests.codes['forbidden']: | |
175 | raise exception.Forbidden() | |
176 | ||
177 | resp = resp.json() | |
178 | self._cached_approle_token_id = resp['auth']['client_token'] | |
179 | self._approle_token_issue = token_issue_utc | |
180 | self._approle_token_ttl = resp['auth']['lease_duration'] | |
181 | return {'X-Vault-Token': self._approle_token_id} | |
182 | ||
183 | return {} | |
184 | ||
126 | 185 | def _do_http_request(self, method, resource, json=None): |
127 | 186 | verify = self._verify_server |
128 | headers = {'X-Vault-Token': self._root_token_id} | |
187 | headers = self._build_auth_headers() | |
129 | 188 | |
130 | 189 | try: |
131 | 190 | resp = method(resource, headers=headers, json=json, verify=verify) |
38 | 38 | def set_defaults(conf, backend=None, barbican_endpoint=None, |
39 | 39 | barbican_api_version=None, auth_endpoint=None, |
40 | 40 | retry_delay=None, number_of_retries=None, verify_ssl=None, |
41 | api_class=None, vault_root_token_id=None, vault_url=None, | |
41 | api_class=None, vault_root_token_id=None, | |
42 | vault_approle_role_id=None, vault_approle_secret_id=None, | |
43 | vault_url=None, | |
42 | 44 | vault_ssl_ca_crt_file=None, vault_use_ssl=None, |
43 | 45 | barbican_endpoint_type=None): |
44 | 46 | """Set defaults for configuration values. |
53 | 55 | :param number_of_retries: Use this attribute to set number of retries. |
54 | 56 | :param verify_ssl: Use this to specify if ssl should be verified. |
55 | 57 | :param vault_root_token_id: Use this for the root token id for vault. |
58 | :param vault_approle_role_id: Use this for the approle role_id for vault. | |
59 | :param vault_approle_secret_id: Use this for the approle secret_id | |
60 | for vault. | |
56 | 61 | :param vault_url: Use this for the url for vault. |
57 | 62 | :param vault_use_ssl: Use this to force vault driver to use ssl. |
58 | 63 | :param vault_ssl_ca_crt_file: Use this for the CA file for vault. |
96 | 101 | if vkm is not None: |
97 | 102 | if vault_root_token_id is not None: |
98 | 103 | conf.set_default('root_token_id', vault_root_token_id, |
104 | group=vkm.VAULT_OPT_GROUP) | |
105 | if vault_approle_role_id is not None: | |
106 | conf.set_default('approle_role_id', vault_approle_role_id, | |
107 | group=vkm.VAULT_OPT_GROUP) | |
108 | if vault_approle_secret_id is not None: | |
109 | conf.set_default('approle_secret_id', vault_approle_secret_id, | |
99 | 110 | group=vkm.VAULT_OPT_GROUP) |
100 | 111 | if vault_url is not None: |
101 | 112 | conf.set_default('vault_url', vault_url, |
16 | 16 | """ |
17 | 17 | import abc |
18 | 18 | import os |
19 | import uuid | |
19 | 20 | |
20 | 21 | from oslo_config import cfg |
21 | 22 | from oslo_context import context |
22 | 23 | from oslo_utils import uuidutils |
23 | 24 | from oslotest import base |
25 | import requests | |
24 | 26 | from testtools import testcase |
25 | 27 | |
26 | 28 | from castellan.common import exception |
108 | 110 | base.BaseTestCase): |
109 | 111 | def get_context(self): |
110 | 112 | return context.get_admin_context() |
113 | ||
114 | ||
115 | TEST_POLICY = ''' | |
116 | path "{backend}/*" {{ | |
117 | capabilities = ["create", "read", "update", "delete", "list"] | |
118 | }} | |
119 | ||
120 | path "sys/internal/ui/mounts/{backend}" {{ | |
121 | capabilities = ["read"] | |
122 | }} | |
123 | ''' | |
124 | ||
125 | AUTH_ENDPOINT = 'v1/sys/auth/{auth_type}' | |
126 | POLICY_ENDPOINT = 'v1/sys/policy/{policy_name}' | |
127 | APPROLE_ENDPOINT = 'v1/auth/approle/role/{role_name}' | |
128 | ||
129 | ||
130 | class VaultKeyManagerAppRoleTestCase(VaultKeyManagerOSLOContextTestCase): | |
131 | ||
132 | def _create_key_manager(self): | |
133 | key_mgr = vault_key_manager.VaultKeyManager(cfg.CONF) | |
134 | ||
135 | if ('VAULT_TEST_URL' not in os.environ or | |
136 | 'VAULT_TEST_ROOT_TOKEN' not in os.environ): | |
137 | raise testcase.TestSkipped('Missing Vault setup information') | |
138 | ||
139 | self.root_token_id = os.environ['VAULT_TEST_ROOT_TOKEN'] | |
140 | self.vault_url = os.environ['VAULT_TEST_URL'] | |
141 | ||
142 | test_uuid = str(uuid.uuid4()) | |
143 | vault_policy = 'policy-{}'.format(test_uuid) | |
144 | vault_approle = 'approle-{}'.format(test_uuid) | |
145 | ||
146 | self.session = requests.Session() | |
147 | self.session.headers.update({'X-Vault-Token': self.root_token_id}) | |
148 | ||
149 | self._enable_approle() | |
150 | self._create_policy(vault_policy) | |
151 | self._create_approle(vault_approle, vault_policy) | |
152 | ||
153 | key_mgr._approle_role_id, key_mgr._approle_secret_id = ( | |
154 | self._retrieve_approle(vault_approle) | |
155 | ) | |
156 | key_mgr._vault_url = self.vault_url | |
157 | return key_mgr | |
158 | ||
159 | def _enable_approle(self): | |
160 | params = { | |
161 | 'type': 'approle' | |
162 | } | |
163 | self.session.post( | |
164 | '{}/{}'.format( | |
165 | self.vault_url, | |
166 | AUTH_ENDPOINT.format(auth_type='approle') | |
167 | ), | |
168 | json=params, | |
169 | ) | |
170 | ||
171 | def _create_policy(self, vault_policy): | |
172 | params = { | |
173 | 'rules': TEST_POLICY.format(backend='secret'), | |
174 | } | |
175 | self.session.put( | |
176 | '{}/{}'.format( | |
177 | self.vault_url, | |
178 | POLICY_ENDPOINT.format(policy_name=vault_policy) | |
179 | ), | |
180 | json=params, | |
181 | ) | |
182 | ||
183 | def _create_approle(self, vault_approle, vault_policy): | |
184 | params = { | |
185 | 'token_ttl': '60s', | |
186 | 'token_max_ttl': '60s', | |
187 | 'policies': [vault_policy], | |
188 | 'bind_secret_id': 'true', | |
189 | 'bound_cidr_list': '127.0.0.1/32' | |
190 | } | |
191 | self.session.post( | |
192 | '{}/{}'.format( | |
193 | self.vault_url, | |
194 | APPROLE_ENDPOINT.format(role_name=vault_approle) | |
195 | ), | |
196 | json=params, | |
197 | ) | |
198 | ||
199 | def _retrieve_approle(self, vault_approle): | |
200 | approle_role_id = ( | |
201 | self.session.get( | |
202 | '{}/v1/auth/approle/role/{}/role-id'.format( | |
203 | self.vault_url, | |
204 | vault_approle | |
205 | )).json()['data']['role_id'] | |
206 | ) | |
207 | approle_secret_id = ( | |
208 | self.session.post( | |
209 | '{}/v1/auth/approle/role/{}/secret-id'.format( | |
210 | self.vault_url, | |
211 | vault_approle | |
212 | )).json()['data']['secret_id'] | |
213 | ) | |
214 | return (approle_role_id, approle_secret_id) |