Skip to content

Vault & secrets

Scope: the course page shows one vault.yml per group encrypted with a single password, run via --ask-vault-pass or --vault-password-file, plus the vault-indirection naming trick, vault IDs as a concept, and encrypt_string for one-off values. This page slows down on rotating a vault password without touching its content, running with several vault passwords at once, and not having to pass --vault-id on every invocation — then zooms out to where secrets can come from besides Vault at all.

TL;DR — in 30 seconds:

  • ansible-vault rekey rotates a vault password without ever exposing the file's plaintext to you — it decrypts with the old password and re-encrypts with the new one in one step, but every teammate/CI job still needs the new password file afterward.
  • A --vault-password-file target can be a plain file or an executable — the executable form is what lets a password live in a keychain or secrets manager instead of on disk, since Ansible runs it and reads the password from stdout.
  • Vault isn't the only way a secret reaches a play: an env lookup or an external secret-manager lookup both keep the value out of the repo entirely, at the cost of depending on something live at run time instead of something committed.

1. Rekeying — rotating a password without re-encrypting by hand

A vault password should be rotated periodically (an operator leaves, a password leaks, a compliance policy requires it). ansible-vault rekey re-encrypts a file under a new password without ever exposing its plaintext to you — you never decrypt then encrypt by hand:

ansible-vault rekey group_vars/prod/vault.yml --new-vault-password-file ~/.ansible/vault-pass-prod-new

Ansible decrypts with the old password (prompted, or read from --vault-password-file if the file still holds it) and re-encrypts with the new one in a single step. Once rekeyed, every teammate and every CI job needs the new password file — rekeying one file doesn't rotate anyone's stored copy for you.

2. The vault-password-file: plaintext file vs executable script

--vault-password-file accepts two different kinds of target, and the distinction matters for where a password can safely live:

Target Behavior
A plain text file Ansible reads its contents (trimmed of a trailing newline) as the password directly
An executable file Ansible runs it and reads the password from its stdout

The executable form is what lets the password live in a credential manager instead of on disk: the "password file" is really a small script that calls out to your OS keychain, a password manager's CLI, or a secrets API, and prints the result. Either way the file must not be world-readable — restrict its permissions (chmod 600) and never commit it, the same rule the course page already applies to the vault password itself.

3. Running with more than one vault password at once

A real inventory usually has more than one vault-encrypted group (prod, staging, each with its own password). The course page's single-password flow doesn't cover this: you pass one --vault-id per password, and Ansible tries each candidate against each encrypted blob until one matches:

ansible-playbook site.yml \
  --vault-id prod@~/.ansible/vault-pass-prod \
  --vault-id staging@~/.ansible/vault-pass-staging

This works because an encrypted file's header carries a version, a cipher, and — when it was encrypted with --vault-id label@… — the label it was encrypted under (format documented here):

$ANSIBLE_VAULT;<version>;<cipher>[;<label>]

Ansible matches an incoming --vault-id label@source against that header label first; if no label is present in the header (a file encrypted the old single-password way) it falls back to trying every supplied password in turn. This is why the label in --vault-id label@source is only a hint for matching and routing — it is not itself part of the encryption.

flowchart LR
    F1["group_vars/prod/vault.yml\nheader label: prod"]
    F2["group_vars/staging/vault.yml\nheader label: staging"]
    V1["--vault-id prod@vault-pass-prod"]
    V2["--vault-id staging@vault-pass-staging"]
    V1 -->|label matches| F1
    V2 -->|label matches| F2

4. Not typing --vault-id every time — ansible.cfg

Once a project has more than one vault password, retyping every --vault-id on every run gets old fast. ansible.cfg can declare the same identities once, under [defaults]:

[defaults]
vault_identity_list = prod@~/.ansible/vault-pass-prod, staging@~/.ansible/vault-pass-staging

Every ansible-playbook run in that project then has both identities available automatically. A single, unlabeled password can similarly be set with vault_password_file = <path> for the simple, one-password case the course page shows. Either setting does exactly what its command-line equivalent does — it just saves you from repeating the password source on every invocation or in every CI step.

5. Secret injection beyond Vault

Vault is not the only way a secret reaches a play — it is the one that fits "commit an encrypted value into the repo." Two other patterns solve the same problem differently, and knowing they exist matters more than memorizing their exact syntax:

Pattern How the secret gets in Trade-off
Ansible Vault (course page) Encrypted at rest in the repo; decrypted at run time with a password Secret travels with the code; password distribution is now your problem
Environment-variable lookup An env lookup reads a variable already present in the runner's environment No secret content in the repo at all; but it depends on whatever set that environment variable being trustworthy and not logged
External secret-manager lookup A lookup plugin calls out to a secrets service (e.g. a HashiCorp Vault server, a cloud provider's secrets manager) at run time and returns the current value Nothing secret ever sits at rest in the repo, and rotation happens centrally — at the cost of needing network access and its own authentication at run time
flowchart TB
    T["A task needs {{ db_password }}"]
    A["Ansible Vault\nencrypted file, decrypted with a password"]
    B["env lookup\nreads the runner's environment"]
    C["Secret-manager lookup\nlive call to an external service"]
    A --> T
    B --> T
    C --> T

The three are not mutually exclusive on one project: it's common to Vault-encrypt long-lived, rarely rotated values while pulling anything that rotates frequently or is centrally managed from an external secret manager instead.

6. How this connects

The course page's one vault.yml, one password, one encrypt_string example is the smallest version of "keep a secret out of plaintext in the repo." This page is what to reach for once that password needs to rotate, once a project has more than one environment's worth of secrets to juggle at once, once retyping --vault-id on every run gets in the way, or once the question stops being "how do I encrypt this" and becomes "should this even live in the repo, encrypted or not."

Common gotchas

  • Manually decrypt-then-encrypt-ing to rotate a vault password. Fix: use ansible-vault rekey instead — it never exposes the plaintext to you in between, and does the old→new swap in one step.
  • Leaving a --vault-password-file target world-readable. Fix: chmod 600 it and never commit it, whether it's a plaintext password file or an executable script that calls out to a keychain.
  • Forgetting that rekeying a file doesn't rotate anyone else's stored copy. Fix: after rekey, every teammate and CI job needs the new password file — the old one no longer works against that file.
  • Retyping every --vault-id on every invocation instead of setting vault_identity_list in ansible.cfg. Fix: declare all identities once under [defaults] and every run in that project picks them up automatically.
Check yourself — you rekey group_vars/prod/vault.yml to a new password. A CI job using the old password file tries to run against it the next day. What happens?

It fails to decrypt — rekeying re-encrypts the file under the new password only, it doesn't propagate to anyone else's stored copy. Every consumer (teammates, CI) needs the new password file before their next run.

Check yourself — your --vault-password-file argument points at an executable script instead of a plain text file. What does Ansible actually do with it?

It runs the script and reads the password from its stdout, rather than reading file contents directly — that's what lets the password live in a credential manager or OS keychain instead of sitting in a file on disk.

You can defend this when you can explain why rekey never exposes plaintext to the operator, describe the difference between a plaintext and an executable --vault-password-file target, say how Ansible matches a --vault-id label against an encrypted file's header, name the ansible.cfg setting that removes the need to repeat --vault-id on every invocation, and name at least one alternative to Vault for getting a secret into a play and the trade-off it makes against Vault.