Chapter 9: Secrets — Ansible Vault
Topic 52

Ansible Vault

ConceptSecurity

Ansible Vault is symmetric encryption built into ansible-core for the secrets your playbooks need — the database password, an API key, a TLS private key — so they can live in the same git repo as everything else without sitting there in plaintext. You encrypt a vars file or a whole file with a passphrase; at rest in git it is an AES-256 blob, and at run time Ansible decrypts it in memory using the key you supply, never writing the cleartext to the managed node or to disk.

This is the answer to the secrets question every infrastructure tool eventually faces. There is no state file here to leak credentials into, so the only place a secret persists is the encrypted file you control. For Larkspur that means vault_db_password — the PostgreSQL password the db role sets and the app reads — stops being a plaintext line in a vars file and becomes ciphertext that is safe to push to a shared remote.

What Vault Encrypts

Vault works on three granularities: a vars file such as vault.yml, an arbitrary file like a private key or a certificate, or a single string embedded in an otherwise-plaintext file. What it never touches is the playbook's logic. Vault encrypts data, not structure, so a task list, a handler, the names of your variables — all of it stays readable. The secret is hidden; the shape of the automation is not.

That distinction matters for review. A teammate can read a playbook that references vault_db_password, see exactly where the password is wired into the db role, and approve the change — without the value itself ever being visible to them or to anyone who clones the repo. You hide the one thing that must stay secret and keep everything else as plain, diffable text.

The Core Commands

