Integrating Ansible Vault with pass Password Manager

· cyclicircuit's blog

How to effectively integrate ansible-vault secrets management with a CLI password manager like pass.

Table of Contents

Ansible Vault is a pretty basic system for managing secrets, and overall, this is a very good thing because at the end of the day, all those secret variables are just encrypted with AES-256 which is simple, reliable, and secure. However, this simplicity leaves us with a few user-interface annoyances that, I feel, are relatively easy to overcome, but not obvious from merely reading the documentation.

Most notably, we shouldn't have to worry about secrets when crafting an incantation to run a playbook, we should just run the playbook, and if we need to be prompted to unlock our password manager, we should unlock it. Otherwise, the system should basically "just work" with the CLI password manager of our choice. The alternative is that we keep copy-pasting the key to our Ansible Vault into our commandline, leaving secret keys strewn throughout our bash history.

Required Reading:

To configure Ansible with our CLI password manager of choice, there are three key steps. Once you've performed those key steps, you can just run your ansible incantations normally, like ansible-playbook production.yaml and it will just prompt you for a password when it needs it, and not prompt you for one when it doesn't.

1) ansible.cfg #

───────────────────────────────────────────────────────────────────────
[defaults]
inventory = inventory.yaml
stdout_callback = yaml
bin_ansible_callbacks = True
roles_path = roles
# library = library
pipelining = True
vault_identity_list = encrypted_vars@contrib/vault/vault-pass-client.py

The important bit is the last line, vault_identity_list, this is one of those "blink and you'll miss it" elements of the Ansible documentation that makes the whole thing work. Ansible will run that path as a script, and encrypted_vars (the thing before the @) will be passed into the script. The STDOUT of the script will be used as the password, which works REALLY WELL with most CLI password managers.

2) contrib Script #

However, as you may have noticed, we need a script to actually do this. The script should live in contrib/vault/vault-pass-client.py (though the observant among you may notice that ANY scripting language will work). The script is a very thin wrapper around your CLI password manager of choice, in this case pass:

 1#!/usr/bin/env python3
 2from argparse import ArgumentParser
 3from subprocess import run
 4from sys import argv
 5
 6parser = ArgumentParser(description='Integrates vault password storage with pass (https://www.passwordstore.org/)')
 7parser.add_argument('--vault-id', help='The vault ID to retrieve')
 8args = parser.parse_args()
 9
10run(['pass', 'show', f'ansible/{args.vault_id}'], shell=False, check=True)

Technically, you don't need anything other than a couple imports and the last line, cause you can just use the first argument as the vault ID and that's it. I decided to use the ArgumentParser purely for documentation purposes. All this is doing is running pass show ansible/encrypted_vars where encrypted_vars is the thing that Ansible passes in (this allows multiple vaults to be used).

This kind of script should be trivially adaptable to any other CLI password manager.

3) Variables #

Now that we have the scrpt and Ansible configuration set up, we need to know how to use the secrets in variables easily. Assuming that your secrets are in a YAML file and you edit them as described here, the dictionary structure of that file should be asessible in your variable definitions.

For example, I have a Zammad deployment which uses a postgreSQL database and needs a password which is stored in host_vars/encrypted/prod-vault.yaml (there is also an equivalent stag-vault.yaml.

The file looks like this:

1postgres_db_passwords:
2  zammad:
3    zammad_ro: XXXXXXXXXXXXXXXXXXXXXXXXX

Where zammad_ro refers to the read-only password that Zammad can use. Now, in the actual variables file, I can access those encrypted variables like so:

 1all:
 2  vars:
 3    ansible_user: root
 4    ansible_python_interpreter: /usr/bin/python3
 5  children:
 6    production:
 7      vars:
 8        unattended_upgrades:
 9          automatic_reboot: "true"
10          reboot_time: "07:30"
11        zammad:
12          ro_password: "{{ vault.postgres_db_passwords.zammad['zammad_ro'] }}"

There is a similar set of variables for the staging environment.

In order to load the different variables for the relevant environment, we simply have a pre-task in our production and staging playbooks, like so:

production.yaml

 1- hosts: production
 2  pre_tasks:
 3  - name: Loading Encrypted Keys for Production Hosts
 4    include_vars:
 5      file: "{{ inventory_dir }}/host_vars/encrypted/prod-vault.yaml"
 6      name: vault
 7    tags: [always]
 8
 9- hosts: zammad-prod
10  roles:
11  - zammad

staging.yaml

 1- hosts: staging
 2  pre_tasks:
 3  - name: Loading Encrypted Keys for Staging Hosts
 4    include_vars:
 5      file: "{{ inventory_dir }}/host_vars/encrypted/stag-vault.yaml"
 6      name: vault
 7    tags: [always]
 8
 9- hosts: zammad-staging
10  roles:
11  - zammad

This way, when you go to deploy production or staging, the right set of secrets will be loaded and the ansible vault will be unlocked using your password manager, either prompting you for a password if this is the first time in a while, or not bothering you at all!

last updated: