Ansible repositories are unusually attractive targets. A single clone
can contain the credentials for an entire estate — database passwords,
API tokens, the automation account’s access to every host — organised
neatly by environment, with a README explaining what each one is for.
That is fine, and it is what vault is for. This lesson is about the four
ways the protection gets defeated, and about why the remediation is much
larger than people expect.
The four shapes
1. Plaintext, uncommented
Read-only / Safethe obvious form— Read-only: file contents, shown for recognition. Do not commit this.
Easy to find, easy to lint for, and it is not the one that gets you.
2. The vault password committed alongside the vault
The variables are properly encrypted. ansible.cfg says
vault_password_file = .vault_pass. And .vault_pass is in the
repository, because somebody wanted CI to work.
The encryption is now decoration. Anyone with a clone has both halves.
3. A secret in a file nobody classified as a secret
The one that actually happens. It is not in group_vars; it is in:
A dynamic-inventory config file with an API token in it
A Makefile or wrapper script with export ANSIBLE_VAULT_PASSWORD=…
A test fixture with “fake” credentials that turned out to be real
A .env file that a .gitignore rule stopped covering when it moved
A commented-out line kept “just in case”
A CI configuration file with an inline token
4. A secret that was removed
Committed on Tuesday, removed on Wednesday, and still in the history —
which is the property that makes this anti-pattern different from every
other one in this part.
The failure: remediation is a fleet-wide rotation
This is the part that gets underestimated, because “delete the file and
force-push” feels proportionate to “committed a file by mistake”.
The actual work:
Enumerate what leaked. Not just the obvious value - every credential in every commit in the affected range, because the same push often carried more than one.
Determine the blast radius per credential: which hosts, which services, which third parties accept it. A database password may be in six application configs; an API token may be in use by two other teams.
Rotate at the source: generate new credentials in the database, the cloud console, the vendor portal.
Distribute the new values to every consumer - which is an Ansible run, against production, under time pressure.
Retire the old credential, which is a separate step and the one most often skipped. A rotated-but-not-revoked credential is still valid.
Verify. Prove the old value no longer authenticates, per service, rather than assuming the revocation applied.
Rewrite the history if you wish, as hygiene. It is the last step and the least important one.
Prevention that works
A pre-commit hook that scans staged content. It catches the mistake before the push, which is the only point at which the cost is still small. Part XXXVIII covers the CI half; the local hook is what saves you.
Secret scanning in CI as the second line, so a bypassed hook is still caught - though by then it is pushed, and the clock has started.
A .gitignore for the vault password file plus a check that it is genuinely absent. .gitignore does not untrack a file that is already tracked, which is a common and expensive surprise.
The vault password from an environment variable or a file outside the repository, per Part XXI - and in CI, from the CI system secret store rather than from a file in the checkout.
A review habit: any diff that adds a long high-entropy string gets read carefully, whatever the filename.
Read-only / Safeaudit what is currently tracked— READ-ONLY: reads the index and history. Run it now rather than after an incident.
cd /srv/ansible
# Is the vault password file tracked, despite .gitignore?
git ls-files | grep -E 'vault[-_]?pass|\.vault'
# Files that should be encrypted but are not.
grep -rLE '^\$ANSIBLE_VAULT' --include='vault*.yml' group_vars/ host_vars/ 2>/dev/null
# Any occurrence of a likely secret assignment across all history.
git log -p --all -S'password' -- '*.yml' | head -40
The same reflex, elsewhere: host_key_checking = False
This belongs in the same lesson because it comes from the same decision,
made in the same five seconds: a control was inconvenient, so it was
removed rather than configured.
Read-only / Safethe line, and what it costs— Read-only: configuration shown for recognition. This setting applies to every host in every run made from this controller.
[defaults]
host_key_checking = False
It appears because the first run against new hosts prompted for host key
acceptance and the run was in CI where nothing could answer. That is a
real problem with a real solution, and this is not it.
The failure. SSH host key verification is what detects a
man-in-the-middle. Disabled, the controller will connect to whatever
answers on that address and hand it the automation account’s
credentials, the vault-decrypted variables for that host, and whatever
become password the play uses. A DNS change, an ARP spoof, or an IP
reassigned to a different machine is now indistinguishable from the real
host.
Note the asymmetry with the secrets case: this one has no incident until
it has a very large one, and no signal in between.
The corrected forms, in order of preference: populate known_hosts
from a trusted source at provisioning time; use ssh-keyscan as an
explicit, reviewed step whose output is checked in and versioned; or
manage entries with ansible.builtin.known_hosts driven from inventory.
Part XLVII covers all three, and Part XX covers which settings should
never be weakened.
Knowledge check
Knowledge check · 4 questions
Q1. An API token is committed, noticed within an hour, and removed in the next commit with the history rewritten and force-pushed. What is the correct assessment?
Q2. Which of these are genuine instances of this anti-pattern that a naive scan of group_vars would miss? Select all that apply.
Q3. The main cost of committing a secret is the leak itself, so a repository that is private and access-controlled carries little risk.
Q4. Why does this lesson treat `host_key_checking = False` as the same anti-pattern as committing a secret?
Passing score: 75%. Answers are checked in this browser.