The CLI is one binary, ansible-vault, with a small set of subcommands. ansible-vault create secrets.yml opens a new encrypted file in your $EDITOR and writes ciphertext when you save. ansible-vault edit secrets.yml decrypts the file to a temporary file for your $EDITOR, then re-encrypts it and removes that temp file when you save — so a brief decrypted copy does touch disk under ~/.ansible/tmp (and your editor's own swap or backup files may too), unlike encrypt_string, which stays in memory. ansible-vault encrypt and decrypt convert an existing file between plaintext and ciphertext, and ansible-vault view secrets.yml opens the cleartext in a pager for reading without leaving a decrypted copy behind.

The everyday Vault commands against the Larkspur secrets file
# create a new encrypted file in $EDITOR
ansible-vault create group_vars/db/vault.yml

# edit it later — decrypt, edit, re-encrypt, all in place
ansible-vault edit group_vars/db/vault.yml

# read a value without writing cleartext to disk
ansible-vault view group_vars/db/vault.yml

# convert an existing plaintext file to ciphertext (and back)
ansible-vault encrypt group_vars/db/vault.yml
ansible-vault decrypt group_vars/db/vault.yml

The pairing to internalize is view versus decrypt. Both let you see a secret, but view prints it and forgets it, while decrypt rewrites the file to plaintext on disk and waits for you to remember to re-encrypt. For reading, view is always the right call; decrypt is for the rare case where a file genuinely needs to leave the encrypted state for good.

The Encrypted Blob in Git

An encrypted file is not opaque binary — it is text that starts with the header $ANSIBLE_VAULT;1.1;AES256 followed by lines of hex. That format is deliberate: it survives a git diff cleanly. When you change a vaulted secret and commit, git diff shows the hex churn, so a reviewer can see that the secret changed without seeing what it changed to. The file is safe to push to a shared remote, and the encryption travels with it.

There is a sharp limit to what the header proves, though. It tells you the file is encrypted now. It says nothing about the file's history. If vault.yml was committed once in plaintext and only encrypted later, the cleartext is still sitting in git history one git show away, and the only real fix is to rotate the secret — a point topic 56 returns to in detail.

Decryption Is In-Memory and Per-Run

When a play references a vaulted variable, Ansible decrypts it into memory for that run only. The value materializes when the task that needs it runs and is gone when the process exits. Nothing decrypted is written to /opt/larkspur or /etc/larkspur/app.env unless one of your own tasks deliberately writes it there. The encrypted file in git is the secret's only home at rest; the run is a brief window in which it exists in cleartext, in RAM, and nowhere else.

This is the spine of the whole book restated for secrets. There is no state file accumulating credentials, no agent caching them on the node, no decrypted artifact left behind for the next person to find. Secrets live encrypted in git, materialize in memory, and vanish when the run ends — and that lifecycle is the same whether you run the playbook from your laptop or a CI runner.

A secret's lifecycle — plaintext to ciphertext to memory
vault_db_password in plaintext
ansible-vault encrypt
encrypted blob in git
decrypted in memory at run time

Rekeying

ansible-vault rekey group_vars/db/vault.yml re-encrypts a file under a new passphrase without ever exposing the plaintext in between. Ansible decrypts in memory with the old key and re-encrypts with the new one in a single step, so the secret never touches disk in the clear. This is the command you run when a key leaks, when someone who knew the passphrase leaves, or on a fixed rotation schedule — and pointed at each encrypted file, it rotates the vault password across the whole project one file at a time.

Rekeying changes the key, not the secret. The database password inside the file is identical before and after; what changed is which passphrase unlocks it. That is the right move when the passphrase is what is at risk. If the secret itself has been exposed — say it was once committed in plaintext — rekeying does nothing useful, because the exposed value is unchanged; that situation needs a new password, not a new key.

Common Mistakes
  • Encrypting group_vars/db/vars.yml wholesale so the plaintext settings — the PostgreSQL version, the listen address — become ciphertext too; every diff is now opaque and a non-secret change needs the vault key. Split secrets into a separate vault.yml instead.
  • Running ansible-vault decrypt secrets.yml just to read one value and then committing the now-plaintext file — decrypt rewrites the file in place. Use view to read without leaving cleartext on disk.
  • Storing the vault passphrase in the same repo as the encrypted file, like a vault_pass.txt committed next to vault.yml — anyone with the repo then has both halves and the encryption bought nothing.
  • Treating the $ANSIBLE_VAULT header as proof the secret is gone from history — if the file was committed in plaintext before you encrypted it, the cleartext is still in git history, and the secret must be rotated, not just encrypted going forward.
  • Reaching for rekey after a secret leaks, expecting it to help — rekeying changes the passphrase, not the value, so the exposed password is still the live password until you actually change it.
Best Practices
  • Encrypt only the secret values, in a dedicated vault.yml, and keep non-secret settings in a plaintext sibling so reviews stay meaningful and diffs stay readable.
  • Use ansible-vault view to read and edit to change a secret in place, never decrypt, so no decrypted copy is left behind on disk — edit removes its temp file on save, while decrypt rewrites the file as persistent cleartext you might commit.
  • Rekey with ansible-vault rekey whenever someone with the passphrase leaves or on a fixed rotation, so the key is not the same one three engineers ago set.
  • Keep the vault passphrase out of the repo entirely — in a password manager or a CI secret store — so the encrypted file and its key never travel together.
  • Treat a once-committed plaintext secret as compromised and rotate it; encrypting the file afterward protects future commits, not the cleartext already in history.
Comparable tools SOPS · git-crypt file-level encryption in the Terraform and Kubernetes world sealed-secrets per-cluster encryption for Kubernetes HashiCorp Vault · cloud secret managers the external alternative, covered in topic 55 Plaintext secrets.yml in git the anti-pattern Vault replaces

Knowledge Check

What does Ansible Vault encrypt, and what does it leave readable?

  • The data — a vars file, a whole file, or a single string — while the playbook's logic and structure stay in plaintext
  • The entire playbook, every task and handler included, so nothing in the repo is reviewable at all without the passphrase
  • Only files whose names end in vault.yml, with that suffix checked and enforced by the engine on every load
  • The variable names but never their values, so the file's structure is hidden while the data stays in the clear

You need to read one value out of an encrypted file. Why is ansible-vault view the right command rather than decrypt?

  • view shows the cleartext in a pager and leaves no decrypted copy behind, while decrypt rewrites the file to plaintext on disk
  • view is measurably faster on large vars files because it streams the bytes straight to the pager and skips the AES-256 decryption step that decrypt is forced to run in full
  • decrypt requires a reachable HashiCorp Vault server to fetch the unwrap key over TLS before it can open the blob, while view works entirely offline against a cached key
  • view re-encrypts the file under a fresh passphrase as a side effect, rotating the key for you for free

At run time, where does a decrypted vaulted value live and for how long?

  • In memory, for the length of the run only — nothing decrypted is written to the node unless one of your own tasks writes it
  • In a JSON state file under the control node's project directory that records every decrypted secret and its source path so the next run can reuse it without re-prompting
  • In a fact cache written to the managed node's disk, kept warm across plays so subsequent runs against that host can skip the decryption step entirely
  • In /etc/ansible/decrypted on the node, cleaned up only when you remember to run ansible-vault clean

A secret was committed in plaintext, then encrypted in a later commit. Is the secret safe now?

  • No — the plaintext is still in git history, so the secret must be rotated, not just encrypted going forward
  • Yes — the $ANSIBLE_VAULT;1.1;AES256 header on the current file is proof the cleartext has been rewritten and purged from every commit in the repo's history
  • Yes — encrypting the current file makes git automatically re-encrypt every past version of it in place
  • Yes, as long as you also run ansible-vault rekey afterward to change the passphrase on the blob

You got correct