← All TILs · ansible

Secrets in the inventory: vault.yml next to vars.yml, with plaintext aliases

ansible - 2026-10-03

Tenth entry in the Ansible inventory from scratch series. Item 8 put desired state in the inventory's group_vars/ and host_vars/, and some of that state is secret: a database password, an API token. Ansible Vault encrypts it so the repository holds ciphertext, decrypted at run time with a vault password. What Ansible Vault actually encrypts explains the tool itself, the two ways to encrypt, and where its protection stops. This entry stays in the inventory: it compares three layouts for one PostgreSQL password, measures what each reveals to a search, to ansible-inventory, to a playbook's output and to git diff, then splits prod and staging secrets with vault IDs. Everything below ran with ansible-core 2.21.4.

Three layouts for one password

The vault password in the example is a committed test value, encrypting dummy data, so the example runs in CI. A real vault password never goes in the repository: a password manager, a CI secret or a vault client script holds it.

What each layout reveals

Question Alias No alias Inline
grep -r postgresql_password finds vars.yml nothing vars.yml
ansible-inventory --host db1, no vault password fails, exit 4 fails, exit 4 works; the value is an encrypted blob
ansible-inventory --host db1, with the password the secret in clear, as vault_postgresql_password the secret in clear still an encrypted blob

The playbook output

A task that prints the password, msg: "password is {{ postgresql_password }}", printed it in clear: the alias resolves at run time like any variable. The same task with no_log: true printed only ok: [db1]. Vault protects the file; no_log is what protects the output of every task that uses the secret.

What git sees

Changing the password in the encrypted vault.yml, decrypted, edited and re-encrypted, changed 6 of its 7 lines: everything but the header, because each encryption uses a new random salt. ansible-vault rekey, which re-encrypts with a new vault password, did the same with the value unchanged. A reviewer sees that vault.yml changed and nothing more, which is why the alias layout keeps every non-secret setting in vars.yml, where diffs stay readable. In the inline layout, only the !vault block changes; the lines around it stay plaintext, though an inline value can't be rekeyed in place, as the vault entry explains.

Prod and staging with vault IDs

A vault ID labels encrypted content with the name of the password that encrypted it: ansible-vault encrypt --vault-id prod@<password file> wrote the header $ANSIBLE_VAULT;1.2;AES256;prod. With prod and staging each in its own group_vars/<env>/vault.yml:

Passwords given Hosts Result
--vault-id prod@… --vault-id staging@… all exit 0
--vault-id prod@… --limit prod exit 0
--vault-id prod@… all exit 4, "Decryption failed (no vault secrets were found that could decrypt)"

Ansible decrypts the files of the hosts it targets: with --limit prod, the staging file was never read, so its password wasn't needed. A run on every host fails at the start without each environment's password, before any task, which is the safe way round. Item 5's separate inventory directories get the same separation from -i alone, with one vault password per directory.

In short

The example repository

The series' companion repository, abdelhousni/ansible-inventory-series, holds the three layouts, the vault ID inventory and their test passwords. run.sh greps, runs ansible-inventory with and without the password, runs a playbook that uses the secret with and without no_log, changes and rekeys a copy of vault.yml in its out/ directory, and runs the vault ID cases; it prints whether the secret appears in clear, never the ciphertext. Its CI runs it on every push and compares the output with the expected one.

Sources

Created 2026-10-03T12:06:38+02:00 · Edit