The Two-File Pattern
The most important convention: keep encrypted secrets in a file named vault.yml
and reference them from a plain vars.yml using indirection. This way every secret
in your playbooks is read from vars.yml, but the actual value lives encrypted in
vault.yml.
YAML — group_vars/all/vault.yml (encrypted)
---
# Encrypted file — every key starts with vault_ for clarity
vault_mysql_root_password: "ChangeMeNow123!"
vault_appuser_password: "AnotherSecret456!"
vault_api_token: "tok_aBc123XyZ"
vault_tls_private_key: |
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAA...
-----END PRIVATE KEY-----
YAML — group_vars/all/vars.yml (plain — indirection)
---
# Plain file — playbooks reference these names
mysql_root_password: "{{{{ vault_mysql_root_password }}}}"
appuser_password: "{{{{ vault_appuser_password }}}}"
api_token: "{{{{ vault_api_token }}}}"
tls_private_key: "{{{{ vault_tls_private_key }}}}"
# Plain settings live here too — no encryption needed
mysql_port: 3306
backup_retention_days: 14
💡 Tip: Why the indirection? Three benefits: (1) playbooks reference one consistent name regardless of whether the source is encrypted, (2)
grep'ing for usages works without decrypting, (3) you can move a value between encrypted/plain by editing only vault.yml + vars.yml — never the playbooks. What's Committed, What's Not
INI — .gitignore
# Vault password files — NEVER commit these
.vault_pass
.vault_pass_*
*.vault_pass
# Decrypted backups, accidental dumps
*.tmp
*.decrypted
# Local CI scratch
.molecule/
*.retry
# SSH private keys
id_rsa
id_ed25519
*.pem
Where to Put the Vault Password
| Storage | Pros | Cons |
|---|---|---|
Plain file in ~/.ansible/vault_pass | Simple, no auth | Anyone with shell access can read it |
| 1Password / Bitwarden CLI | Audit log, MFA, easy rotation | Requires CLI tools on every dev box |
| HashiCorp Vault (different "vault") | Centralised, audited, dynamic secrets | Infrastructure to run |
| AWS Secrets Manager | If you're already in AWS | Cloud lock-in |
| CI variable (GitLab/GitHub) | Native, encrypted at rest | One copy per CI provider |
BASH — vault password from 1Password CLI
# Create a script that prints the password to stdout
cat > scripts/get_vault_pass.sh << 'EOF'
#!/usr/bin/env bash
op item get "Ansible Vault Production" --fields password
EOF
chmod +x scripts/get_vault_pass.sh
# Use it
ansible-playbook site.yml --vault-password-file scripts/get_vault_pass.sh
# Or in ansible.cfg
[defaults]
vault_password_file = ./scripts/get_vault_pass.sh
CI/CD Integration
YAML — GitLab CI example
---
deploy_production:
stage: deploy
image: quay.io/ansible/creator-ee:latest
variables:
ANSIBLE_HOST_KEY_CHECKING: "False"
before_script:
# vault password comes from a CI/CD masked variable
- echo "$VAULT_PASS_PROD" > .vault_pass_prod
- chmod 600 .vault_pass_prod
# SSH key likewise
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
- chmod 600 ~/.ssh/id_ed25519
script:
- ansible-playbook site.yml \
-i inventories/production \
--vault-id prod@.vault_pass_prod
after_script:
# Always wipe the password file at the end of the job
- rm -f .vault_pass_prod
only:
- main
YAML — GitHub Actions example
---
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup SSH key
run: |
mkdir -p ~/.ssh
echo "${{{{ secrets.SSH_PRIVATE_KEY }}}}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
- name: Setup vault password
run: |
echo "${{{{ secrets.VAULT_PASS_PROD }}}}" > .vault_pass_prod
chmod 600 .vault_pass_prod
- name: Run playbook
run: |
ansible-playbook site.yml \
-i inventories/production \
--vault-id prod@.vault_pass_prod
- name: Cleanup
if: always()
run: rm -f .vault_pass_prod
Rotating a Compromised Vault Password
If a vault password leaks, here's the recovery procedure:
- Generate a new password. Use
openssl rand -base64 32for strong randomness. - Re-encrypt every file with the new password.
- Update the password in your secret store (1Password, CI variable, etc.).
- Rotate the underlying secrets the vault contained (the leaked password may have been used to read them).
BASH — bulk rekey
# Find every vault-encrypted file in the repo
find . -type f -name "*.yml" -exec sh -c \
'head -1 "$1" | grep -q "ANSIBLE_VAULT" && echo "$1"' _ {{}} \;
# Rekey each one
for f in $(find . -type f -name "*.yml" -exec sh -c \
'head -1 "$1" | grep -q "ANSIBLE_VAULT" && echo "$1"' _ {{}} \;); do
ansible-vault rekey \
--vault-id old@~/.vault_pass_old \
--new-vault-id new@~/.vault_pass_new \
"$f"
done
# Commit the re-encrypted files
git add -A && git commit -m "Rotate vault password $(date +%Y-%m-%d)"
⚠ Warning: After a leak, treat every secret inside the vault as also compromised. Rotate database passwords, API keys, and TLS keys — not just the vault password itself. The cost of over-rotating is small; the cost of trusting a leaked vault is enormous.
Hard Rules
- Never commit a
.vault_passfile or any unencrypted credentials. - Never echo a vault password into shell history. Use file-based access or stdin.
- Always use different vault passwords per environment (dev/staging/prod).
- Always wipe vault password files at the end of CI jobs (
rm -finafter_script). - Audit who has each environment's vault password — same group of people who have prod SSH access, no more.
✅ Tip: Vault is plumbing — invisible when done right, painful when sloppy. The two-file pattern + rigorous .gitignore + per-environment passwords + CI rotation discipline covers 95% of the practical risk.