Vault in Practice
Encrypting the secret is half the job; supplying the key at run time is the other half, and how you do it differs between a human at a terminal and a CI runner with no human at all. --ask-vault-pass prompts interactively, --vault-password-file reads the key from a file or an executable script, and vault_password_file in ansible.cfg makes that the default.
That last combination is how a pipeline decrypts Larkspur's secrets without anyone typing a passphrase. The decision is always the same: a human running a one-off can be prompted, but anything repeated or automated needs the key to arrive without a keystroke — and the right mechanism depends on where the key is allowed to live.
--ask-vault-pass--vault-password-fileInteractive Prompt
ansible-playbook site.yml --ask-vault-pass stops at the start of the run and asks for the passphrase once, then holds it in memory for the whole play. This is fine for a human running a one-off deploy from their laptop — type the passphrase, the run proceeds, nothing lands on disk. It is also completely useless for anything automated, because there is no human to answer the prompt, which is the trap that ends this topic.
Password File
--vault-password-file ~/.vault_pass reads the key from a file instead of asking. Ansible never writes to that file; you create it once, and its only job is to hold the passphrase. Crucially, you never commit it — it lives outside the repo, often in your home directory with tight permissions. This moves the secret-of-the-secret out of your shell history, where --ask-vault-pass typed inline would otherwise leave it.
# human at a terminal — prompted once ansible-playbook site.yml --ask-vault-pass # key read from a file outside the repo ansible-playbook site.yml --vault-password-file ~/.vault_pass # key read from an executable that prints it to stdout ansible-playbook site.yml --vault-password-file ./vault-pass.sh
Vault Password Script
Point --vault-password-file at an executable instead of a flat file and Ansible runs it, then reads the passphrase from its stdout. That one indirection changes everything: the script can fetch the key from a system keychain, an environment variable, or a cloud secret manager rather than storing it on disk at all. The file Ansible reads is now a tiny program, and the actual passphrase lives wherever the program goes to get it.
#!/usr/bin/env bash # CI injects ANSIBLE_VAULT_PASSWORD as a masked secret; # this script echoes it to stdout and nowhere else. echo "$ANSIBLE_VAULT_PASSWORD"
The same script works for a developer whose keychain exports the variable and for a runner that injects it as a masked secret. Nothing about the passphrase is hard-coded; the script is just a pipe from wherever the key lives to Ansible's stdin reader.
CI Without a Human
A pipeline run has no terminal, so the key has to arrive another way. The runner exports the passphrase as a masked secret into an environment variable, the one-line password script above echoes it, and ansible-playbook reads the value, decrypts in memory, and runs non-interactively. The passphrase exists only in the runner's secret store and, briefly, in the run's memory — it never touches the repo or a committed file, which is the whole point of doing it this way.
This is the model the spine demands at CI scale. The encrypted vault.yml is in git, the key is in the runner's vault, and the two meet only for the length of one job. Larkspur's deploy pipeline decrypts vault_db_password exactly this way: nothing decrypted persists past the job, and no engineer ever types the passphrase into the pipeline.
Defaulting It in ansible.cfg
Passing --vault-password-file on every invocation is repetitive and easy to forget, so push it into config. Set vault_password_file = ./vault-pass.sh under [defaults] in ansible.cfg, and every ansible and ansible-playbook run uses that source automatically. Engineers and CI both stop passing the flag, and the project has exactly one place that declares where the key comes from.
[defaults] vault_password_file = ./vault-pass.sh # relative to the repo, works everywhere inventory = ./inventory/prod.ini
Make that path relative to the repo, not an absolute path into one engineer's home directory. A relative ./vault-pass.sh resolves the same way on every laptop and on the runner; an absolute /home/maria/.vault_pass works for Maria and fails for everyone else and every CI job, which is the last mistake in this topic.
- Committing the
--vault-password-filetarget — a literalvault_pass.txt— to the repo so CI can find it, which ships the key alongside the ciphertext and makes the encryption decorative. The runner must inject the key, never the repo. - Wiring
--ask-vault-passinto a CI job and watching the pipeline hang forever, waiting on a prompt that no human will ever answer. - Writing a vault password script that prints the passphrase to stderr or logs it "for debugging," so the key lands in the CI build log in plaintext.
- Setting
vault_password_filein a committedansible.cfgto an absolute path that exists only on one engineer's laptop, so every other run and every CI job fails to find the key. - Typing the passphrase inline on the command line to avoid a prompt, leaving it sitting in shell history where the next person with the box can read it.
- Use
--ask-vault-passfor interactive one-offs and a--vault-password-file— orvault_password_fileinansible.cfg— for anything repeated or automated. - Have CI inject the passphrase as a masked environment variable and read it through a tiny password script, so the key lives only in the runner's secret store and in memory.
- Point
vault_password_fileat an executable script when the key should come from a keychain or secret manager, so the passphrase is never a file at rest. - Keep the password-file path out of the repo and out of shell history, treating the vault passphrase with the same care as the secrets it unlocks.
- Make the configured path relative to the repo so it resolves identically on every laptop and runner, never an absolute path into one person's home directory.
Knowledge Check
Why does --ask-vault-pass break a CI job?
- It stops to prompt for the passphrase interactively, and with no human at the runner the pipeline hangs forever waiting
- It requires an interactive TTY that hardened CI runners are forbidden from allocating under their default container security policy, so the flag is rejected before the play even starts
- It only works with whole-file encryption, never the inline
encrypt_stringsecrets CI relies on - It re-encrypts the vault file partway through the run, corrupting the blob for the next job
What does a vault password script add over a flat password file?
- It can fetch the key from a keychain, environment variable, or secret manager at run time, so the passphrase need never sit on disk at all
- It encrypts the passphrase a second time on the way out before handing it to Ansible
- It lets a whole team share one common passphrase at run time, brokering the key through the script so that no individual engineer on the rota ever learns or sees the actual value
- It becomes mandatory the moment a run carries more than one vault ID at once
How does a CI runner supply the vault key without committing it?
- It injects the passphrase as a masked environment variable that a one-line password script echoes to stdout, so the key stays in the runner's secret store
- It commits a second-layer encrypted copy of the passphrase into a dedicated file in the repo that only the CI runner, holding a build-time key, is able to decrypt at the start of each job
- It reads the passphrase straight out of the
$ANSIBLE_VAULTheader line of the encrypted file - It prompts the engineer who triggered the pipeline to type the key over a webhook callback
What does setting vault_password_file under [defaults] in ansible.cfg change for the project?
- Every
ansibleandansible-playbookrun uses that key source automatically, so engineers and CI stop passing the flag and one place defines where the key comes from - It encrypts
ansible.cfgitself in place under the same vault passphrase, so the project config and every directive inside it cannot be read or parsed without first supplying the key - It disables the
--ask-vault-passflag entirely across the whole project from then on - It uploads the passphrase into the control node's system keychain on the very first run
You got correct