skip to content

How does the Ansible Vault password reach an automated CI run, given that nobody is there to type it and it must not be committed to the repository?

level: seniorimportance: must knowfreq 60%

answer

  1. no TTY, so no prompt
  2. CI secret store to a private temp file
  3. umask, trap, outside the workspace
  4. never on the command line
  5. executable script prints the password

basics

~20 s

The CI system injects the password from its own secret store, the job writes it to a private temporary file, and Ansible reads it with --vault-password-file or the ANSIBLE_VAULT_PASSWORD_FILE variable. Interactive prompting cannot work without a terminal.

solid answer

~50 s

Ansible accepts a vault password three ways: an interactive prompt, a file, or a labelled identity. CI has no terminal, so `--ask-vault-pass` is out and the answer is always a file. The pipeline pulls the password from the CI platform's encrypted secret store, writes it to a file under the job's temporary directory with a restrictive umask, points `--vault-password-file` or `ANSIBLE_VAULT_PASSWORD_FILE` at it, and deletes it in a trap so it does not survive into a cached workspace. The stronger variant is to make that file an executable script: Ansible runs it and takes stdout as the password, so the script can fetch a short-lived credential from a secret manager and nothing is ever written to disk. The details that separate a real answer from a hand-wave are keeping the value out of the process table and the logs, not leaving it in the workspace on a shared runner, and having one bootstrap secret rather than the vault password pasted into every repository's settings.

code

bash · 7 lines
bash
set -euo pipefail
umask 077                                  # 0600, not readable by other runner users
pwfile="$(mktemp)"                         # outside the checked-out workspace
trap 'rm -f "$pwfile"' EXIT                # removed even when the play fails
printf '%s' "$ANSIBLE_VAULT_PASS" > "$pwfile"   # injected by the CI secret store

ansible-playbook site.yml --vault-password-file "$pwfile"

go deeper

for a junior

Know that automation reads the vault password from a file via --vault-password-file or the ANSIBLE_VAULT_PASSWORD_FILE variable, because the interactive prompt needs a terminal that CI does not have.

for a middle

Explain the bridge from the CI secret store to that file: create it with a restrictive umask outside the workspace, point Ansible at it, and delete it in a trap when the job ends.

for a senior

Demonstrate the operational care — no secret in the process table or logs, nothing left on a persistent runner, and the executable password script that fetches from a secret manager so nothing is written to disk at all.

for a principal

Own the trust chain: reduce the estate to one bootstrap credential issued to the pipeline's identity, decide which secrets deserve a managed store rather than Vault, and make rotation a single action instead of a project.

## Why the prompt is not an option `--ask-vault-pass` requires a TTY. A CI job has no interactive terminal, so the entire question reduces to: how does a password get onto the runner without being in git? Every workable answer routes through the CI platform's own encrypted secret store — the place designed to hold values that must not be in the repository — and then bridges from an environment variable to the file interface Ansible actually wants. ## The file bridge, done carefully The minimal correct shape is short but every line is there for a reason: ```bash set -euo pipefail umask 077 # file is 0600, not world-readable pwfile="$(mktemp)" trap 'rm -f "$pwfile"' EXIT # gone even if the play fails printf '%s' "$ANSIBLE_VAULT_PASS" > "$pwfile" # from the CI secret store ansible-playbook site.yml --vault-password-file "$pwfile" ``` Points to be able to defend: - **Never pass the secret as a command-line argument.** Anything on the command line is visible in `ps` to every other process on the machine, and CI logs frequently echo the command being run. Ansible has no flag that takes the password inline, which is a deliberate design choice — respect it rather than working around it with a `<(echo ...)` process substitution whose contents can still leak. - **`umask 077` before creating the file.** On a shared or self-hosted runner, a world-readable password file in a shared temp directory is a real exposure. - **Clean up in a trap, not on the happy path.** A failed play must not leave the password behind for the next job, especially on persistent runners that reuse the workspace. - **Write it outside the checked-out repository.** A file inside the workspace can be swept into a build artifact, a Docker build context, or a workspace cache. It can also be committed by an over-eager `git add -A` in a later step. - **Do not `echo` it.** Most CI systems mask registered secrets in log output, but masking is best-effort — it fails on base64 or otherwise transformed values. Treat masking as a safety net, not a control. `ANSIBLE_VAULT_PASSWORD_FILE` (or the matching `ansible.cfg` setting) does the same job without repeating the flag on every invocation, which matters when the pipeline calls `ansible-playbook`, `ansible`, and `ansible-vault view` in different steps. With multiple environments, the identity-list form is the same idea with labels. ## The better shape: an executable password script Ansible accepts an executable file for `--vault-password-file` and takes its stdout as the password. That single hook is what lets you avoid persisting the secret at all: ```bash #!/usr/bin/env bash # vault-pass.sh -- fetch the vault password from a secret manager set -euo pipefail fetch-secret ansible/vault-password # prints the password on stdout ``` Now the repository can contain the script (it holds no secret), the runner authenticates to the secret manager with a short-lived workload identity rather than a stored password, and nothing sensitive touches the filesystem. It also gives you a rotation path: rotate in the manager, and every pipeline picks up the new value on its next run with no pipeline edits at all. ## The recursion problem, and the honest answer Someone will point out that you have not eliminated the secret, only moved it: the credential the runner uses to reach the secret manager is itself a secret. That is the correct observation and the correct answer is that you reduce the problem to exactly one bootstrap credential, held in one place, ideally short-lived and issued to the pipeline's identity rather than stored at all. What you are avoiding is the anti-pattern where the same vault password is pasted into a dozen repository settings pages, a shared password manager, three laptops, and a wiki, so that rotating it is a project rather than a command. ## What to say about scope The last piece of judgment is which secrets belong behind Ansible Vault at all. A vault password unlocks every value in the files it protects, with no per-user access, no expiry, and no record of who decrypted what. For an estate where credentials are long-lived and the team is small, that is a reasonable trade. When the requirement is short-lived database credentials, per-team access, or an audit trail, the answer is that the playbook reads those at run time from a managed store and Vault protects only the bootstrap credential — which is also why the executable-script form of `--vault-password-file` is worth knowing.

  • Why is there no flag to pass the vault password directly on the command line?
    Because arguments are visible in the process table to any other user on the host and are routinely echoed into CI logs and shell history. Ansible deliberately offers only a prompt, a file, or an executable source. Working around it — for example with process substitution that materialises the value in a command line — reintroduces the same exposure.
  • What is wrong with writing the password file inside the checked-out repository directory?
    Anything in the workspace can be swept into a build artifact, a Docker build context, a workspace cache reused by the next job, or an accidental `git add -A`. Write it to the job's temporary directory instead, with a restrictive umask, and remove it in a trap so a failed run does not leave it behind on a persistent runner.
  • Your pipeline runs against dev and prod in the same job. How do you avoid giving it the production password for the dev step?
    Split the steps so each invocation carries only the identity it needs, supplying `--vault-id dev@...` for the dev play and the production identity only in the step that deploys production. Better still, run production from a separate job or pipeline whose secret scope contains only the production password, so a compromised dev step never has it in its environment.

saying these in an interview costs you the question

  • Suggesting --ask-vault-pass in a pipeline that has no terminal
  • Passing the password as a command-line argument or echoing it
  • Committing the password file with a .gitignore entry as the safeguard
  • Relying entirely on the CI log masking to keep it secret
  • Leaving the temp password file behind on a persistent self-hosted runner

context