Skip to main content
RunBook Academy

AnsibleXXI · Secrets ManagementSecrets management

Where the vault password lives

Advanced⏱ ~24 minansible-core

What you'll learn

  • Judge a vault password location by who can read it rather than by whether it is committed
  • Use an executable password source, and distinguish it from a vault-ID-aware client script
  • Write a client script that fails closed and does not leak through its own stderr
  • Choose between prompt, file, client script and keyring for a given controller

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

Every lesson so far has moved a secret behind a password. This one asks the obvious follow-up question, which is remarkably often the one nobody asks:

Where is that password?

Encryption does not remove a secret. It replaces many secrets with one, and the security of the whole arrangement collapses to the security of the replacement. If the vault password is easier to obtain than the credentials it protects, the encryption has made things worse — it has added a step and a false sense of completion.

The arrangement that protects nothing

This is what a great many estates actually have:

/srv/automation/
    ansible.cfg
    inventory/
        production/group_vars/all/vault.yml     0644, encrypted
    .vault-pass                                 0644

Or the same thing with vault_password_file = .vault-pass in ansible.cfg, and .vault-pass in .gitignore so it is at least not committed.

Anyone who can read the repository directory can read the password. Which means anyone who can read the repository can decrypt the vault. Which means the encryption protects the repository against exactly one thing: being copied without its directory — a git clone by someone who has repository access but not filesystem access to that server.

That is not nothing. It defends against a leaked clone, a mirror, a misconfigured Git host, a laptop backup of a checkout. Those are real and they happen. But it is a much narrower protection than “our secrets are encrypted” suggests, and the gap between the belief and the reality is where incidents live.

What the setting actually accepts

Read-only / Safethe documented behaviour
$ ansible-config list | grep -A3 DEFAULT_VAULT_PASSWORD_FILE
DEFAULT_VAULT_PASSWORD_FILE:
description:
- The vault password file to use. Equivalent to --vault-password-file or --vault-id.
- If executable, it will be run and the resulting stdout will be used as the password.
default: null

That second line is the interesting one. If the file is executable, Ansible runs it and uses its standard output. The “password file” is therefore a general extension point, not just a place to keep a string.

Which opens the good options:

SourcePassword at rest on the controller?Auditable centrally?
promptnono
plain fileyesno
executable scriptno, if it fetchesyes, if the store logs
client scriptno, if it fetchesyes, and it knows the label
keyring via a scriptin the keyring, unlocked per sessionpartly

Executable, and vault-ID-aware

There are two kinds of executable source and the difference is the filename. Measured on 2.21.3:

Read-only / Safea plain executable gets no arguments
$ ansible-vault view --vault-id prod@plainexec.sh p2.yml
invoked with: []
Read-only / Safea script named *-client is told which label is wanted
$ ansible-vault view --vault-id dev@prod-client.sh p2.yml
client invoked with: [--vault-id dev]
no password configured for vault-id: dev

A script whose name ends in -client — or -client.EXTENSION — is invoked with --vault-id <label>. A plain executable is invoked with nothing and cannot know which of several passwords is being asked for.

Ansible even distinguishes them in its error text, which is a useful diagnostic when a script is not being called the way you expected:

Vault password script ... returned non-zero (1)
Vault password client script ... returned non-zero (1) when getting secret for vault-id=dev

If you see the first message and expected the second, your script is not named -client.

Read-only / Safea client script that fails closed
#!/bin/sh
# /usr/local/lib/ansible/vault-client.sh
# Named *-client so Ansible passes --vault-id <label>.
# Fetches from the platform secret store; nothing is stored on this host.
set -eu

label=""
while [ $# -gt 0 ]; do
case "$1" in
  --vault-id) label="$2"; shift 2 ;;
  *) shift ;;
esac
done

case "$label" in
dev|staging|prod) ;;
*) echo "refusing unknown vault-id" >&2; exit 1 ;;
esac

# Writes ONLY the password to stdout. Diagnostics go to stderr, and
# must never contain the password - see the warning below.
secret-store get "ansible/vault/$label" 2>/dev/null

Two design points in that script, both load-bearing.

It fails closed. An unrecognised label exits non-zero rather than falling through to a default. A client script that returns the production password for any label it does not recognise is worse than no scheme at all, because it looks like a scheme.

It writes only the password to standard output. Anything else on stdout becomes part of the password — a debug line, a trailing banner, a warning from a tool you called. This is the most common bug in a hand-written client script and it presents as a decryption failure with a correct password, which is a miserable thing to debug.

Ranking the options

prompt — nothing at rest anywhere. Correct for interactive work against production, and impossible for unattended runs. Use it as the default for a workstation, and stop treating “but I have to type it” as a problem to engineer away: for production material, being unable to run unattended from a laptop is the feature.

Plain file — the common answer. Acceptable when, and only when, the file is mode 0600, owned by a dedicated automation account, on a machine whose login access is itself the control. Its weaknesses are that it does not expire, its access is not logged, and revoking it means finding every copy.

Client script fetching from a secret store — the best of these for an unattended controller. Nothing at rest, access is logged and revocable centrally, and one script serves every label. The cost is a hard dependency on the store, and the same trade-off appears again in the external secret managers lesson.

Keyring — a good fit for an engineer’s workstation. The password lives in the desktop keyring, unlocked once per session by the user’s login. A small script reading from it is an executable password source like any other. It is not a fit for a headless controller, where there is nobody to unlock it.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A repository has an encrypted vault.yml and a .vault-pass file at mode 0644 in the same directory, listed in .gitignore. What has the encryption actually achieved?

  2. Q2. On 2.21.3, what distinguishes a vault password script named vault-client.sh from one named vault-pass.sh?

  3. Q3. Which are correct design rules for a vault client script? Select all that apply.

  4. Q4. Fetching the vault password from a secret store at run time changes offboarding from asking who holds a copy to asking which identities are authorised.

Passing score: 75%. Answers are checked in this browser.