Most engineers think…
Most engineers think Ansible Vault "hides" their passwords — so once a var is vaulted they treat the repo as safe and stop worrying. They picture the secret being tucked away somewhere clever.
Wrong — and that mindset gets repos breached. Vault doesn't hide anything; it encrypts the value with AES-256 using a password you must keep outside the repo. The committed file is unreadable ciphertext, yes — but the security comes entirely from where the vault password lives. Commit that password (or pass -vvv on a vaulted var, or paste the key into CI logs) and the encryption is worthless. Vault moves the secret to one thing you must protect — the key — instead of scattering plaintext across forty files.
① The plaintext-secret problem — why a password in Git is a grenade
Meet Sneha, an L1 automation engineer at Infosys. She inherits a tidy-looking Ansible repo that pushes config to 200 Cisco and Fortinet boxes. In group_vars/all.yml she finds this: enable_password: Wipro@123, an api_token for the monitoring API, and an ssh_pass. All in clear text. All committed. The repo is mirrored to the company GitHub. That folder is a breach waiting to happen — anyone who clones the repo, sees a fork, or reads the commit history reads every device's enable password in plain sight.
It gets worse than "don't commit secrets." A junior on Sneha's team, Rahul, spotted the same passwords and 'fixed' it by deleting the lines and committing. He felt safe. He wasn't: Git keeps history, so git log -p group_vars/all.yml still shows every old version with the password intact. Deleting a line does not erase it from the repo's past — you'd have to rewrite history, and by then it may already be cloned. That is the single most common, most dangerous misunderstanding about secrets in version control.
Enter Ansible Vault. It encrypts the sensitive value — or a whole file — at rest using AES-256, keyed by a password you choose. The committed file is replaced by unreadable ciphertext that begins with the header $ANSIBLE_VAULT;1.1;AES256. Ansible decrypts it only at run time, in memory, when you supply the password. So the secret can sit right there in Git, fully version-controlled and diffed alongside everything else, and a thief who clones the repo gets nothing but encrypted noise.
Plaintext secrets in Git are like writing every flat's spare-key location in the open society gate-pass register at the gate — convenient, but every visitor, guard and courier reads it. Ansible Vault is putting those details in a sealed bank locker: the register (your repo) can still be public, but the locker is opened only when you bring the locker key — and you never leave that key in the register. Lose the locker key, and even you can't open it; leave the key in the register, and the lock was pointless.
One sober, recent reminder that the key is the real asset: in 2025, CVE-2025-4656 showed that even a dedicated secret store (HashiCorp Vault) had a flaw in its rekey path that an operator could abuse to cause a denial of service — fixed in Vault 1.20.0. The lesson for an Ansible engineer: encryption tooling is itself attack surface, so the discipline around who holds the key and how you rotate it matters as much as turning encryption on.
Priya at TCS removed a plaintext API token from vars.yml, committed, and pushed. A teammate says the secret is still exposed. Why is the teammate right?
Pause & Predict
Predict: if the committed vault.yml is just AES-256 ciphertext, what is the ONE thing an attacker still needs to read your secrets — and where must you make sure it never lives? Type your guess.
② Core workflow — create, encrypt_string, and the vault.yml convention
Vault is one command, ansible-vault, with seven sub-commands you will use constantly. create opens your $EDITOR on a brand-new file and encrypts it when you save. edit decrypts an existing vault file into a temp editor session and re-encrypts on save (never decrypting to disk). view prints the decrypted content to the screen without editing. encrypt takes an existing plaintext file and encrypts it in place; decrypt reverses that. rekey changes the password. And encrypt_string encrypts a single value to paste inline into a YAML file. That last one is the one most teams reach for.
ram@netauto:~/playbooks$ ansible-vault create group_vars/all/vault.yml New Vault password: Confirm New Vault password: # ($EDITOR opens — you type vault_enable_password: Cisco@123 — save & quit) ram@netauto:~/playbooks$ head -1 group_vars/all/vault.yml
$ANSIBLE_VAULT;1.1;AES256 3134663539663... (ciphertext — the secret is never on disk in clear)
Now the pattern that keeps things readable. The convention is to keep a vault.yml beside your normal group_vars, holding only the encrypted secrets, and to name every encrypted variable with a vault_ prefix. Your plain, reviewable vars file then references them. So vars/main.yml says enable_password: "{{ vault_enable_password }}" (readable, diff-friendly), while vault.yml holds vault_enable_password as ciphertext. Anyone scanning the repo sees instantly which values are secret, and code review still works on the non-secret file.
ram@netauto:~/playbooks$ ansible-vault encrypt_string 'Cisco@123' --name 'vault_enable_password' New Vault password: Confirm New Vault password:
vault_enable_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
66386439653761...3863316665
3937336466...c41
Encryption successfulThat !vault block you paste straight into a YAML file — for example into group_vars/all.yml or a host_vars file — and the rest of that file stays human-readable. This is the difference from whole-file create/encrypt: with encrypt_string only the one value is opaque, so reviewers can still read and diff the structure around it. Use whole-file when a file is all secrets; use encrypt_string when one or two values hide inside otherwise-public config.
The Vault command map, in one tap each
Tap each card — these are the four moves you'll repeat every week, and the exact verb for each.
Whole-file: create opens $EDITOR new, edit re-opens it decrypted, view just prints. So: manage a vault.yml that is all secrets.
Take an existing plaintext file to ciphertext in place, or back. So: retrofit a file you already wrote, or hand it off in clear.
Encrypt ONE value into a !vault block you paste inline. So: hide one token in an otherwise reviewable YAML file.
Swap the password old→new on a vault file. So: rotate the key after a leak or when someone leaves the team.
Symptom: your play references a vaulted var and Ansible stops with ERROR! Attempting to decrypt but no vault secrets found. Cause: you ran the playbook but never supplied the vault password — Vault encrypts at rest, so every run that touches a vaulted var needs the key. Fix: add --ask-vault-pass (or --vault-password-file). A close cousin: Decryption failed means you supplied the wrong password — the file is fine, your key isn't.
Aditya at HCL has a vars file that is 90% plain config and just ONE secret API token inside it. He wants reviewers to keep reading and diffing the file. Which command fits best?
Pause & Predict
Predict: you run 'ansible-vault edit vault.yml', change a value, and save. Was the secret ever written to disk in plaintext during that edit — and why does the answer matter for a shared dev box? Type your guess.
③ Supplying the password — prompt, file, vault IDs, and a real secret store
Encryption is half the job; the other half is getting the password to Ansible at run time without leaking it. The simplest is the interactive prompt: ansible-playbook site.yml --ask-vault-pass makes Ansible ask for the password before it runs. Great on a laptop, useless in automation — a cron job or CI pipeline can't type. So the next step is a vault password file: a one-line file you reference with --vault-password-file.
ram@netauto:~/playbooks$ printf '%s' 'mySuperSecretVaultPass' > .vault_pass ram@netauto:~/playbooks$ chmod 600 .vault_pass ram@netauto:~/playbooks$ echo '.vault_pass' >> .gitignore ram@netauto:~/playbooks$ git check-ignore .vault_pass ram@netauto:~/playbooks$ ansible-playbook site.yml --vault-password-file .vault_pass
.vault_pass PLAY [Push base config to branch routers] ********************** TASK [set enable secret from vault] **************************** changed: [BR-Mumbai-rtr01] changed: [BR-Pune-rtr01] PLAY RECAP ***************************************************** BR-Mumbai-rtr01 : ok=4 changed=1 unreachable=0 failed=0
Notice git check-ignore .vault_pass printed the filename — that's your proof Git will skip it. Two non-negotiables here: chmod 600 (only you can read the password file) and .gitignore (it never enters the repo). On a real CI/CD runner you don't even keep a file: you store the password as a masked pipeline secret, write it to a temp file at job start, run with --vault-password-file, then delete it — and you never echo it into the logs.
Now the grown-up version: multiple vault IDs. You almost never want the same key for dev and prod — if the dev key leaks, prod should stay safe. With --vault-id you label each password: --vault-id dev@dev.vp --vault-id prod@prompt means "use the dev key from a file, and ask me interactively for the prod key." Ansible tries each supplied ID against each encrypted block. That prod@prompt form keeps the production key in a human's head, never on the runner.
ram@netauto:~/playbooks$ ansible-vault encrypt_string --vault-id prod@prompt 'Airtel#Prod99' --name 'vault_enable_password' New vault password (prod): Confirm new vault password (prod): ram@netauto:~/playbooks$ ansible-playbook site.yml --vault-id dev@dev.vp --vault-id prod@prompt
vault_enable_password: !vault |
$ANSIBLE_VAULT;1.2;AES256;prod
39383166...0a64
Vault password (prod):
PLAY RECAP ***************************************************
BR-Mumbai-rtr01 : ok=4 changed=1 unreachable=0 failed=0Look at the header in that last output: $ANSIBLE_VAULT;1.2;AES256;prod. A plain vault file is format 1.1; the moment you attach a vault ID label, the format becomes 1.2 and the label is stored right in the header. That's how Ansible knows which key to try first — and how you can tell at a glance whether a block belongs to dev or prod just by reading head -1.
▶ Watch the password reach a vaulted variable, three different ways
Karthik runs the same play three times — once on his laptop, once in CI, once with a secret store. Follow how the password gets in each time, and what's exposed. Press Play for the healthy path, then Break it to see the failure.
enable secret; plaintext gone at exitThe best long-term pattern moves past a static file entirely: a client script (its filename ends in -client.py) that fetches the password from a real secret store. Better still, skip the static-ciphertext model for high-value secrets and pull them at run time with the community.hashi_vault lookup — so nothing sensitive is committed at all, and rotation, audit and RBAC live in the store. Vault-the-file is your floor; a secret store is your ceiling.
Neha at Flipkart faces this
Neha's nightly AWX job that pushes firewall rules suddenly fails for every host with: 'ERROR! Attempting to decrypt but no vault secrets found'. It worked yesterday from her laptop.
On her laptop she runs with --ask-vault-pass and types the key. The scheduled AWX job has no human to type it, and no vault credential was attached to the job template — so Ansible reaches a vaulted var with no key and aborts before touching a single device.
She confirms the play references vault_ vars, then checks how the password is being supplied in automation versus interactively — the laptop has a key source, the job template does not.
AWX → Templates → nightly-firewall-push → Edit → Credentials → add a 'Vault' credential (or set --vault-password-file via the job's extra args)Attach a Vault credential to the AWX job template (AWX injects it as a vault-password-file at run time), or for a raw cron use --vault-password-file pointing at a 600/root-owned file written from the secret store. Never bake the password into the playbook.
Re-run the job: the PLAY RECAP shows changed/ok with failed=0, and the AWX job output never prints the password (no_log on the sensitive task keeps it out of the log).
Meera wants dev secrets readable by the whole team from a file, but the PROD key to stay only in a senior engineer's head, never on any runner. Which invocation matches?
Pause & Predict
Predict: your CI pipeline stores the vault password as a masked variable, but a task runs a shell command that echoes a vaulted value for 'debugging'. What leaks, and what one Ansible keyword would have stopped it? Type your guess.
④ Doing it right — what to encrypt, key hygiene, rekey, and a worked play
Three habits separate engineers who use Vault from engineers who use it safely. First, encrypt only what's secret — passwords, tokens, private keys, license strings. Encrypting the whole repo makes every diff opaque, kills code review, and tempts people to decrypt-everything-to-debug. Second, keep the key out of the repo, always — the password file is gitignored and chmod 600, and the prod key ideally never touches disk. Third, name secrets with the vault_ prefix so anyone can see at a glance which variables are sensitive.
Now the rotation discipline. When someone leaves the team, when a key is suspected leaked, or just on a schedule, you rotate the vault password with ansible-vault rekey. It asks for the old password, then the new one, and re-encrypts the file under the new key — the plaintext never appears. Rekey changes the vault password; if an actual device password leaked, you must also change that on the device and re-encrypt the new value. Two different secrets, two different rotations.
ram@netauto:~/playbooks$ ansible-vault rekey group_vars/all/vault.yml Vault password: New Vault password: Confirm New Vault password: ram@netauto:~/playbooks$ ansible-vault view group_vars/all/vault.yml --vault-password-file .vault_pass_new
Rekey successful vault_enable_password: Cisco@123 vault_api_token: sk_live_9f2c...c41
Two gotchas bite teams constantly. Diffing encrypted files is useless — a one-character change scrambles the whole ciphertext, so git diff on a vault file is noise. Fix: a per-repo Git diff driver that decrypts on the fly for review, or just keep secrets in a small dedicated vault.yml so the noisy diffs are contained. Accidentally committing the password is the other — a stray .vault_pass that wasn't gitignored, or a key pasted into CI logs. A pre-commit hook that rejects any staged vars file not starting with $ANSIBLE_VAULT stops both classes of accident.
Cold, in five seconds: head -1 group_vars/all/vault.yml must print $ANSIBLE_VAULT;1.1;AES256 (or 1.2 with a label) — if you see your secret in clear text, STOP. Then git check-ignore .vault_pass must echo the filename, proving the key is excluded. A pre-commit hook that greps for the $ANSIBLE_VAULT header on every staged *vault* file turns this from a habit into a guarantee.
# group_vars/all/vars.yml (plain, committed, reviewable)
enable_password: "{{ vault_enable_password }}"
# group_vars/all/vault.yml (ciphertext, committed)
# built with: ansible-vault encrypt_string 'Cisco@123' --name 'vault_enable_password'
vault_enable_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
66386439653761...3863316665
# push_enable.yml (the play)
- name: Push enable secret to branch routers
hosts: branch_routers
gather_facts: false
tasks:
- name: Configure the enable secret from Vault
cisco.ios.ios_config:
lines:
- "enable secret {{ enable_password }}"
no_log: true # never print the secret, even on failure$ ansible-playbook push_enable.yml -i inventory --vault-password-file .vault_pass TASK [Configure the enable secret from Vault] **************** changed: [BR-Mumbai-rtr01] changed: [BR-Pune-rtr01] PLAY RECAP ************************************************** BR-Mumbai-rtr01 : ok=1 changed=1 unreachable=0 failed=0
For your certification path this lesson lands squarely on the RHCE EX294 objective "use Ansible Vault in playbooks to protect sensitive data." In the lab you'll typically be asked to create an encrypted vault.yml, reference its variables from a play, and run that play non-interactively using a vault password file — exactly the workflow above. Practice doing it without notes: ansible-vault create, reference with the vault_ prefix, run with --vault-password-file, and add no_log: true on the sensitive task. That muscle memory is worth easy marks.
An interviewer asks Arjun: "Your repo is public, vault.yml is encrypted, but you committed .vault_pass last month. How bad is it, and what's the FULL fix?"
🤖 Ask the AI Tutor
Tap any question — instant, scoped to this lesson. No login, no waiting.
Pre-curated from Ansible docs + community Q&A, scoped to this lesson. For a live prod issue, paste your export into chat.techclick.in.
📝 Wrap-up assessment — six more
You've answered 4 inline. Six left. 70% (7 of 10) marks the lesson complete on your profile. Tap Submit all answers at the end.
🧠 In your own words
Type one line: In one line, why does committing the vault password file to Git make AES-256 encryption of your vault.yml pointless? Then compare to the expert version.
🗣 Teach a friend
Best way to lock it in — explain it in one line to a teammate. Tap to generate a paste-ready summary.
📖 Glossary
- Ansible Vault
- Ansible's built-in feature that encrypts variables or whole files at rest with AES-256, so secrets can live in version control safely.
- AES-256
- The symmetric cipher Vault uses (Advanced Encryption Standard, 256-bit key). The committed file is ciphertext, not readable text.
- $ANSIBLE_VAULT header
- The first line of any vaulted file — e.g. $ANSIBLE_VAULT;1.1;AES256 — your quick proof the content is encrypted, not plaintext.
- encrypt_string
- Sub-command that encrypts a single value into an inline !vault block you paste into otherwise-readable YAML.
- !vault tag
- A YAML tag marking the following block as a Vault-encrypted value, letting a secret sit inline inside a plaintext file.
- vault_ prefix
- Naming convention (e.g. vault_enable_password) so encrypted variables are obvious at a glance; plain vars reference them.
- --ask-vault-pass
- Flag that prompts interactively for the vault password — good on a laptop, can't run unattended.
- Vault password file
- A one-line file holding the password, used with --vault-password-file. Must be chmod 600 and gitignored.
- --vault-id label@source
- Attach a labelled password (dev/prod) from a prompt, file or client script, so one repo can use different keys per environment.
- rekey
- Sub-command that rotates a vault file from the old password to a new one, re-encrypting without exposing plaintext.
- no_log
- Task/play keyword that hides a task's output so a decrypted secret is never printed to the console or CI logs.
- community.hashi_vault
- An Ansible collection whose lookup pulls secrets from HashiCorp Vault at run time — the dynamic, audited alternative to a static vault file.
📚 Sources
- Ansible Community Documentation — "Encrypting content with Ansible Vault" (ansible-vault create/edit/view/encrypt/decrypt/rekey; encrypt_string with --name; the !vault tag; AES-256; $ANSIBLE_VAULT;1.1;AES256 header). docs.ansible.com/projects/ansible/latest/vault_guide/vault_encrypting_content.html · docs.ansible.com/projects/ansible/latest/cli/ansible-vault.html
- Ansible Community Documentation — "Managing vault passwords" (--ask-vault-pass; --vault-password-file; --vault-id label@source with prompt/file/script; client scripts ending in -client; DEFAULT_VAULT_ID_MATCH). docs.ansible.com/projects/ansible/latest/vault_guide/vault_managing_passwords.html
- Ansible community / OneUptime guides — "Ansible Vault with Git for team collaboration" + selivan.github.io pre-commit hook (committing encrypted files is fine; diffs are useless on ciphertext; pre-commit hook checks files start with $ANSIBLE_VAULT; never commit the password; chmod 600 + .gitignore). oneuptime.com/blog/post/2026-02-21-how-to-use-ansible-vault-with-git-for-team-collaboration/view · selivan.github.io/2017/04/08/ansible-check-on-commit-vault-files-are-encrypted.html
- Recent item (2025–26) — CVE-2025-4656 (HashiCorp Vault rekey/recovery-key DoS, fixed in Vault 1.20.0) + community.hashi_vault collection v7.1.0 lookup for run-time secret retrieval as the dynamic alternative to a static vault file. cvedetails.com/cve/CVE-2025-4656/ · docs.ansible.com/projects/ansible/latest/collections/community/hashi_vault/hashi_vault_lookup.html
- Red Hat — EX294 (RHCE) exam objectives: "use Ansible Vault in playbooks to protect sensitive data" (create vault.yml, reference encrypted vars, run non-interactively with a vault password file). redhat.com/en/services/training/ex294-red-hat-certified-engineer-rhce-exam-red-hat-enterprise-linux
- Ansible Community Documentation — "Using encrypted variables and files" (referencing vaulted vars from plays; no_log to keep secrets out of output; whole-file vs single-variable encryption trade-offs). docs.ansible.com/projects/ansible/latest/vault_guide/vault_using_encrypted_content.html
What's next?
You've got the secrets locked down — now stop copy-pasting tasks between playbooks. Roles turn your scattered plays into clean, reusable building blocks with a standard layout (and a tidy place to keep that vault.yml).