Ansible Vault Best Practices
This chapter revolves around best practice and common patterns of using Ansible Vault — genuinely common, practical ways Vault gets used in real projects, plus a direct return to the exact question this whole section opened with.
Pattern: Never Committing The Password — .gitignore
# .gitignore
.vault_pass.txt
*.vault_passSimple, but a genuine safety net rather than just documentation of intent — even with the best of intentions, a careless git add . shouldn’t be able to accidentally stage a password file. Let .gitignore enforce what good habits alone shouldn’t have to.
Also the best thing even above .gitignore is simply not putting secret file on your repository directory — keep it elsewhere with restrictive 400 or 600 permission.
Pattern: Choosing Whole-File vs. encrypt_string
- Whole-file encryption (
ansible-vault create/encrypt) fits a file that’s mostly or entirely sensitive — a dedicatedsecrets.yamlwith nothing else in it. Simpler workflow, one password unlocks everything inside. encrypt_stringfits a file that’s primarily plain configuration, with just one or a few genuinely sensitive values mixed in. Keeps the rest of the file readable and diffable in git, rather than hiding non-sensitive settings behind encryption for no real benefit.
Pattern: Rebuilding The env-Based Secret, Properly
This is the direct payoff promised at the very start of this section. Recall the lookups section’s pattern:
# From the lookups section — depends on the control node's own environment
vars:
db_password: "{{ lookup('env', 'DB_PASSWORD') }}"
tasks:
- name: Fail if the secret is missing
ansible.builtin.fail:
msg: "DB_PASSWORD is not set on the control node"
when: db_password | length == 0Rebuilt properly with Vault:
vars_files:
- secrets.yaml # encrypted, containing db_password
tasks:
- name: Use the secret
ansible.builtin.template:
src: db.conf.j2
dest: /etc/myapp/db.confNo dependency on someone having manually exported DB_PASSWORD on whatever machine happens to run this playbook, and no emptiness check needed at all — the secret genuinely lives in the repository, encrypted, reproducible by anyone with the vault password from a completely clean clone, with nothing separate to set up or remember.
Pattern: Separate Encrypted Files Per Environment
Combining vars_files with --vault-id, for genuinely different secrets per environment:
vars_files:
- "secrets_{{ deployment_env }}.yaml"ansible-vault encrypt secrets_dev.yaml --vault-id dev@prompt
ansible-vault encrypt secrets_prod.yaml --vault-id prod@promptansible-playbook site.yaml -e "deployment_env=prod" \
--vault-id dev@~/.vault_pass_dev.txt \
--vault-id prod@~/.vault_pass_prod.txtEach environment’s secrets are protected by their own password, loaded based on which environment the run actually targets.
Pattern: An Encrypted Default In A Role
Combining the roles section’s defaults/main.yml with !vault directly:
app_port: 8080
db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
...A role can ship with a sensible, encrypted default password — for a development or testing environment, say — while remaining genuinely overridable by anyone applying it who needs a different value for production, exactly per the sensitivity-versus-configurability distinction from earlier in this section.
Pattern: A Secret Flowing Into A Template
Combining directly with template from the common modules section:
password = {{ db_password }}- name: Deploy db config with secret
ansible.builtin.template:
src: db.conf.j2
dest: /etc/myapp/db.conf
mode: '0600'mode: '0600' here matters for exactly the reason permissions have mattered throughout this course — the rendered file, sitting on the remote host, now contains a genuine decrypted secret. Protecting the vault password locally means nothing if the file it ultimately produces is left world-readable on the server it was deployed to.
Pattern: Vault Passwords From A Real Secrets Manager
Expanding on the executable-script option from earlier in this section — the standard way Vault integrates with CI/CD:
#!/usr/bin/env bash
# get_vault_pass.sh
curl -s https://secrets.example.com/api/vault-passwordchmod +x get_vault_pass.sh
ansible-playbook site.yaml --vault-password-file ./get_vault_pass.shAnsible recognizes an executable file passed to --vault-password-file and runs it, using its output as the password, rather than reading it as static content. This is how Vault genuinely integrates with a real secrets platform in an automated pipeline — the actual vault password is never stored at rest on the CI runner at all.
A Pre-Commit Safety Checklist
- Does
git diffshow a$ANSIBLE_VAULTheader — not plaintext — for every secrets file about to be committed? - Is the vault password file itself covered by
.gitignore, and confirmed absent fromgit status? - If
ansible-vault decryptwas used for any reason, was the file re-encrypted before committing? - Is the correct
--vault-idlabel being used for the environment this specific secret actually belongs to?
Best Practices
- Never commit a vault password, only vault-encrypted content — enforce this with
.gitignore, not just discipline alone. - Choose whole-file encryption for dedicated secrets files,
encrypt_stringfor mostly-plaintext files with a few sensitive values — match the tool to how much of the file actually needs protecting. - Reach for Vault, not
env, the moment a secret needs to be part of your reproducible, version-controlled repository — this was this section’s entire opening argument, now fully demonstrated in practice. - Set restrictive permissions on any file that ends up containing a decrypted secret, on the remote host as much as the control node — protecting the vault password means little if the deployed result is left exposed.