Module 07 — Secrets & Ansible Vault
Updated 20 August 2026
Keeping passwords, keys and tokens out of Git without making automation impossible. Module 06 left you with a group_vars/vault.yml sitting there in plaintext — this module fixes that.
🧠 concept → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (answers hidden)
Prerequisite: Modules 01–06.
Part A · Ansible Vault fundamentals
A1 · What Vault is, and what it is not
Ansible Vault is that diary lock: good at the job it has, and honest about the jobs it does not do. The third limit is the one that matters most in practice — when someone leaves the team, you have to rotate the actual passwords, not just the vault password, because they already read the pages.
Ansible Vault is symmetric AES256 encryption for files in your repository. One password encrypts, the same password decrypts. That is the entire model.
| Vault is | Vault is not |
|---|---|
| Encryption at rest — safe to commit to Git | A secrets manager — no rotation, no audit log, no access policy |
| Built in, no infrastructure to run | Per-user access control — one password, everyone who has it sees everything |
| Transparent at run time — playbooks just work | Protection at run time — values are plaintext in memory and can leak to logs |
| Good enough for most teams | A substitute for HashiCorp Vault or AWS Secrets Manager at scale |
Saying this out loud in an interview is worth more than reciting the commands.
A2 · The core commands
ansible-vault create secrets.yml # create a new encrypted file in $EDITOR
ansible-vault edit secrets.yml # ⭐ decrypt, open in $EDITOR, re-encrypt on save
ansible-vault view secrets.yml # ⭐ print decrypted, do not modify
ansible-vault encrypt existing.yml # encrypt a file that already exists
ansible-vault decrypt secrets.yml # ⚠️ permanently decrypt on disk
ansible-vault rekey secrets.yml # ⭐ change the password
ansible-vault encrypt_string 'value' # ⭐ encrypt ONE value for pasting inlineRunning a playbook that needs a password
ansible-playbook site.yml --ask-vault-pass # prompt
ansible-playbook site.yml --vault-password-file ~/.vault-pass # ⭐ a file
export ANSIBLE_VAULT_PASSWORD_FILE=~/.vault-pass # ⭐ env var# ansible.cfg — the everyday convention
[defaults]
vault_password_file = ~/.ansible-vault-pass # path is committed; the FILE is notA3 · Whole files versus single variables
Whole-file encryption is the envelope; encrypt_string is the black bar. That is precisely why a reviewer can look at git diff on an encrypt_string file and see that exactly one secret rotated, while an encrypted whole file shows up as an unreadable wall of changed characters every single time.
ansible-vault encrypt group_vars/prod/vault.yml
✅ Simple, everything inside is protected
❌ Opaque in git diff — every change looks like the whole file changed
❌ Cannot see what changed in review
ansible-vault encrypt_string 'pw' --name 'db_pass'
✅ The rest of the file stays readable and diffable
✅ Review shows which secret changed
❌ Noisy — a wall of base64 in an otherwise clean file
ansible-vault encrypt_string 'SuperSecret123' --name 'vault_db_password'# Paste the output straight into a normal, unencrypted vars file
vault_db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
66386439653236336462626566653063336164663966303231363934653561363964363833313662
6431626536303530376336343832656537303632313433360a626438346336353331386135323734
62656361653630373231613662633962316233633936396165386439616533343337663461363537
3335313962373435640a333234323439353564373662633436633537373234376266393936623463
3234🧪 Exercise A3.1 — Encrypt both ways and compare the git diff
git init vaultlab && cd vaultlab
mkdir -p group_vars/prod
echo 'vaultpass123' > ~/.vault-lab-pass && chmod 600 ~/.vault-lab-pass
export ANSIBLE_VAULT_PASSWORD_FILE=~/.vault-lab-pass
# Approach 1: whole file
cat > group_vars/prod/vault.yml <<'EOF'
---
vault_db_password: SuperSecret123
vault_api_token: tok_abc123
EOF
ansible-vault encrypt group_vars/prod/vault.yml
# Approach 2: single variable inside a readable file
ansible-vault encrypt_string 'SuperSecret123' --name 'vault_db_password' \
> /tmp/enc.txt
{ echo '---'; echo 'db_host: db01.example.com'; echo 'db_port: 5432'; cat /tmp/enc.txt; } \
> group_vars/prod/mixed.yml
git add -A && git commit -qm "secrets"
head -3 group_vars/prod/vault.yml
echo '---'
head -8 group_vars/prod/mixed.yml✅ Expected result — click to reveal
Whole-file encryption — nothing is legible:
$ANSIBLE_VAULT;1.1;AES256
35646166326566373432653066363966613464393633613864343739383464643037313961363764
3234353361666232383735623564373135643163373339650a6234623463303433353862373635Single-variable — structure stays visible:
---
db_host: db01.example.com
db_port: 5432
vault_db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
66386439653236336462626566653063336164663966303231363934653561363964363833
6431626536303530376336343832656537303632313433360a6262383463363533313861Now change one value in each and look at git diff. The whole-file version shows every line as changed, because the ciphertext of the entire file differs. A reviewer sees only "vault.yml changed" and has no way to tell whether you rotated one password or replaced all of them.
The single-variable version shows exactly one variable's block changing, with db_host and db_port untouched and readable. A reviewer can see the shape of the change without ever seeing the secret.
When to choose which — the answer interviewers want: whole-file for a dedicated secrets file where everything in it is sensitive; encrypt_string when secrets are mixed into configuration that benefits from being reviewable. Many teams use both.
🎯 Interview questions — Vault basics
Q. What is Ansible Vault and what does it protect against?
Symmetric AES256 encryption for files in your repository, so secrets can be committed to Git safely. It is built into Ansible with no infrastructure to run, and decryption is transparent at run time.
It protects against someone reading your repository. It does not protect against someone who can run your playbooks — they have the password. It offers no rotation, no audit log and no per-user access control, and secrets are plaintext in memory at run time and can leak into logs without no_log.
Q. Whole-file encryption or encrypt_string — which and why?
Whole-file for a dedicated secrets file where everything is sensitive; it is simpler and there is nothing to review anyway.
encrypt_string when secrets sit alongside ordinary configuration, because the rest of the file stays readable and git diff shows which secret changed without revealing it. Whole-file encryption makes every change look like the entire file was rewritten, so review is impossible.
Most mature repositories use both, choosing per file.
Q. Why should you never use ansible-vault decrypt on a working file?
It writes plaintext to disk permanently, and the next git add -A commits the secret. Use view to read and edit to modify — both keep the file encrypted at rest.
decrypt exists for deliberately retiring a file from Vault, not for inspecting it.
Q. Where does the vault password itself live?
Never in the repository. In a file in the user's home directory with 0600 permissions, referenced by vault_password_file in ansible.cfg — committing the path is fine, committing the file is a breach. Add the pattern to .gitignore as a second line of defence.
In CI it comes from the platform's secret store, written to a temporary file for the run. In AWX it is a Vault credential managed by the platform.
Part B · Vault in a real repository
B1 · The indirection pattern
That is the indirection pattern. Your vars.yml says db_password: "{{ vault_db_password }}" — fully readable, fully reviewable in a pull request, and it tells anyone looking that a database password exists and where it gets used, without revealing one character of it.
The convention every mature Ansible repository uses:
inventories/production/group_vars/prod/
vars.yml <- readable. References vault_ variables
vault.yml <- encrypted. Defines ONLY vault_ variables# vars.yml -- committed readable, fully reviewable
---
db_host: db01.example.com
db_port: 5432
db_user: appuser
db_password: "{{ vault_db_password }}" # indirection
api_token: "{{ vault_api_token }}"# vault.yml -- encrypted. Contains nothing but the secrets
---
vault_db_password: SuperSecret123
vault_api_token: tok_abc123You can see what exists without decrypting. vars.yml tells any reviewer that a db_password is required and where it comes from, while the value stays sealed.
Playbooks never reference vault_* directly. They use db_password, so you can swap the backing store later — to HashiCorp Vault or AWS Secrets Manager — by changing one line in vars.yml and touching nothing else.
Grep works. grep -r vault_ --include=vars.yml lists every secret the project consumes, which is exactly what a security review asks for.
The vault_ prefix is a convention, not a requirement — but it is a universal one, and its absence reads as inexperience.
B2 · Vault IDs — more than one password
Vault IDs are separate keys instead of one master key. A single password for everything feels convenient right up to the first time you need to give somebody access to one thing — and without vault IDs, anyone who can deploy to development can also decrypt every production secret in the repository.
One password for everything means the intern who deploys to dev can decrypt production. Vault IDs fix that.
# Create files under different identities
ansible-vault create --vault-id dev@prompt group_vars/dev/vault.yml
ansible-vault create --vault-id prod@prompt group_vars/prod/vault.yml
# Supply several at once - Ansible matches each file to its label
ansible-playbook site.yml \
--vault-id dev@~/.vault-dev \
--vault-id prod@~/.vault-prod# ansible.cfg
[defaults]
vault_identity_list = dev@~/.vault-dev, prod@~/.vault-prod| Source syntax | Meaning |
|---|---|
| prod@prompt | Ask interactively for the prod password |
| prod@~/.vault-prod | Read it from a file |
| prod@/usr/local/bin/get-pass.sh | Execute a script and read the password from stdout |
This is the bridge between "Vault is too simple for us" and "we need to replace Vault entirely" — often you just need the password itself to come from somewhere real.
B3 · Preventing leaks at run time
Vault is the safe — it protects the file at rest. no_log: true is not saying the number out loud. The two solve completely different problems, which is why a perfectly encrypted secret can still end up leaked: once a decrypted password is printed into a CI log, it is in that log forever, searchable by everyone with access to the build history.
Vault protects the file. It does not stop a task printing the decrypted value into your CI log.
- name: Create the database user
community.postgresql.postgresql_user:
name: "{{ db_user }}"
password: "{{ db_password }}"
no_log: true # ⭐ suppress arguments AND output
- name: Loop over credentials
ansible.builtin.debug:
msg: "configuring {{ item.name }}"
loop: "{{ db_users }}"
loop_control:
label: "{{ item.name }}" # ⭐ Module 03 - hides the rest of the dict
no_log: true- Task output. A module echoing its arguments on failure prints the password. no_log: true is the fix.
- Loop item echoes. Ansible prints (item={'name': 'x', 'password': 'y'}) for every iteration. loop_control: label: or no_log.
- -vvv debugging. Verbose mode prints the full module arguments. no_log still suppresses them — which is exactly why it must be on the task rather than something you remember to avoid.
no_log: true on a task replaces its entire output with censored. The cost is that debugging that task becomes harder, which is the trade you are making deliberately.
🧪 Exercise B3.1 — Watch a secret leak, then stop it
---
- name: Secret leakage
hosts: localhost
gather_facts: false
vars:
db_password: "SuperSecret123"
tasks:
- name: LEAKS - the argument is echoed
ansible.builtin.command: "echo connecting with {{ db_password }}"
register: r1
changed_when: false
- name: LEAKS - and so does the registered result
ansible.builtin.debug:
var: r1.stdout
- name: SAFE - no_log censors everything
ansible.builtin.command: "echo connecting with {{ db_password }}"
no_log: true
changed_when: falseansible-playbook leak.yml
ansible-playbook leak.yml -vvv 2>&1 | grep -c SuperSecret123✅ Expected result — click to reveal
TASK [LEAKS - the argument is echoed] ******************************
ok: [localhost]
TASK [LEAKS - and so does the registered result] *******************
ok: [localhost] => {
"r1.stdout": "connecting with SuperSecret123"
}
TASK [SAFE - no_log censors everything] ****************************
ok: [localhost] => {"censored": "the output has been hidden due to the fact that 'no_log: true' was specified for this task"}With -vvv the count is high — the password appears in the constructed command line, in the module arguments, and in the result object, several times over.
Vault did its job perfectly here. The file on disk was encrypted, the value was decrypted correctly at run time — and then printed to standard output, into CI logs, into log aggregation, and into anyone's terminal scrollback.
The habit to build: no_log: true goes on any task that touches a secret, at the moment you write it. Adding it later means auditing every log you have ever produced. And note the third task shows nothing useful at all — that is the deliberate cost, and why you use no_log surgically rather than everywhere.
🎯 Interview questions — Vault in practice
Q. Describe how you structure vaulted variables in a repository.
The indirection pattern: a readable vars.yml and an encrypted vault.yml in the same group_vars directory. vault.yml defines only vault_-prefixed variables; vars.yml maps them — db_password: "{{ vault_db_password }}".
Three benefits: reviewers can see which secrets exist without decrypting; playbooks reference db_password rather than vault_db_password, so the backing store can be swapped later without touching them; and grep vault_ enumerates every secret the project consumes, which is what a security review asks for.
Q. What are Vault IDs and why use them?
Labelled vault passwords — --vault-id prod@~/.vault-prod — so different files can be encrypted with different passwords and Ansible matches each file to its label automatically.
The reason: one password for everything means anyone who can deploy to dev can decrypt production. Vault IDs give per-environment separation.
The source can be prompt, a file, or an executable script, and the script form is what scales — it can fetch the password from HashiCorp Vault or a CI secret store at run time, so no password file exists on disk.
Q. Vault is working correctly. How can a secret still leak?
Vault encrypts at rest, not at run time. The decrypted value can reach logs through task output, through the item echo on a loop over dictionaries, and through -vvv verbose output which prints full module arguments.
Fixes: no_log: true on any task handling secrets, loop_control: label: on loops over credential dictionaries, and never routinely running production playbooks at -vvv.
no_log also makes that task undebuggable, which is the deliberate trade.
Q. How do you rotate a vault password?
ansible-vault rekey on every encrypted file, then distribute the new password through your secret store.
The uncomfortable part worth volunteering: rekeying does not protect history. Anyone who ever had the old password can decrypt any older commit. Genuine revocation means rotating the underlying secrets themselves — the database passwords and API tokens — not just the vault password.
That limitation is a large part of why teams move to a real secrets manager.
Part C · Vault in automation
C1 · CI/CD
# .gitlab-ci.yml
deploy:
script:
- echo "$ANSIBLE_VAULT_PASSWORD" > /tmp/.vpass
- chmod 600 /tmp/.vpass
- ansible-playbook -i inventories/production playbooks/site.yml
--vault-password-file /tmp/.vpass
after_script:
- shred -u /tmp/.vpass || rm -f /tmp/.vpass# GitHub Actions
- name: Run playbook
env:
ANSIBLE_VAULT_PASSWORD: ${{ secrets.ANSIBLE_VAULT_PASSWORD }}
run: |
printf '%s' "$ANSIBLE_VAULT_PASSWORD" > "$RUNNER_TEMP/.vpass"
chmod 600 "$RUNNER_TEMP/.vpass"
ansible-playbook -i inventories/production playbooks/site.yml \
--vault-password-file "$RUNNER_TEMP/.vpass"
shred -u "$RUNNER_TEMP/.vpass"Better still, use a client script so the password is never written to disk at all — the script fetches it from the platform's secret store when Ansible asks.
C2 · The client-script approach
#!/usr/bin/env bash
# /usr/local/bin/vault-pass-prod.sh (chmod 0700)
# Must print ONLY the password to stdout and exit 0
set -euo pipefail
aws secretsmanager get-secret-value \
--secret-id ansible/vault/prod \
--query SecretString --output text[defaults]
vault_identity_list = prod@/usr/local/bin/vault-pass-prod.shYou keep Vault's simplicity for the content and gain a real secrets manager for the key to it.
Part D · When Vault is not enough
D1 · The alternatives
That register is the whole difference, and it is the same limitation as the diary lock in A1. The moment you need to answer "who opened this, when, and can we revoke just one person?", the home safe stops being enough — and that is the precise point where Ansible Vault gives way to HashiCorp Vault or AWS Secrets Manager. Saying exactly that in an interview is a much better answer than "Vault is insecure", which is not true.
| Option | Strength | Cost |
|---|---|---|
| Ansible Vault | Built in, no infrastructure, works offline | No rotation, no audit, no per-user access, history is permanently readable |
| HashiCorp Vault | Dynamic short-lived credentials, full audit log, fine-grained policy | A cluster to run and secure |
| AWS Secrets Manager / SSM | IAM-controlled, audited via CloudTrail, automatic rotation | AWS-only; a network call per lookup |
| Mozilla SOPS | Encrypted in Git like Vault, but with KMS/age keys and diffable output | An extra tool; needs a KMS or key management |
# HashiCorp Vault at run time - nothing sensitive in the repo at all
- name: Fetch the database password
ansible.builtin.set_fact:
db_password: "{{ lookup('community.hashi_vault.hashi_vault',
'secret=secret/data/prod/db:value') }}"
no_log: true
# AWS Secrets Manager
- name: Fetch from Secrets Manager
ansible.builtin.set_fact:
db_password: "{{ lookup('amazon.aws.aws_secret', 'prod/db/password') }}"
no_log: trueD2 · Choosing
"Ansible Vault for a small team with a modest number of static secrets — it is built in, needs no infrastructure, and works offline. Once you need rotation, audit trails, per-user access, or dynamic short-lived credentials, it stops being sufficient and you move the secrets into HashiCorp Vault or AWS Secrets Manager, pulled at run time by a lookup. A useful middle step is keeping Vault for the content but fetching the vault password itself from a real secrets manager via a client script."
Someone who says "always use HashiCorp Vault" has not run a three-person team; someone who says "Vault is fine" for a regulated environment has not been audited.
🧪 Exercise D2.1 — Make encrypted files diffable in review
# Teach git how to render vault files as plaintext in a diff
git config --local diff.ansible-vault.textconv "ansible-vault view"
printf 'group_vars/**/vault.yml diff=ansible-vault\n' >> .gitattributes
printf '*.vault diff=ansible-vault\n' >> .gitattributes
ansible-vault edit group_vars/prod/vault.yml # change one value
git diff group_vars/prod/vault.yml✅ Expected result — click to reveal
Without the textconv setting:
diff --git a/group_vars/prod/vault.yml b/group_vars/prod/vault.yml
index 8f3a2c1..b7e9d04 100644
Binary files differWith it:
diff --git a/group_vars/prod/vault.yml b/group_vars/prod/vault.yml
--- a/group_vars/prod/vault.yml
+++ b/group_vars/prod/vault.yml
@@ -1,3 +1,3 @@
---
-vault_db_password: SuperSecret123
+vault_db_password: EvenBetterSecret456
vault_api_token: tok_abc123A reviewer can now see that exactly one secret rotated, rather than "the file changed somehow".
🚨 Understand precisely what you have done, and the limits. This decrypts locally, for anyone who already has the vault password — it does not weaken the repository, and the committed file is still ciphertext. But it means the plaintext is now rendered by git diff, git log -p and git show on your machine, so it can end up in a terminal recording, a screen share, or a pager's scrollback.
.gitattributes is committed and shared; the diff.ansible-vault.textconv config is local per-clone, so teammates must opt in themselves. That is deliberate — it should be a conscious choice, not something the repository turns on for you.
🎯 Interview questions — Alternatives
Q. When would you use something other than Ansible Vault?
When you need rotation, an audit trail, per-user access control, or dynamic short-lived credentials — none of which Vault provides.
HashiCorp Vault for dynamic credentials and fine-grained policy; AWS Secrets Manager or SSM Parameter Store when you are already on AWS and want IAM control with CloudTrail auditing; SOPS when you want to keep secrets in Git but need reviewable diffs and KMS-based key management.
A useful middle step: keep Ansible Vault for the content, but fetch the vault password itself from a real secrets manager via a client script.
Q. What happens when someone leaves the team?
Rekeying the vault does not revoke their access to history — they can still decrypt any commit made while they had the password.
Real revocation means rotating the underlying secrets: the database passwords, the API tokens, the certificates. Then rekey the vault as hygiene.
This is one of the strongest practical arguments for a real secrets manager, where revoking a person's access is a policy change rather than a rotation of every credential they ever saw.
Q. What is the performance consideration with a secrets-manager lookup?
Lookups run on the control node, and a lookup inside a task that runs per host executes once per host — so a 200-host play makes 200 API calls to Secrets Manager, which is slow and may hit rate limits.
Fetch once with run_once: true plus delegate_to: localhost, set_fact the result, and read it from hostvars on the other hosts. With no_log: true throughout.
Q. How can encrypted files be made reviewable?
Either use encrypt_string so only the values are ciphertext and the file structure stays diffable, or configure a git textconv filter — git config diff.ansible-vault.textconv "ansible-vault view" with a .gitattributes entry — so git diff renders the plaintext locally for anyone who already has the password.
The caveat: the committed file is unchanged and still encrypted, but plaintext now appears in local git diff output, so it can reach a screen share or terminal recording. .gitattributes is shared; the textconv config is per-clone, so it stays an opt-in choice.
SOPS solves this properly by encrypting only values, leaving structure and keys visible by design.
Part E · Putting it together
E1 · Production practice
| Habit | Why |
|---|---|
| The indirection pattern — readable vars.yml, encrypted vault.yml | Reviewers see which secrets exist without decrypting; the backing store can be swapped later |
| Prefix vaulted variables with vault_ | grep -r vault_ enumerates every secret the project consumes |
| no_log: true on every task that touches a secret, written at the time | Vault encrypts at rest only; the decrypted value reaches logs otherwise |
| loop_control: label: on loops over credential dictionaries | Ansible echoes the full dict per iteration, passwords included |
| Vault IDs per environment | One password for everything means dev access implies production access |
| Password file 0600 in $HOME, never in the repo; path in ansible.cfg is fine | Committing the path is useful; committing the file is a breach |
| In CI, write the password to a temp file — never a CLI argument | Arguments appear in the process list, shell history and job logs |
| A client script fetching the password from a real secrets manager | Nothing on disk, IAM-controlled, audited, rotatable in one place |
| Never ansible-vault decrypt a working file | Plaintext on disk plus git add -A equals a committed secret |
| Rotate the underlying secrets when someone leaves, not just the vault password | Rekeying does not protect history — old commits stay decryptable |
| Add *.vault-pass, .vault_pass*, *.pem, *.key to .gitignore | Defence in depth against a careless git add -A |
E2 · Capstone exercise
Brief. Secure the multi-environment repository from Module 06:
- Production and staging secrets encrypted with different passwords
- Playbooks must reference db_password, never vault_db_password
- A reviewer must be able to see which secrets exist without decrypting anything
- No task may print a secret, including under -vvv
- ansible.cfg configured so neither environment needs extra flags
- .gitignore preventing an accidental commit of a password file
- A single command must prove no plaintext secret exists anywhere in the repository
✅ Model answer — attempt it first, then click
inventories/
production/group_vars/all/
vars.yml readable -> db_password: "{{ vault_db_password }}"
vault.yml encrypted with the prod vault id
staging/group_vars/all/
vars.yml readable
vault.yml encrypted with the staging vault id# Requirement 1 - two identities, two passwords
echo 'prodpass' > ~/.vault-prod && chmod 600 ~/.vault-prod
echo 'stagingpass' > ~/.vault-staging && chmod 600 ~/.vault-staging
ansible-vault create --vault-id prod@~/.vault-prod \
inventories/production/group_vars/all/vault.yml
ansible-vault create --vault-id staging@~/.vault-staging \
inventories/staging/group_vars/all/vault.yml# inventories/production/group_vars/all/vars.yml
# Requirements 2 and 3 - readable, shows WHAT exists, not the values
---
db_host: db01.prod.example.com
db_port: 5432
db_user: appuser
db_password: "{{ vault_db_password }}"
api_token: "{{ vault_api_token }}"# ansible.cfg -- requirement 5
[defaults]
vault_identity_list = prod@~/.vault-prod, staging@~/.vault-staging# playbooks/site.yml -- requirement 4
---
- hosts: all
tasks:
- name: Configure the database connection
ansible.builtin.template:
src: db.conf.j2
dest: /etc/app/db.conf
mode: "0640"
no_log: true
- name: Create application database users
community.postgresql.postgresql_user:
name: "{{ item.name }}"
password: "{{ item.password }}"
loop: "{{ db_users }}"
loop_control:
label: "{{ item.name }}"
no_log: true# .gitignore -- requirement 6
.vault-*
*.vault-pass
.vault_pass*
*.pem
*.key# Requirement 7 - prove no plaintext secret is committed
git grep -nE '(password|token|secret|api_key)\s*:\s*[^"{ ]' -- \
':!*vault.yml' ':!*.md' || echo "CLEAN - no plaintext secrets found"The six decisions that matter:
- vault_identity_list in ansible.cfg — requirement 5. Both identities are always available, Ansible matches each file to its label, and neither environment needs a flag.
- group_vars/all/ as a *directory* containing both files — Module 02 Part C2. Ansible merges every file in the directory, which is exactly how the readable/encrypted split works.
- Playbooks reference db_password, never vault_db_password — requirement 2. Swap to HashiCorp Vault later by editing one line in vars.yml.
- no_log: true on both tasks, plus loop_control: label: — requirement 4. The label alone is not enough for the postgresql_user task, because the module arguments contain the password.
- .vault-* in .gitignore matches the actual filenames chosen — a .gitignore full of patterns that match nothing is theatre.
- The git grep in requirement 7 excludes vault.yml deliberately: those files are supposed to contain secrets, and they are ciphertext. What you are hunting for is a plaintext password that leaked into a readable file.
Verify:
ansible-playbook -i inventories/staging playbooks/site.yml --list-tasks
ansible-playbook -i inventories/production playbooks/site.yml --check
ansible-vault view --vault-id prod@~/.vault-prod \
inventories/production/group_vars/all/vault.yml
ansible-vault view --vault-id staging@~/.vault-staging \
inventories/production/group_vars/all/vault.yml # MUST fail - wrong password
git status --short # no ~/.vault-* filesThat second view failing is the proof that requirement 1 actually holds.
E3 · Command reference — everything from this module
Creating and editing
ansible-vault create secrets.yml # new encrypted file in $EDITOR
ansible-vault create --vault-id prod@prompt secrets.yml # ⭐ under a specific identity
ansible-vault edit secrets.yml # ⭐ decrypt, edit, re-encrypt
ansible-vault view secrets.yml # ⭐ read without modifying
ansible-vault encrypt existing.yml # encrypt a plaintext file
ansible-vault encrypt file1.yml file2.yml # several at once
ansible-vault decrypt secrets.yml # ⚠️ permanent - rarely correct
ansible-vault rekey secrets.yml # ⭐ change the password
ansible-vault rekey --new-vault-id prod@~/.new secrets.ymlSingle-value encryption
ansible-vault encrypt_string 'SuperSecret123' --name 'vault_db_password' # ⭐
ansible-vault encrypt_string --stdin-name 'vault_api_token' # ⭐ no shell history
echo -n 'secret' | ansible-vault encrypt_string --stdin-name 'vault_x'
ansible-vault encrypt_string --vault-id prod@~/.vault-prod 'val' --name 'vault_x'Running playbooks
ansible-playbook site.yml --ask-vault-pass # prompt
ansible-playbook site.yml --vault-password-file ~/.vault-pass # ⭐
ansible-playbook site.yml --vault-id prod@~/.vault-prod # ⭐ labelled
ansible-playbook site.yml --vault-id dev@~/.vault-dev \
--vault-id prod@~/.vault-prod # ⭐ several
export ANSIBLE_VAULT_PASSWORD_FILE=~/.vault-pass # ⭐
ansible-vault view --vault-id prod@~/.vault-prod group_vars/prod/vault.ymlConfig keys
[defaults]
vault_password_file = ~/.ansible-vault-pass # ⭐ single password
vault_identity_list = dev@~/.vault-dev, prod@~/.vault-prod # ⭐ severalAuditing and hygiene
# ⭐ find plaintext secrets that should be vaulted
git grep -nE '(password|passwd|secret|token|api_key)\s*:\s*[^"{ ]' -- ':!*vault.yml'
# ⭐ list every encrypted file in the repo
grep -rl 'ANSIBLE_VAULT;1.1' . --include='*.yml'
# ⭐ list every secret the project consumes, without decrypting
grep -rhoE 'vault_[a-z0-9_]+' --include='vars.yml' . | sort -u
# make vault files diffable locally (opt-in, per clone)
git config --local diff.ansible-vault.textconv "ansible-vault view"
echo 'group_vars/**/vault.yml diff=ansible-vault' >> .gitattributes
# check permissions on password files
ls -l ~/.vault-* && stat -c '%a %n' ~/.vault-*Runtime lookups from a real secrets manager
- ansible.builtin.set_fact:
db_password: "{{ lookup('community.hashi_vault.hashi_vault',
'secret=secret/data/prod/db:value') }}"
no_log: true
run_once: true # ⭐ one API call, not one per host
delegate_to: localhost
- ansible.builtin.set_fact:
db_password: "{{ lookup('amazon.aws.aws_secret', 'prod/db/password') }}"
no_log: truegrep -rl 'ANSIBLE_VAULT;1.1' . --include='*.yml' # what IS encrypted
git grep -nE '(password|token|secret)\s*:\s*[^"{ ]' -- ':!*vault.yml' # what should beIf the second command returns anything, you have found a plaintext credential in version control — and the correct response is to rotate that credential, not merely to encrypt the file, because the history still contains it.
E4 · Official documentation
| Link | Covers |
|---|---|
| Protecting sensitive data with Ansible Vault | The whole guide — concepts, formats, vault IDs |
| Encrypting content | Whole files versus encrypt_string, the !vault inline tag |
| Managing vault passwords | Vault IDs, password files, client scripts |
| ansible-vault CLI | Every subcommand and flag |
| Keeping secrets out of logs — no_log | The official answer on run-time leakage |
| community.hashi_vault collection | HashiCorp Vault lookups and plugins |
| amazon.aws.aws_secret lookup | AWS Secrets Manager at run time |
| Mozilla SOPS | The diffable alternative to whole-file Vault encryption |
E5 · Self-assessment
1. What does Vault protect against, and what does it not?
It protects against someone reading your repository — AES256 encryption at rest.
It does not protect against anyone who can run your playbooks, it has no rotation, no audit log and no per-user access control, secrets are plaintext in memory at run time, and rekeying does not stop old commits from being decryptable by anyone who ever had the password.
2. Describe the indirection pattern and its three benefits.
A readable vars.yml mapping db_password: "{{ vault_db_password }}", and an encrypted vault.yml defining only vault_-prefixed variables.
Benefits: reviewers see which secrets exist without decrypting; playbooks never reference vault_* so the backing store can be swapped by editing one line; and grep vault_ enumerates every secret the project consumes.
3. Why does encrypt_string make review possible where whole-file encryption does not?
Only the value is ciphertext, so the file's structure and its non-secret keys stay readable and git diff shows exactly which secret changed. Whole-file encryption makes every change look like the entire file was rewritten.
4. What are vault IDs and why do they matter?
Labelled passwords — --vault-id prod@~/.vault-prod — letting different files use different passwords with Ansible matching each file to its label.
Without them, one password decrypts everything, so anyone who can deploy to dev can read production secrets.
5. Name three ways a secret leaks despite Vault working correctly.
Task output echoing module arguments, the item echo on a loop over credential dictionaries, and -vvv printing full module arguments.
Fixes: no_log: true, loop_control: label:, and not routinely running production at high verbosity.
6. Why should the vault password never be a command-line argument in CI?
It appears in the process list, in shell history, and frequently in job logs. Write it to a 0600 temp file and use --vault-password-file, removing it in an after_script that runs even on failure — or use a client script so it is never written to disk at all.
7. What is a vault password client script?
An executable given as the vault-id source, which prints the password to stdout and exits 0. It can fetch the password from AWS Secrets Manager or HashiCorp Vault at run time, authenticated by the runner's IAM role — so no password file exists on disk and rotation happens in one place.
8. Someone leaves the team. What actually needs to happen?
Rotate the underlying secrets — database passwords, API tokens, certificates. Rekeying the vault alone does not revoke access to history; they can still decrypt any commit made while they held the password.
Then rekey the vault as hygiene. This limitation is a primary argument for a real secrets manager, where revocation is a policy change.
9. When would you move off Ansible Vault, and to what?
When you need rotation, audit trails, per-user access or dynamic short-lived credentials. HashiCorp Vault for dynamic credentials and policy; AWS Secrets Manager or SSM if you are on AWS and want IAM control with CloudTrail; SOPS if you want to stay in Git but need diffable, KMS-managed encryption.
The pragmatic middle step: keep Vault for the content, fetch the vault password from a real secrets manager via a client script.
10. What is the performance trap with secrets-manager lookups?
Lookups run on the control node and execute once per host, so a 200-host play makes 200 API calls — slow and liable to hit rate limits.
Fetch once with run_once: true and delegate_to: localhost, set_fact it, and read from hostvars elsewhere. With no_log: true throughout.
serial, strategy, forks, async, throttle and fact caching — making Ansible fast and safe on hundreds of hosts rather than five.
📚 Sources for the interview questions
Behaviour verified against the current official Ansible Vault guide.
Question selection cross-referenced against publicly published 2026 Ansible interview question sets:
- Spacelift — 50+ Top Ansible Interview Questions & Answers for 2026
- GeeksforGeeks — Top 50+ Ansible Interview Questions and Answers
- Vinsys — Top 30 Ansible Interview Questions and Answers 2026
- K21 Academy — Ansible Interview Questions & Answers 2026
- Hirist — Top 25+ Ansible Interview Questions and Answers 2026
Answers were rewritten and deepened rather than reproduced — published versions are usually correct but shallow, and the added operational detail is what differentiates a candidate in the room.