From 7f39ea724a460200f34cd6eec0c6eb16e870a205 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Thu, 8 Apr 2021 10:48:05 +0200 Subject: [PATCH] Read become password from pass Signed-off-by: Yohann D'ANELLO --- ansible.cfg | 4 +- vars_plugins/__pycache__/pass.cpython-39.pyc | Bin 0 -> 3400 bytes .../vault_cranspasswords.cpython-39.pyc | Bin 0 -> 4018 bytes vars_plugins/pass.ini | 6 ++ vars_plugins/pass.ini.example | 6 ++ vars_plugins/pass.py | 102 ++++++++++++++++++ 6 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 vars_plugins/__pycache__/pass.cpython-39.pyc create mode 100644 vars_plugins/__pycache__/vault_cranspasswords.cpython-39.pyc create mode 100644 vars_plugins/pass.ini create mode 100644 vars_plugins/pass.ini.example create mode 100644 vars_plugins/pass.py 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 0000000000000000000000000000000000000000..cd637fa43e2c4f0bc189283a4fbcbbcc29109777 GIT binary patch literal 3400 zcmai0&2Hn!5$2yLik4-M$BQI8*+C;&WFf?sfglMIV>TF%?b!@6{>j+h#S#f3=LX^#Z?G;^I7NDx3e<_&VrWAq^ikW*e^FW#!A{?-T(3T!p2tE;QKzOSmI2b2=`lC;yGy|m^s7Pr3xj&J_THd1w)D;pmZ^PWH2wd=ibm5^wUifO z9K{_5AJt1Hdyx=_(07g_*@6FhoM-LivKMi|ID9u=M1p+@U&m>2nPkzwOvCUAJl&iY zDcdG!Knrd_X`9?cMJmg0#zm56VMHmT+vFFm-)gbwP+LnMhz(sJC-oIumn{;&!!zi&k(0r3nG{n4GV=sC5usgByil zBySTM$%tSe(8(ktuxG}J%t>^UClp4~6i2?lCh5ZACOvRaCS4{Ljh_jyjhjewm`g5u zm*krDOUF?R%W}etEJLf4-H|-gQ+D%A@;oJ3PFc&35h{yM1wNj`D}D_PGXj$ufyHd* z&>FR<{oZ^Bzo4Qe4z0X5^tVf0T77Q-SUm)w@)1M_8Z<9s3oK2aDs*b_?!qhn0gW_9 z=GeHihStcE_LVcL=+@P3ReED{Y*KS#wV`$8qZ`s5Em23-M)fhUL+jSu!+Wmvgd z8LiTlv3cjpM^|g3#~@iNCFsY{g-xyw{xXBs@f0T{;*8J|W{l1QQpRZtCn99d*#?Fq z(=#+j={&+O`Y;wravSwG$SqjRv(MzzE=~hjO^pE~ZW!+)M&<^-^>DbAWQlUH7pzUy zL%nuL6CSJ`@0^@`b@XO0JUKmjvmfrgdNWvQO%*q_A|wT`4e-2(;_Kk42530dn3xj9 zMVTIgr@L=<4o|`dW>3yu931VPz1|NhdVXL7UI*4D9XKuMoi;7ih@+k?I1BSa_6n(L zaheMjwy79Y&Q4!${-(Lk5tynD=!JxJtP_@UIE$#Va)Fza9pzl*Nv5i>w=C*1<-u^O z2fQ#uriUTNtgLKEh02A7WjD&{)7hu^}-gu~|0{Qn#V z9Y`cYW8{y4wfs+VX;cT<@?17HM-6HN>zyANCq}7BE7um6as|^2^*&f9Q2l^|wdiPC zp|C{!u>d0IBzFNp>&`h+vI&Y&ky3J@^COTmhZGFS@-n#r8p8P|vOj0gtM)`hk}}oa zc5+C?mpm_erMV7NpezSgwy_G%__#643TXVHBezMKq6ZNAZcnlW&YyyI37djPSSrcf z-_k`W96H3n>J6+SX#X4~VY$lrY-R0bi+r6Iq!Zmh3Cf^gLb=hfCFgqhbDX(}v_K4> z&v(u}2x+jiU|5+xM*t`rGQr@{i~ZfBgZ?E(1W-drp|x(3b@$Ef;;VQ(jwI{%jiFgBahQj(jG_9^5?e z!ov7Gz?c%Z$!@_pSS7tQ>VqQwUF7*Sf$Yg*s80)Am@d9eexC~ox0eYGFjg>=ahYUj z3J`{v1g9sru#7Rd4$fERKZ9cnmM7V(yljWp#=Jytb@vA)9bpMyHbIS)ajafc`U3I+wj(pYsY56}TQ^4@cN(=&Z* z@c2tmDep8bL@Lez*&{dEB z5an0})YHj9w|p=!bm#Qd(P0^p b|KYsyo|Oyy4qXz+d?cKwUp(Hr?W}wU%7V}D literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fd89940f0530a4eda6a4e1901eb75ffe55ec75d3 GIT binary patch literal 4018 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 Pkv;ybRggXYENA7vl`v`l literal 0 HcmV?d00001 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