Secrets and Vault¶
What You'll Learn¶
- Every
ansible-vaultcommand you'll use, and when - Whole-file encryption vs.
encrypt_string, and thevault_indirection pattern - How vault IDs separate environments so one leaked password doesn't expose everything
- How to supply vault passwords safely in CI
- Vault's real limitations, and when an external secret manager is the better answer
Why This Exists¶
Secrets need to exist somewhere — playbook repos, CI systems, control nodes — without being readable by anyone who shouldn't have them. Vault is Ansible's built-in answer; an external secret manager is often the better one for larger teams.
Mental Model¶
Ansible Vault is symmetric encryption (AES-256) for files and values in your repository. Anyone with the vault password can decrypt; anyone without it sees ciphertext. Ansible decrypts transparently at run time, in memory, whenever a play reads an encrypted value.
The Commands¶
ansible-vault create group_vars/production/vault.yml # new encrypted file, opens $EDITOR
ansible-vault edit group_vars/production/vault.yml # decrypt → edit → re-encrypt
ansible-vault view group_vars/production/vault.yml # read-only
ansible-vault encrypt files/tls/shop.key # encrypt an existing file in place
ansible-vault decrypt files/tls/shop.key # permanently decrypt (rarely what you want)
ansible-vault rekey group_vars/production/vault.yml # change the password
Running a playbook that uses encrypted content:
ansible-playbook site.yml --ask-vault-pass
ansible-playbook site.yml --vault-password-file ~/.vault/pass.txt
Pattern 1: Encrypted File With Indirection¶
Encrypting a whole group_vars file hides variable names too, so nobody can search for where db_password is defined. The standard fix is two files:
inventories/production/group_vars/all/
├── vars.yml # plaintext, readable, searchable
└── vault.yml # encrypted
vault_db_password: "S3cure-and-long"
vault_api_token: "tok_live_8c1f..."
Tasks and roles only ever reference db_password. Reviewers can see that a secret is used and where it comes from, without seeing its value.
Pattern 2: encrypt_string for Single Values¶
db_host: db01.internal.example.com
vault_db_password: !vault |
$ANSIBLE_VAULT;1.2;AES256;prod
6231643965383464303433383237366234...
Good for a handful of secrets in otherwise plaintext files. Downside: ansible-vault edit can't edit inline values — you re-run encrypt_string and paste the result.
Keep secrets out of shell history
Omit the plaintext argument and use --stdin-name, then type or pipe the value: ansible-vault encrypt_string --vault-id prod@prompt --stdin-name vault_db_password.
Vault IDs: Separate Trust Domains¶
One password for everything means a staging leak decrypts production. Label each encrypted item with a vault ID and use a different password per ID:
ansible-vault create --vault-id staging@prompt inventories/staging/group_vars/all/vault.yml
ansible-vault create --vault-id prod@prompt inventories/production/group_vars/all/vault.yml
The ID is written into the header ($ANSIBLE_VAULT;1.2;AES256;prod). At run time, supply only the passwords that environment needs:
# A staging run: the production password is never present
ansible-playbook -i inventories/staging site.yml --vault-id staging@~/.vault/staging.txt
# A production run
ansible-playbook -i inventories/production site.yml --vault-id prod@~/.vault/prod.txt
Configure defaults in ansible.cfg, so nobody has to remember flags:
[defaults]
vault_identity_list = staging@~/.vault/staging.txt, prod@~/.vault/prod.txt
vault_id_match = True
vault_id_match = True makes Ansible try only the password whose ID matches the header, instead of trying every password on every value.
Vault Passwords in CI¶
Never commit the password, and never pass it on the command line. Store it in the CI system's secret store and write it to a short-lived file:
- name: Deploy to production
env:
VAULT_PROD_PASSWORD: ${{ secrets.VAULT_PROD_PASSWORD }}
run: |
umask 077
printf '%s' "$VAULT_PROD_PASSWORD" > "$RUNNER_TEMP/vault-prod"
ansible-playbook -i inventories/production site.yml \
--vault-id "prod@$RUNNER_TEMP/vault-prod"
rm -f "$RUNNER_TEMP/vault-prod"
Alternatively, point --vault-id prod@scripts/vault-pass.sh at an executable script that prints the password to stdout — for example by fetching it from a cloud secret manager. Ansible runs executable password files instead of reading them.
Keeping Decrypted Values Out of Logs¶
Decryption is transparent, so a decrypted value appears in output like any other variable unless you hide it:
- name: Create the database user
community.postgresql.postgresql_user:
name: checkout
password: "{{ db_password }}"
no_log: true
no_log: true hides the task's arguments and results even at -vvv. Full logging hygiene is in Security.
Rotating a Secret¶
ansible-vault editand change the value, then commit.- Run the playbook that applies the new credential to the system and its consumers.
- Revoke the old credential in the system that issued it.
Rotating the vault password is ansible-vault rekey --vault-id prod@old.txt --new-vault-id prod@new.txt <files>. Old commits in Git history remain decryptable with the old password — rekeying doesn't protect secrets that were already exposed. If the password leaked, rotate the secrets themselves.
Limitations, and When to Use an External Secret Manager¶
| Ansible Vault | External manager (HashiCorp Vault, AWS Secrets Manager, ...) |
|---|---|
| Shared symmetric password per vault ID | Per-identity access policies |
| No audit log of who decrypted what | Access audit logs |
| Rotation = edit, commit, re-run | Automatic rotation; dynamic, short-lived credentials |
| Ciphertext lives in Git history forever | Nothing secret in the repository |
| Zero extra infrastructure | Another service to run or pay for |
Vault is a solid choice for small teams and bootstrap secrets. As a team grows, fetch secrets at run time with a lookup instead of committing any encrypted value at all — see Lookup and Filter Plugins. A common hybrid: the only thing in Ansible Vault is the credential Ansible uses to authenticate to the external manager.
Full worked example¶
See Case Study: Vault Secrets.
Common Mistakes¶
- Passing a secret via
-eon a CI command line, where it can land in shell history or process listings — see Security. - One vault password for every environment, so a staging compromise exposes production secrets too.
- Forgetting
no_log: trueon a task that decrypts and uses a secret, leaking it into readable output. - Encrypting whole vars files without the
vault_indirection, so nobody can find where a variable is defined. - Committing a decrypted file after
ansible-vault decryptand forgetting to re-encrypt — add a pre-commit check that rejects unencryptedvault.ymlfiles. - Assuming
rekeymakes previously leaked ciphertext safe.
Interview Questions¶
- How does Ansible Vault protect secrets, and what are its limitations compared to an external secret manager?
- Why would a team use multiple
--vault-ids instead of one shared vault password? - How would you give a CI pipeline the vault password without committing it or exposing it in logs?
- Why pair an encrypted
vault.ymlwith a plaintextvars.yml?
Next¶
Continue to Security. When secrets outgrow Ansible Vault, see Secrets Management With HashiCorp Vault.