commit 23a7bc1379d557f547e66017b386e48aa343afba
parent c45e1736c2e51689f7ef1d0ec2a3fce6c5a81f5a
Author: Leah (ctucx) <leah@ctu.cx>
Date: Sun, 21 Feb 2021 02:24:43 +0100
parent c45e1736c2e51689f7ef1d0ec2a3fce6c5a81f5a
Author: Leah (ctucx) <leah@ctu.cx>
Date: Sun, 21 Feb 2021 02:24:43 +0100
add local copy of passwordstore lookup plugin
5 files changed, 323 insertions(+), 16 deletions(-)
A
|
309
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
diff --git a/configuration/joguhrtbecher.yml b/configuration/joguhrtbecher.yml @@ -33,10 +33,10 @@ networkd: - Name: wg-pbb - Kind: wireguard - WireGuard: - - PrivateKey: "{{ lookup('diskcache', 'community.general.passwordstore', 'Server/joguhrtbecher/wireguard.privkey returnall=true') }}" + - PrivateKey: "{{ lookup('diskcache', 'passwordstore', 'Server/joguhrtbecher/wireguard.privkey returnall=true') }}" - FirewallMark: 0x8888 - WireGuardPeer: - - PublicKey: "{{ lookup('diskcache', 'community.general.passwordstore', 'Server/desastro/wireguard.pubkey returnall=true') }}" + - PublicKey: "{{ lookup('diskcache', 'passwordstore', 'Server/desastro/wireguard.pubkey returnall=true') }}" - AllowedIPs: "0.0.0.0/0, ::/0" - Endpoint: "195.39.247.172:51820" - PersistentKeepalive: 10
diff --git a/configuration/lollo.yml b/configuration/lollo.yml @@ -42,10 +42,10 @@ networkd: - Name: wg-pbb - Kind: wireguard - WireGuard: - - PrivateKey: "{{ lookup('diskcache', 'community.general.passwordstore', 'Server/lollo/wireguard.privkey returnall=true') }}" + - PrivateKey: "{{ lookup('diskcache', 'passwordstore', 'Server/lollo/wireguard.privkey returnall=true') }}" - FirewallMark: 51820 - WireGuardPeer: - - PublicKey: "{{ lookup('diskcache', 'community.general.passwordstore', 'Server/desastro/wireguard.pubkey returnall=true') }}" + - PublicKey: "{{ lookup('diskcache', 'passwordstore', 'Server/desastro/wireguard.pubkey returnall=true') }}" - AllowedIPs: "0.0.0.0/0, ::/0" - Endpoint: "195.39.247.172:51820" - PersistentKeepalive: 10 @@ -124,7 +124,7 @@ networkd: files: /etc/nginx/passwd/influx: state: "file" - content: "{{ lookup('diskcache', 'community.general.passwordstore', 'Server/{{system.hostname}}/passwd/home.ctu.cx/influx returnall=true')}}" + content: "{{ lookup('diskcache', 'passwordstore', 'Server/{{system.hostname}}/passwd/home.ctu.cx/influx returnall=true')}}" mode: "0600" owner: "nginx" group: "nginx" @@ -254,12 +254,12 @@ services: " hostapd: - enable: true + enable: false interface: wlp3s0 bridge: brlan channel: 1 ssid: hostapd.home.ctu.cx - passphrase: "{{ lookup('diskcache', 'community.general.passwordstore', 'WiFi/legacy.home.ctu.cx returnall=true')}}" + passphrase: "{{ lookup('diskcache', 'passwordstore', 'WiFi/legacy.home.ctu.cx returnall=true')}}" dnsmasq: enable: true @@ -314,8 +314,6 @@ services: hosts: # accesspoint - f4:06:8d:df:1f:e3, accesspoint, 10.0.0.2 - # tradfri gateway - - 58:d5:0a:ba:23:29, tradfri, 10.0.0.10 # ctucx macbook - id:00:01:00:01:27:51:55:30:80:e6:50:21:e0:6a, toaster, [2a0f:4ac0:acab::34] - 80:e6:50:21:e0:6a, toaster, 195.39.246.34 @@ -350,7 +348,7 @@ services: enable: true serverAddress: wanderduene.ctu.cx serverPort: 5050 - token: "{{ lookup('diskcache', 'community.general.passwordstore', 'Server/wanderduene/frps/token returnall=true')}}" + token: "{{ lookup('diskcache', 'passwordstore', 'Server/wanderduene/frps/token returnall=true')}}" dashboard: false tunnels: - name: lollo-ssh
diff --git a/configuration/taurus.yml b/configuration/taurus.yml @@ -124,7 +124,7 @@ services: # nginx: # enable: true # domain: "restic.ctu.cx" -# password: "{{ lookup('diskcache', 'community.general.passwordstore', 'Server/taurus/rest-server.htpasswd returnall=true') }}" +# password: "{{ lookup('diskcache', 'passwordstore', 'Server/taurus/rest-server.htpasswd returnall=true') }}" # sslOnly: true # ssl: # enable: true
diff --git a/configuration/wanderduene.yml b/configuration/wanderduene.yml @@ -293,7 +293,7 @@ services: radicale: enable: true configFile: config-files/radicale/config - users: "{{ lookup('diskcache', 'community.general.passwordstore', 'Server/{{system.hostname}}/radicale.users returnall=true')}}" + users: "{{ lookup('diskcache', 'passwordstore', 'Server/{{system.hostname}}/radicale.users returnall=true')}}" nginx: enable: true domain: "dav.ctu.cx" @@ -426,7 +426,7 @@ services: scrape_configs: - job_name: 'prometheus' static_configs: - - targets: ['localhost:9090'] + - targets: ['127.0.0.1:9090'] - job_name: 'node-exporter' metrics_path: '/node-exporter' @@ -489,7 +489,7 @@ services: pleroma: enable: true configFile: config-files/pleroma/config.exs - secretsContent: "{{ lookup('diskcache', 'community.general.passwordstore', 'Server/{{system.hostname}}/pleroma.secrets returnall=true')}}" + secretsContent: "{{ lookup('diskcache', 'passwordstore', 'Server/{{system.hostname}}/pleroma.secrets returnall=true')}}" nginx: enable: true domain: "pleroma.ctu.cx" @@ -512,7 +512,7 @@ services: frps: enable: true - token: "{{ lookup('diskcache', 'community.general.passwordstore', 'Server/{{system.hostname}}/frps/token returnall=true')}}" + token: "{{ lookup('diskcache', 'passwordstore', 'Server/{{system.hostname}}/frps/token returnall=true')}}" port: 5050 vhostDomain: "frp.ctu.cx" vhostPort: 8088 @@ -539,7 +539,7 @@ files: group: "nginx" /etc/nginx/passwd/print: state: "file" - content: "{{ lookup('diskcache', 'community.general.passwordstore', 'Server/{{system.hostname}}/passwd/ctu.cx/drucken returnall=true')}}" + content: "{{ lookup('diskcache', 'passwordstore', 'Server/{{system.hostname}}/passwd/ctu.cx/drucken returnall=true')}}" mode: "0600" owner: "nginx" group: "nginx" \ No newline at end of file
diff --git a/lookup_plugins/passwordstore.py b/lookup_plugins/passwordstore.py @@ -0,0 +1,309 @@ +# (c) 2017, Patrick Deelman <patrick@patrickdeelman.nl> +# (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 = ''' + name: passwordstore + author: + - Patrick Deelman (!UNKNOWN) <patrick@patrickdeelman.nl> + short_description: manage passwords with passwordstore.org's pass utility + description: + - Enables Ansible to retrieve, create or update passwords from the passwordstore.org pass utility. + It also retrieves YAML style keys stored as multilines in the passwordfile. + options: + _terms: + description: query key. + required: True + passwordstore: + description: location of the password store. + default: '~/.password-store' + directory: + description: The directory of the password store. + env: + - name: PASSWORD_STORE_DIR + create: + description: Create the password if it does not already exist. + type: bool + default: 'no' + overwrite: + description: Overwrite the password if it does already exist. + type: bool + default: 'no' + umask: + description: + - Sets the umask for the created .gpg files. The first octed must be greater than 3 (user readable). + - Note pass' default value is C('077'). + env: + - name: PASSWORD_STORE_UMASK + version_added: 1.3.0 + returnall: + description: Return all the content of the password, not only the first line. + type: bool + default: 'no' + subkey: + description: Return a specific subkey of the password. When set to C(password), always returns the first line. + default: password + userpass: + description: Specify a password to save, instead of a generated one. + length: + description: The length of the generated password. + type: integer + default: 16 + backup: + description: Used with C(overwrite=yes). Backup the previous password in a subkey. + type: bool + default: 'no' + nosymbols: + description: use alphanumeric characters. + type: bool + default: 'no' +''' +EXAMPLES = """ +# Debug is used for examples, BAD IDEA to show passwords on screen +- name: Basic lookup. Fails if example/test doesn't exist + ansible.builtin.debug: + msg: "{{ lookup('community.general.passwordstore', 'example/test')}}" + +- name: Create pass with random 16 character password. If password exists just give the password + ansible.builtin.debug: + var: mypassword + vars: + mypassword: "{{ lookup('community.general.passwordstore', 'example/test create=true')}}" + +- name: Different size password + ansible.builtin.debug: + msg: "{{ lookup('community.general.passwordstore', 'example/test create=true length=42')}}" + +- name: Create password and overwrite the password if it exists. As a bonus, this module includes the old password inside the pass file + ansible.builtin.debug: + msg: "{{ lookup('community.general.passwordstore', 'example/test create=true overwrite=true')}}" + +- name: Create an alphanumeric password + ansible.builtin.debug: + msg: "{{ lookup('community.general.passwordstore', 'example/test create=true nosymbols=true') }}" + +- name: Return the value for user in the KV pair user, username + ansible.builtin.debug: + msg: "{{ lookup('community.general.passwordstore', 'example/test subkey=user')}}" + +- name: Return the entire password file content + ansible.builtin.set_fact: + passfilecontent: "{{ lookup('community.general.passwordstore', 'example/test returnall=true')}}" +""" + +RETURN = """ +_raw: + description: + - a password + type: list + elements: str +""" + +import os +import subprocess +import time +import yaml + + +from distutils import util +from ansible.errors import AnsibleError, AnsibleAssertionError +from ansible.module_utils._text import to_bytes, to_native, to_text +from ansible.utils.encrypt import random_password +from ansible.plugins.lookup import LookupBase +from ansible import constants as C + + +# backhacked check_output with input for python 2.7 +# http://stackoverflow.com/questions/10103551/passing-data-to-subprocess-check-output +def check_output2(*popenargs, **kwargs): + if 'stdout' in kwargs: + raise ValueError('stdout argument not allowed, it will be overridden.') + if 'stderr' in kwargs: + raise ValueError('stderr argument not allowed, it will be overridden.') + if 'input' in kwargs: + if 'stdin' in kwargs: + raise ValueError('stdin and input arguments may not both be used.') + b_inputdata = to_bytes(kwargs['input'], errors='surrogate_or_strict') + del kwargs['input'] + kwargs['stdin'] = subprocess.PIPE + else: + b_inputdata = None + process = subprocess.Popen(*popenargs, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs) + try: + b_out, b_err = process.communicate(b_inputdata) + except Exception: + process.kill() + process.wait() + raise + retcode = process.poll() + if retcode != 0 or \ + b'encryption failed: Unusable public key' in b_out or \ + b'encryption failed: Unusable public key' in b_err: + cmd = kwargs.get("args") + if cmd is None: + cmd = popenargs[0] + raise subprocess.CalledProcessError( + retcode, + cmd, + to_native(b_out + b_err, errors='surrogate_or_strict') + ) + return b_out + + +class LookupModule(LookupBase): + def parse_params(self, term): + # I went with the "traditional" param followed with space separated KV pairs. + # Waiting for final implementation of lookup parameter parsing. + # See: https://github.com/ansible/ansible/issues/12255 + params = term.split() + if len(params) > 0: + # the first param is the pass-name + self.passname = params[0] + # next parse the optional parameters in keyvalue pairs + try: + for param in params[1:]: + name, value = param.split('=', 1) + if name not in self.paramvals: + raise AnsibleAssertionError('%s not in paramvals' % name) + self.paramvals[name] = value + except (ValueError, AssertionError) as e: + raise AnsibleError(e) + # check and convert values + try: + for key in ['create', 'returnall', 'overwrite', 'backup', 'nosymbols']: + if not isinstance(self.paramvals[key], bool): + self.paramvals[key] = util.strtobool(self.paramvals[key]) + except (ValueError, AssertionError) as e: + raise AnsibleError(e) + if not isinstance(self.paramvals['length'], int): + if self.paramvals['length'].isdigit(): + self.paramvals['length'] = int(self.paramvals['length']) + else: + raise AnsibleError("{0} is not a correct value for length".format(self.paramvals['length'])) + + # Collect pass environment variables from the plugin's parameters. + self.env = os.environ.copy() + + # Set PASSWORD_STORE_DIR if directory is set + if self.paramvals['directory']: + if os.path.isdir(self.paramvals['directory']): + self.env['PASSWORD_STORE_DIR'] = self.paramvals['directory'] + else: + raise AnsibleError('Passwordstore directory \'{0}\' does not exist'.format(self.paramvals['directory'])) + + # Set PASSWORD_STORE_UMASK if umask is set + if 'umask' in self.paramvals: + if len(self.paramvals['umask']) != 3: + raise AnsibleError('Passwordstore umask must have a length of 3.') + elif int(self.paramvals['umask'][0]) > 3: + raise AnsibleError('Passwordstore umask not allowed (password not user readable).') + else: + self.env['PASSWORD_STORE_UMASK'] = self.paramvals['umask'] + + def check_pass(self): + try: + self.passoutput = to_text( + check_output2(["pass", "show", self.passname], env=self.env), + errors='surrogate_or_strict' + ).splitlines() + self.password = self.passoutput[0] + self.passdict = {} + try: + values = yaml.safe_load('\n'.join(self.passoutput[1:])) + for key, item in values.items(): + self.passdict[key] = item + except (yaml.YAMLError, AttributeError): + for line in self.passoutput[1:]: + if ':' in line: + name, value = line.split(':', 1) + self.passdict[name.strip()] = value.strip() + except (subprocess.CalledProcessError) as e: + if e.returncode != 0 and 'not in the password store' in e.output: + # if pass returns 1 and return string contains 'is not in the password store.' + # We need to determine if this is valid or Error. + if not self.paramvals['create']: + raise AnsibleError('passname: {0} not found, use create=True'.format(self.passname)) + else: + return False + else: + raise AnsibleError(e) + return True + + def get_newpass(self): + if self.paramvals['nosymbols']: + chars = C.DEFAULT_PASSWORD_CHARS[:62] + else: + chars = C.DEFAULT_PASSWORD_CHARS + + if self.paramvals['userpass']: + newpass = self.paramvals['userpass'] + else: + newpass = random_password(length=self.paramvals['length'], chars=chars) + return newpass + + def update_password(self): + # generate new password, insert old lines from current result and return new password + newpass = self.get_newpass() + datetime = time.strftime("%d/%m/%Y %H:%M:%S") + msg = newpass + '\n' + if self.passoutput[1:]: + msg += '\n'.join(self.passoutput[1:]) + '\n' + if self.paramvals['backup']: + msg += "lookup_pass: old password was {0} (Updated on {1})\n".format(self.password, datetime) + try: + check_output2(['pass', 'insert', '-f', '-m', self.passname], input=msg, env=self.env) + except (subprocess.CalledProcessError) as e: + raise AnsibleError(e) + return newpass + + def generate_password(self): + # generate new file and insert lookup_pass: Generated by Ansible on {date} + # use pwgen to generate the password and insert values with pass -m + newpass = self.get_newpass() + datetime = time.strftime("%d/%m/%Y %H:%M:%S") + msg = newpass + '\n' + "lookup_pass: First generated by ansible on {0}\n".format(datetime) + try: + check_output2(['pass', 'insert', '-f', '-m', self.passname], input=msg, env=self.env) + except (subprocess.CalledProcessError) as e: + raise AnsibleError(e) + return newpass + + def get_passresult(self): + if self.paramvals['returnall']: + return os.linesep.join(self.passoutput) + if self.paramvals['subkey'] == 'password': + return self.password + else: + if self.paramvals['subkey'] in self.passdict: + return self.passdict[self.paramvals['subkey']] + else: + return None + + def run(self, terms, variables, **kwargs): + result = [] + self.paramvals = { + 'subkey': 'password', + 'directory': variables.get('passwordstore'), + 'create': False, + 'returnall': False, + 'overwrite': False, + 'nosymbols': False, + 'userpass': '', + 'length': 16, + 'backup': False, + } + + for term in terms: + self.parse_params(term) # parse the input into paramvals + if self.check_pass(): # password exists + if self.paramvals['overwrite'] and self.paramvals['subkey'] == 'password': + result.append(self.update_password()) + else: + result.append(self.get_passresult()) + else: # password does not exist + if self.paramvals['create']: + result.append(self.generate_password()) + return result