From e7559b7769a09e00ea4197a7061c89607f3f0a65 Mon Sep 17 00:00:00 2001 From: jmoore Date: Tue, 4 Oct 2016 14:17:42 +0200 Subject: [PATCH 1/2] Add a lookup plugin for `pass` Copied from https://github.com/gcoop-libre/ansible-lookup-plugin-pass, this lookup plugin permits reading encrypted values using https://www.passwordstore.org By settings ANSIBLE_PASS_PASSWORD_STORE_DIR globally, a non-standard credentials directory can be chosen. ``` --- - hosts: localhost connection: local gather_facts: False tasks: - debug: msg="{{ lookup('pass', item) }}" with_items: - "ci/snoopy/sshconfig" ``` Also looked at: * https://github.com/Dissonant-Tech/ansible-lookup-pass * https://github.com/morphje/ansible_pass_lookup * https://github.com/lbischof/ansible-role-pass --- ansible/.gitignore | 2 + ansible/lookup_plugins/pass.py | 126 ++++++++++++++++++++++ ansible/roles/jenkinsslave/tasks/main.yml | 36 ++++--- 3 files changed, 150 insertions(+), 14 deletions(-) create mode 100755 ansible/lookup_plugins/pass.py diff --git a/ansible/.gitignore b/ansible/.gitignore index 598691b23..e867d9709 100644 --- a/ansible/.gitignore +++ b/ansible/.gitignore @@ -1,3 +1,5 @@ .vagrant *.retry *.un~ +.cloud*.lock +*.pyc diff --git a/ansible/lookup_plugins/pass.py b/ansible/lookup_plugins/pass.py new file mode 100755 index 000000000..27b059b3a --- /dev/null +++ b/ansible/lookup_plugins/pass.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python +# +# Ansible lookup plugin for ZXC24 password manager [0]. +# +# To change the default password store set ANSIBLE_PASS_PASSWORD_STORE_DIR +# environment variable. +# +# If the pass doesn't exist in the store it's generated. It accepts two extra +# parameters: length and symbols (if symbols is True or yes -n is appended to +# the pass generate command). +# +# example: {{ lookup('pass', 'path/to/site lenght=20 symbols=False) }} +# +# [0] https://www.passwordstore.org/ +# +# Originally: https://github.com/gcoop-libre/ansible-lookup-plugin-pass +# +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +try: + from __main__ import display +except ImportError: + from ansible.utils.display import Display + display = Display() + +import subprocess +import os + +from ansible.errors import AnsibleError +from ansible.plugins.lookup import LookupBase +from ansible.parsing.splitter import parse_kv + +PASSWORD_STORE_DIR = '~/.password-store' +if os.getenv('ANSIBLE_PASS_PASSWORD_STORE_DIR') is not None: + PASSWORD_STORE_DIR = os.environ['ANSIBLE_PASS_PASSWORD_STORE_DIR'] + +PASS_EXEC = 'PASSWORD_STORE_DIR=%s pass' % PASSWORD_STORE_DIR + +DEFAULT_LENGTH = 32 +VALID_PARAMS = frozenset(('length', 'symbols')) + +def _parse_parameters(term): + # Hacky parsing of params taken from password lookup. + first_split = term.split(' ', 1) + if len(first_split) <= 1: + # Only a single argument given, therefore it's a path + name = term + params = dict() + else: + name = first_split[0] + params = parse_kv(first_split[1]) + if '_raw_params' in params: + # Spaces in the path? + name = ' '.join((name, params['_raw_params'])) + del params['_raw_params'] + + # Check that we parsed the params correctly + if not term.startswith(name): + # Likely, the user had a non parameter following a parameter. + # Reject this as a user typo + raise AnsibleError('Unrecognized value after key=value parameters given to password lookup') + # No _raw_params means we already found the complete path when + # we split it initially + + # Check for invalid parameters. Probably a user typo + invalid_params = frozenset(params.keys()).difference(VALID_PARAMS) + if invalid_params: + raise AnsibleError('Unrecognized parameter(s) given to password lookup: %s' % ', '.join(invalid_params)) + + # Set defaults + params['length'] = int(params.get('length', DEFAULT_LENGTH)) + symbols = params.get('symbols', 'False') + if symbols.lower() in ['true', 'yes']: + params['symbols'] = True + else: + params['symbols'] = False + + return name, params + + +def get_password(path): + """Get password from pass.""" + command = '%s show %s' % (PASS_EXEC, path) + p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + (stdout, stderr) = p.communicate() + if p.returncode == 0: + return stdout.rstrip() + raise Exception(stderr) + +def generate_password(path, length, symbols): + """Generate password using pass.""" + command = '%s generate %s %s' % (PASS_EXEC, path, length) + display.vvv('COMMAND: %s' % command) + if not symbols: + command = command + ' -n' + + p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + (stdout, stderr) = p.communicate() + if p.returncode != 0: + raise Exception(stderr) + +class LookupModule(LookupBase): + def run(self, terms, variables=None, **kwargs): + ret = [] + + for term in terms: + ''' + http://docs.python.org/2/library/subprocess.html#popen-constructor + The shell argument (which defaults to False) specifies whether to use the + shell as the program to execute. If shell is True, it is recommended to pass + args as a string rather than as a sequence + https://github.com/ansible/ansible/issues/6550 + ''' + name, params = _parse_parameters(term) + try: + password = get_password(term) + except: + try: + generate_password(name, params['length'], params['symbols']) + display.vvv('Generated password for %s' % name) + password = get_password(name) + except Exception as e: + raise AnsibleError("lookup_plugin.pass(%s) returned %s" % (term, e.message)) + ret.append(password) + return ret diff --git a/ansible/roles/jenkinsslave/tasks/main.yml b/ansible/roles/jenkinsslave/tasks/main.yml index 28d92d73b..8effa9170 100644 --- a/ansible/roles/jenkinsslave/tasks/main.yml +++ b/ansible/roles/jenkinsslave/tasks/main.yml @@ -25,34 +25,42 @@ user: "{{ jenkinsuser }}" key: "{{ authorized_key }}" -- name: copy snoopy keys +- name: jenkins | prepare .ssh + become: yes + become_user: "{{ jenkinsuser }}" + file: + path: "/home/{{ jenkinsuser }}/.ssh" + state: directory + mode: 0700 + +- name: jenkins | copy snoopy keys become: yes become_user: "{{ jenkinsuser }}" copy: - src={{ snoopy_dir_path }}/{{ item.srcfile }} - dest=/home/{{ jenkinsuser }}/{{ item.dest }} - owner={{ jenkinsuser }} - group={{ jenkinsuser }} - mode=0700 + content: "{{ lookup('pass', item.pass_path) }}" + dest: "/home/{{ jenkinsuser }}/{{ item.dest }}" + owner: "{{ jenkinsuser }}" + group: "{{ jenkinsuser }}" + mode: 0600 with_items: - - { srcfile: "config", dest: ".ssh/config" } - - { srcfile: "snoopycrimecop_github", dest: ".ssh/snoopycrimecop_github" } - - { srcfile: "snoopycrimecop_github.pub", dest: ".ssh/snoopycrimecop_github.pub" } - when: not spacewalk and snoopy_dir_path|length>0 + - { pass_path: "ci/snoopy/sshconfig", dest: ".ssh/config" } + - { pass_path: "ci/snoopy/id_rsa", dest: ".ssh/snoopycrimecop_github" } + - { pass_path: "ci/snoopy/id_rsa.pub", dest: ".ssh/snoopycrimecop_github.pub" } + when: not spacewalk -- name: generate known_hosts +- name: jenkins | generate known_hosts become: yes become_user: "{{ jenkinsuser }}" shell: ssh-keyscan github.com >> /home/{{ jenkinsuser }}/.ssh/known_hosts - when: not spacewalk and snoopy_dir_path|length>0 + when: not spacewalk -- name: chmod known_hosts +- name: jenkins | chmod known_hosts become: yes become_user: "{{ jenkinsuser }}" file: path=/home/{{ jenkinsuser }}/.ssh/known_hosts mode=0700 - when: not spacewalk and snoopy_dir_path|length>0 + when: not spacewalk - name: jenkins | ssh access become: yes From 03fc7ef2f069c5ceef0b8ba87af8b6c5b7b5e78d Mon Sep 17 00:00:00 2001 From: jmoore Date: Wed, 5 Oct 2016 08:46:36 +0200 Subject: [PATCH 2/2] pass: set default length to 0 Rather than create a password when none exists, require the user to set a length > 0. This is especially useful for detecting that no PASSWORD_STORE_DIR or similar is unset. --- ansible/lookup_plugins/pass.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/ansible/lookup_plugins/pass.py b/ansible/lookup_plugins/pass.py index 27b059b3a..752c5ebf3 100755 --- a/ansible/lookup_plugins/pass.py +++ b/ansible/lookup_plugins/pass.py @@ -5,11 +5,12 @@ # To change the default password store set ANSIBLE_PASS_PASSWORD_STORE_DIR # environment variable. # -# If the pass doesn't exist in the store it's generated. It accepts two extra -# parameters: length and symbols (if symbols is True or yes -n is appended to -# the pass generate command). +# It accepts two extra parameters: length and symbols +# (if symbols is True or yes -n is appended to the pass generate command). # -# example: {{ lookup('pass', 'path/to/site lenght=20 symbols=False) }} +# If the pass doesn't exist in the store and lenght > 0, it's generated. +# +# example: {{ lookup('pass', 'path/to/site length=20 symbols=False) }} # # [0] https://www.passwordstore.org/ # @@ -37,7 +38,7 @@ PASS_EXEC = 'PASSWORD_STORE_DIR=%s pass' % PASSWORD_STORE_DIR -DEFAULT_LENGTH = 32 +DEFAULT_LENGTH = 0 VALID_PARAMS = frozenset(('length', 'symbols')) def _parse_parameters(term): @@ -116,6 +117,10 @@ def run(self, terms, variables=None, **kwargs): try: password = get_password(term) except: + if not params['length']: + raise AnsibleError(( + "lookup_plugin.pass(%s) not found " + "- set length > 0 to create" ) % term) try: generate_password(name, params['length'], params['symbols']) display.vvv('Generated password for %s' % name)