# (c) 2015, Jonathan Davila # (c) 2017 Ansible Project # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import (absolute_import, division, print_function) __metaclass__ = type DOCUMENTATION = """ lookup: hashi_vault author: Jonathan Davila version_added: "2.0" short_description: retrieve secrets from HashiCorp's vault requirements: - hvac (python library) description: - retrieve secrets from HashiCorp's vault notes: - Due to a current limitation in the HVAC library there won't necessarily be an error if a bad endpoint is specified. options: secret: description: query you are making required: True token: description: vault token env: - name: VAULT_TOKEN url: description: url to vault service env: - name: VAULT_ADDR default: 'http://127.0.0.1:8200' username: description: authentication user name password: description: authentication password role_id: description: Role id for a vault AppRole auth env: - name: VAULT_ROLE_ID secret_id: description: Secret id for a vault AppRole auth env: - name: VAULT_SECRET_ID auth_method: description: authentication method used mount_point: description: vault mount point, only required if you have a custom mount point default: ldap cacert: description: path to certificate to use for authentication validate_certs: description: controls verification and validation of SSL certificates, mostly you only want to turn off with self signed ones. type: boolean default: True """ EXAMPLES = """ - debug: msg: "{{ lookup('hashi_vault', 'secret=secret/hello:value token=c975b780-d1be-8016-866b-01d0f9b688a5 url=http://myvault:8200')}}" - name: Return all secrets from a path debug: msg: "{{ lookup('hashi_vault', 'secret=secret/hello token=c975b780-d1be-8016-866b-01d0f9b688a5 url=http://myvault:8200')}}" - name: Vault that requires authentication via LDAP debug: msg: "{{ lookup('hashi_vault', 'secret=secret/hello:value auth_method=ldap mount_point=ldap username=myuser password=mypas url=http://myvault:8200')}}" - name: Using an ssl vault debug: msg: "{{ lookup('hashi_vault', 'secret=secret/hola:value token=c975b780-d1be-8016-866b-01d0f9b688a5 url=https://myvault:8200 validate_certs=False')}}" - name: using certificate auth debug: msg: "{{ lookup('hashi_vault', 'secret=secret/hi:value token=xxxx-xxx-xxx url=https://myvault:8200 validate_certs=True cacert=/cacert/path/ca.pem')}}" - name: authenticate with a Vault app role debug: msg: "{{ lookup('hashi_vault', 'secret=secret/hello:value auth_method=approle role_id=myroleid secret_id=mysecretid url=http://myvault:8200')}}" """ RETURN = """ _raw: description: - secrets(s) requested """ import os from ansible.errors import AnsibleError from ansible.module_utils.parsing.convert_bool import boolean from ansible.plugins.lookup import LookupBase HAS_HVAC = False try: import hvac HAS_HVAC = True except ImportError: HAS_HVAC = False ANSIBLE_HASHI_VAULT_ADDR = 'http://127.0.0.1:8200' if os.getenv('VAULT_ADDR') is not None: ANSIBLE_HASHI_VAULT_ADDR = os.environ['VAULT_ADDR'] class HashiVault: def __init__(self, **kwargs): self.url = kwargs.get('url', ANSIBLE_HASHI_VAULT_ADDR) # split secret arg, which has format 'secret/hello:value' into secret='secret/hello' and secret_field='value' s = kwargs.get('secret') if s is None: raise AnsibleError("No secret specified for hashi_vault lookup") s_f = s.rsplit(':', 1) self.secret = s_f[0] if len(s_f) >= 2: self.secret_field = s_f[1] else: self.secret_field = '' self.verify = self.boolean_or_cacert(kwargs.get('validate_certs', True), kwargs.get('cacert', '')) # If a particular backend is asked for (and its method exists) we call it, otherwise drop through to using # token auth. This means if a particular auth backend is requested and a token is also given, then we # ignore the token and attempt authentication against the specified backend. # # to enable a new auth backend, simply add a new 'def auth_' method below. # self.auth_method = kwargs.get('auth_method') if self.auth_method and self.auth_method != 'token': try: self.client = hvac.Client(url=self.url, verify=self.verify) # prefixing with auth_ to limit which methods can be accessed getattr(self, 'auth_' + self.auth_method)(**kwargs) except AttributeError: raise AnsibleError("Authentication method '%s' not supported" % self.auth_method) else: self.token = kwargs.get('token', os.environ.get('VAULT_TOKEN', None)) if self.token is None and os.environ.get('HOME'): token_filename = os.path.join( os.environ.get('HOME'), '.vault-token' ) if os.path.exists(token_filename): with open(token_filename) as token_file: self.token = token_file.read().strip() if self.token is None: raise AnsibleError("No Vault Token specified") self.client = hvac.Client(url=self.url, token=self.token, verify=self.verify) if not self.client.is_authenticated(): raise AnsibleError("Invalid Hashicorp Vault Token Specified for hashi_vault lookup") def get(self): data = self.client.read(self.secret) if data is None: raise AnsibleError("The secret %s doesn't seem to exist for hashi_vault lookup" % self.secret) if self.secret_field == '': return data['data'] if self.secret_field not in data['data']: raise AnsibleError("The secret %s does not contain the field '%s'. for hashi_vault lookup" % (self.secret, self.secret_field)) return data['data'][self.secret_field] def auth_ldap(self, **kwargs): username = kwargs.get('username') if username is None: raise AnsibleError("Authentication method ldap requires a username") password = kwargs.get('password') if password is None: raise AnsibleError("Authentication method ldap requires a password") mount_point = kwargs.get('mount_point') if mount_point is None: mount_point = 'ldap' self.client.auth_ldap(username, password, mount_point) def boolean_or_cacert(self, validate_certs, cacert): validate_certs = boolean(validate_certs, strict=False) '''' return a bool or cacert ''' if validate_certs is True: if cacert != '': return cacert else: return True else: return False def auth_approle(self, **kwargs): role_id = kwargs.get('role_id', os.environ.get('VAULT_ROLE_ID', None)) if role_id is None: raise AnsibleError("Authentication method app role requires a role_id") secret_id = kwargs.get('secret_id', os.environ.get('VAULT_SECRET_ID', None)) if secret_id is None: raise AnsibleError("Authentication method app role requires a secret_id") self.client.auth_approle(role_id, secret_id) class LookupModule(LookupBase): def run(self, terms, variables, **kwargs): if not HAS_HVAC: raise AnsibleError("Please pip install hvac to use the hashi_vault lookup module.") vault_args = terms[0].split(' ') vault_dict = {} ret = [] for param in vault_args: try: key, value = param.split('=') except ValueError: raise AnsibleError("hashi_vault lookup plugin needs key=value pairs, but received %s" % terms) vault_dict[key] = value vault_conn = HashiVault(**vault_dict) for term in terms: key = term.split()[0] value = vault_conn.get() ret.append(value) return ret