diff --git a/ansible.cfg b/ansible.cfg index 3bfbe11..9b89217 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -1,13 +1,15 @@ [defaults] # Explicitely redefined some defaults to make play execution work roles_path = ./roles +vars_plugins = ./vars_plugins inventory = ./hosts timeout = 60 [privilege_escalation] become = True -become_ask_pass = True +# Use a separate module to read passwords from pass +become_ask_pass = False [ssh_connection] pipelining = True diff --git a/vars_plugins/__pycache__/pass.cpython-39.pyc b/vars_plugins/__pycache__/pass.cpython-39.pyc new file mode 100644 index 0000000..cd637fa Binary files /dev/null and b/vars_plugins/__pycache__/pass.cpython-39.pyc differ diff --git a/vars_plugins/__pycache__/vault_cranspasswords.cpython-39.pyc b/vars_plugins/__pycache__/vault_cranspasswords.cpython-39.pyc new file mode 100644 index 0000000..fd89940 Binary files /dev/null and b/vars_plugins/__pycache__/vault_cranspasswords.cpython-39.pyc differ diff --git a/vars_plugins/pass.ini b/vars_plugins/pass.ini new file mode 100644 index 0000000..94c6ce9 --- /dev/null +++ b/vars_plugins/pass.ini @@ -0,0 +1,6 @@ +[pass] +# password_store_dir=/home/ynerant/.password-store +# crans_password_store_submodule=crans + +[pass_become] +all=templier diff --git a/vars_plugins/pass.ini.example b/vars_plugins/pass.ini.example new file mode 100644 index 0000000..71bf6af --- /dev/null +++ b/vars_plugins/pass.ini.example @@ -0,0 +1,6 @@ +[pass] +# password_store_dir=/home/me/.password-store +# crans_password_store_submodule=crans + +[pass_become] +# all=mdp-root diff --git a/vars_plugins/pass.py b/vars_plugins/pass.py new file mode 100644 index 0000000..6db4685 --- /dev/null +++ b/vars_plugins/pass.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python + +from functools import lru_cache +from getpass import getpass +import os +from pathlib import Path +import subprocess +import sys + +from ansible.module_utils.six.moves import configparser +from ansible.plugins.vars import BaseVarsPlugin + + +DOCUMENTATION = """ + module: pass + vars: vault + version_added: 2.9 + short_description: Load vault passwords from pass + description: + - Works exactly as a vault, loading variables from pass. + - Decrypts the YAML file `ansible_vault` from cranspasswords. + - Loads the secret variables. + - Makes use of data caching in order to avoid calling cranspasswords multiple times. + - Uses the local gpg key from the user running ansible on the Control node. +""" + + + +class VarsModule(BaseVarsPlugin): + @staticmethod + @lru_cache + def decrypt_password(name, crans_submodule=False): + """ + Passwords are decrypted from the local password store, then are cached. + By that way, we don't decrypt these passwords everytime. + """ + # Load config + config = configparser.ConfigParser() + config.read(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'pass.ini')) + + password_store = Path(config.get('pass', 'password_store_dir', + fallback=os.getenv('PASSWORD_STORE_DIR', Path.home() / '.password-store'))) + + if crans_submodule: + password_store /= config.get('pass', 'crans_password_store_submodule', + fallback=os.getenv('CRANS_PASSWORD_STORE_SUBMODULE', 'crans')) + full_command = ['gpg', '-d', password_store / f'{name}.gpg'] + proc = subprocess.run(full_command, capture_output=True, close_fds=True) + clear_text = proc.stdout.decode('UTF-8') + sys.stderr.write(proc.stderr.decode('UTF-8')) + return clear_text + + @staticmethod + @lru_cache + def become_password(entity): + """ + Query the become password that should be used for the given entity. + If entity is the whole group that has no default password, + the become password will be prompted. + The configuration should be given in pass.ini, in the `pass_become` + group. You have only to write `group=pass-filename`. + """ + # Load config + config = configparser.ConfigParser() + config.read(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'pass.ini')) + if config.has_option('pass_become', entity.get_name()): + return VarsModule.decrypt_password( + config.get('pass_become', entity.get_name())).split('\n')[0] + if entity.get_name() == "all": + return getpass("BECOME password: ", stream=None) + return None + + def get_vars(self, loader, path, entities): + """ + Get all vars for entities, called by Ansible. + + loader: Ansible's DataLoader. + path: Current play's playbook directory. + entities: Host or group names pertinent to the variables needed. + """ + # VarsModule objects are called every time you need host vars, per host, + # and per group the host is part of. + # It is about 6 times per host per task in current state + # of Ansible Crans configuration. + + # It is way to much. + # So we cache the data into the DataLoader (see parsing/DataLoader). + + passwords = {} + + for entity in entities: + # Load vault passwords + if entity.get_name() == 'all': + passwords['vault'] = loader.load( + VarsModule.decrypt_password('ansible_vault', True)) + + # Load become password + become_password = VarsModule.become_password(entity) + if become_password is not None: + passwords['ansible_become_password'] = become_password + + return passwords