Ansible Vault in Practice: File vs Variable Encryption and Multi-Environment Keys
Choosing between file-level and variable-level Ansible Vault encryption, wiring vault IDs into CI/CD without leaking secrets, and the precedence and logging traps that bite in production.
28 Jun 2026, 00:35 UTC

You need to commit database passwords to the same git repository as your playbooks, and you need different credentials for dev and prod. The decision that matters is not whether to use Ansible Vault — it is which encryption granularity to use, and how to wire vault IDs into your pipeline without leaking secrets into logs.
The short version: encrypt individual variables with ansible-vault encrypt_string and a per-environment vault ID, commit the ciphertext to git, and inject the vault password from your CI secret store at runtime. The details below explain why, and where this pattern breaks down.
File-level vs variable-level encryption
Vault supports two modes. ansible-vault encrypt group_vars/prod.yml encrypts the entire file. It is simple, but every change requires ansible-vault edit, diffs in pull requests are useless, and reviewers cannot see which variables exist — only that something changed.
ansible-vault encrypt_string encrypts a single value, producing an inline !vault | block you paste into a YAML file next to plaintext variables:
# group_vars/prod/db.yml
db_host: db.internal.example.com
db_user: app
# ansible-vault encrypt_string --vault-id prod@prompt 's3cr3t' --name db_password
db_password: !vault |
$ANSIBLE_VAULT;1.2;AES256;prod
66386439653...(ciphertext truncated)The header line embeds the vault format version, cipher, and the vault ID label (prod). Reviewers see variable names and structure; only values are hidden. The trade-off: names themselves leak information. A variable called aws_rds_master_password tells anyone with repo access what infrastructure you run. Treat variable names as semi-public.
Vault IDs for per-environment keys
Since Ansible 2.4, vault IDs let you label keys — dev, prod — and encrypt different variables with different passwords in the same repository. At runtime you supply the matching passwords:
ansible-playbook deploy.yml \
--vault-id dev@~/.vault_pass_dev \
--vault-id prod@~/.vault_pass_prodRun this from your control node or CI runner; the password files must exist there and be readable by the Ansible process user. The @prompt suffix (--vault-id prod@prompt) makes Ansible ask interactively, which is right for laptops but not for pipelines.
The sharp edge: the vault ID label embedded in each encrypted value must match a label passed at runtime. If a variable was encrypted with --vault-id prod@... and you only pass --vault-id production@..., decryption fails with an error that does not always name the offending variable. Standardize labels in a team convention document and lint for them in CI if you can.
Precedence still applies to encrypted values
Encryption does not change variable precedence. An encrypted db_password in group_vars/all.yml is still overridden by a plaintext -e db_password=... extra var at the command line, because extra vars outrank inventory and group vars. During incident response this causes real confusion: someone "fixes" a password by editing the vault file, but a stale extra var in a CI job definition silently wins. When a secret change appears not to take effect, check for -e overrides and CI-level variable injection before rekeying anything.
CI/CD integration without leaking secrets
Store the vault password in your CI system's secret store (GitHub Actions secrets, GitLab masked CI variables, Jenkins credentials). At job runtime, write it to a temporary file and point Ansible at it:
# inside the CI job, as the CI user
echo "$VAULT_PASSWORD_PROD" > "$RUNNER_TEMP/.vault_pass"
chmod 600 "$RUNNER_TEMP/.vault_pass"
ansible-playbook deploy.yml --vault-id prod@"$RUNNER_TEMP/.vault_pass"
rm -f "$RUNNER_TEMP/.vault_pass"Never echo the password into a script file or pass it on a command line where it lands in shell history or process listings. A file with 600 permissions in a job-scoped temp directory is the pragmatic minimum.
Second leak vector: Ansible's own logs. Vault decrypts values in memory; if a task prints one via debug and you run with -v, the plaintext can appear in output. Mark any task that touches secrets:
- name: Configure database credentials
template:
src: db.cnf.j2
dest: /etc/myapp/db.cnf
mode: '0600'
no_log: trueNote that no_log: true also suppresses useful diff output, so apply it narrowly — per task, not per play.
Limitations worth planning for
Key rotation is the painful part. ansible-vault rekey changes the password for a file, but with variable-level encryption scattered across dozens of files you need to script re-encryption; there is no single "rotate everything" command. Also note that files encrypted by very old Ansible versions used a weaker key derivation function; Ansible 2.9+ strengthened it, and old files still decrypt but may need rekeying for compliance. Check your version with ansible --version.
Verify before you trust it
Before rolling this out, run three checks on a test branch: encrypt one variable with each vault ID and confirm a playbook run with only one --vault-id fails on the other; run a debug task with and without no_log: true under -v and inspect the log; and run ansible-vault view --vault-id prod@prompt group_vars/prod/db.yml to confirm the header shows the expected label and cipher. If all three behave as expected, your pipeline is safe to automate.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.