Skip to main content
RunBook Academy

← All runbooks in Ansible

high riskservice affecting~75 min

Runbook: Rotate Vault credentials

1 · Prerequisites

Confirm every item is in place before any state change.

2 · Pre-checks

Read-only diagnostic commands. If any of these don't match expected output, stop and investigate further.

  • · Every encrypted file in the repository has been enumerated, including files outside group_vars and host_vars
  • · The vault IDs in use are listed, and which files belong to which ID is known from the file headers not from memory
  • · Every consumer of the current password is listed: each controller, each CI runner, each operator, each scheduled job
  • · The reason for rotation is recorded - scheduled, staff change, or exposure - because exposure also requires rotating the SECRETS INSIDE the vault, not just the password
  • · The current password is confirmed working by decrypting one file before anything is changed
  • · A branch protection or freeze is in place so nobody merges a newly encrypted file mid-rotation

3 · Procedure

Execute each step in order. Verify the expected output of a step before moving to the next.

  1. 1Announce a merge freeze on files under vault control for the duration
  2. 2Enumerate every encrypted file and record its vault ID from the header line
  3. 3Confirm the old password decrypts every one of them - a file that fails now will fail worse later
  4. 4Generate the new password and store it in the password manager BEFORE using it anywhere
  5. 5Rekey every file on a branch, with both passwords available to the rekey command
  6. 6Verify each rekeyed file decrypts with the new password and does NOT decrypt with the old one
  7. 7Verify the decrypted content is byte-identical to what it was before the rekey
  8. 8Distribute the new password to every consumer identified in the pre-checks
  9. 9Merge the rekeyed branch and immediately run a check-mode playbook from a controller using the new password
  10. 10Retire the old password from the password manager and from every controller filesystem
  11. 11If the rotation is due to exposure, rotate the secrets the vault contains as a separate follow-on

4 · Verification

Confirm the procedure actually fixed the problem.

  • ansible-vault view succeeds with the new password on every encrypted file in the repository
  • ansible-vault view FAILS with the old password on every encrypted file - this check must be able to fail and must be run
  • A diff of the decrypted plaintext before and after the rekey is empty for every file
  • A check-mode playbook run from each controller completes without a decryption error
  • The CI pipeline completes a run using the new password, on a branch, before the freeze is lifted
  • No controller filesystem still contains a vault password file holding the old value

5 · Rollback

If verification fails, undo the procedure in reverse order.

  • While the rekeyed branch is unmerged, rollback is deleting the branch - the repository still uses the old password
  • If the rekey is merged and a consumer cannot be updated, rekey BACK to the old password with the same procedure; both passwords are still known at that point
  • POINT OF NO RETURN: once the old password is destroyed in the password manager, files still encrypted under it anywhere - old branches, stale clones, backups - are unrecoverable
  • Before destroying the old password, check long-lived branches and tags for encrypted files that the rekey did not touch, because the rekey only ran on the branch you checked out
  • If a controller is mid-run when the merge lands, let the run finish; it holds its password in memory and is unaffected

6 · Escalation

When the runbook isn't enough, contact:

  • · Escalate to the security owner immediately if the rotation is a response to exposure - rotating the vault password does not change the secrets inside, and those are what leaked
  • · Escalate if any file cannot be decrypted with the current password before the rotation starts; that file is already orphaned and rekeying will not fix it
  • · Escalate to the CI owner before merging if the pipeline cannot be updated with the new password in the same window
  • · Escalate if encrypted files exist on branches nobody owns - deleting the old password strands them permanently

Rotating a vault password is a repository-wide, all-or-nothing change with a distributed set of consumers. The failure is not dramatic: the rekey succeeds, the merge lands, and then one CI runner and two operators discover at different times over the next fortnight that they can no longer decrypt anything.

Read the first callout before anything else. It decides whether this is the right runbook.

When to use this runbook

  • Scheduled vault password rotation.
  • A person with the password has left the team.
  • The password was handled carelessly and needs replacing.
  • Splitting a single password into per-environment vault IDs (the rekey mechanics are the same; the target ID differs per file).

Blast radius

Every consumer of the password, which is more than the fleet:

  • Every controller with a vault password file.
  • Every CI runner and every pipeline secret.
  • Every operator who runs playbooks by hand.
  • Every scheduled job, timer or platform credential.
  • Every stale clone on someone’s laptop.

Managed hosts are not in the blast radius. Nothing changes on them. What breaks is the ability to run automation at all, which is worse in a different way: you find out during the next incident.

Inputs

  • The current vault password, from the password manager.
  • The list of encrypted files and their vault IDs.
  • The consumer list.
  • A merge freeze window.

Step 1: Freeze and enumerate

Read-only / Safefind every encrypted file
cd /srv/automation/repo
grep -rl --binary-files=without-match '^\$ANSIBLE_VAULT' . \
--exclude-dir=.git | sort | tee vault-files.txt
wc -l vault-files.txt

Then read the header of each one, because the header carries the vault ID:

Read-only / Saferead the vault IDs
while read -r f; do
printf '%-60s %s\n' "$f" "$(head -1 "$f")"
done < vault-files.txt

A 1.2 format header ends with the vault ID label:

inventories/production/group_vars/all/vault.yml   $ANSIBLE_VAULT;1.2;AES256;prod
inventories/staging/group_vars/all/vault.yml      $ANSIBLE_VAULT;1.2;AES256;dev

A 1.1 header has no label, which means the file was encrypted without a vault ID and any configured password will be tried against it. Note those separately - they are the files most likely to be missed by an ID-scoped rekey.

Step 2: Prove the current password works everywhere

Read-only / Safedecrypt-check every file
rc=0
while read -r f; do
if ansible-vault view --vault-id prod@/home/ansible/.vault-pass "$f" >/dev/null 2>&1; then
  echo "ok      $f"
else
  echo "FAILED  $f"
  rc=1
fi
done < vault-files.txt
exit $rc

Any FAILED line here is a file that is already orphaned - encrypted under a password nobody has, or under a different vault ID. Resolve that before rotating. Rekeying cannot fix a file you cannot open, and after the rotation you will have two passwords that do not work on it instead of one.

Step 3: Generate and store the new password first

Configuration changegenerate
python3 -c 'import secrets; print(secrets.token_urlsafe(32))' \
> /home/ansible/.vault-pass-new
chmod 600 /home/ansible/.vault-pass-new

Put it in the password manager before you use it. A password that exists only in a file on one controller, and is then used to rekey the whole repository, is one disk failure away from making every encrypted file in your automation unreadable.

Do not type it on a command line. ansible-vault takes --vault-password-file and --vault-id label@file precisely so that the value never appears in the process table or in shell history.

Step 4: Rekey on a branch

Configuration changegit checkout -b
git checkout -b chore/vault-rekey-2026-08
git status --short
Configuration changeansible-vault rekey
while read -r f; do
ansible-vault rekey \
  --vault-id prod@/home/ansible/.vault-pass \
  --new-vault-id prod@/home/ansible/.vault-pass-new \
  "$f"
done < vault-files.txt

Verified on 2.21.3: rekey prints Rekey successful per file, keeps the vault ID label in the header ($ANSIBLE_VAULT;1.2;AES256;prod), and rewrites the file in place.

If you are moving a file to a different vault ID at the same time, that is the --new-vault-id label, and the header changes accordingly. Do one thing at a time: a rotation that also reorganises IDs makes the “which files did I miss” question much harder to answer.

Step 5: Verify - new works, old fails, content unchanged

Three checks. All three matter, and the second one is the one people leave out.

Read-only / Safenew password decrypts
while read -r f; do
ansible-vault view --vault-id prod@/home/ansible/.vault-pass-new "$f" >/dev/null \
  && echo "new ok   $f" || echo "NEW FAILED $f"
done < vault-files.txt
Read-only / Safeold password must FAIL
while read -r f; do
if ansible-vault view --vault-id prod@/home/ansible/.vault-pass "$f" >/dev/null 2>&1; then
  echo "STILL OPENS WITH OLD PASSWORD: $f"
else
  echo "rekeyed ok                     $f"
fi
done < vault-files.txt

Verified failure message on 2.21.3, which is what you should expect to see suppressed above:

Decryption failed (no vault secrets were found that could decrypt).

A file that still opens with the old password was not rekeyed - most often because it was added to the repository after vault-files.txt was generated, which is what the merge freeze exists to prevent.

Read-only / Safecontent is unchanged
while read -r f; do
git show "HEAD:$f" > /tmp/old.vault
a=$(ansible-vault view --vault-id prod@/home/ansible/.vault-pass /tmp/old.vault | sha256sum)
b=$(ansible-vault view --vault-id prod@/home/ansible/.vault-pass-new "$f" | sha256sum)
[ "$a" = "$b" ] && echo "identical $f" || echo "CONTENT CHANGED $f"
rm -f /tmp/old.vault
done < vault-files.txt

This is the check that catches an editor that reformatted a file, a truncated write, or a rekey that ran against a partially written file. CONTENT CHANGED on any line stops the rotation.

Step 6: Distribute before you merge

Work down the consumer list from the pre-checks. For each one, the new password must be in place before the merge, because the moment the rekeyed files land on the main branch, anything still holding the old password is broken.

  • Controllers - write the new password file, keep the old one alongside temporarily.
  • CI - update the pipeline secret; do not rely on a cached value.
  • Operators - the password manager entry, and a note that they must refresh their local password file.
  • Scheduled jobs and platform credentials - these are the ones that get forgotten, because nobody is watching when they run.

A controller can hold both during the transition, which removes the timing pressure entirely:

Configuration changeboth passwords available
[defaults]
vault_identity_list = prod@/home/ansible/.vault-pass-new, prod_old@/home/ansible/.vault-pass

With both listed, files encrypted under either password decrypt. That is a transition aid and a security weakness at the same time, so it is a line in the change record with a removal date, not a permanent configuration.

Step 7: Merge, then prove it from a controller

Configuration changemerge
git push -u origin chore/vault-rekey-2026-08
# open the merge request, get it approved, merge

Immediately after the merge, from each controller:

Read-only / Safecheck-mode proof
cd /srv/automation/repo && git pull --ff-only
ansible-playbook site.yml --limit staging-web01.example.com --check
echo "exit=$?"

Exit code 0 and no decryption error. A vault failure surfaces as a task-level error mentioning decryption, and it fails at the point the variable is first needed - which can be several tasks in, not at startup.

Step 8: Retire the old password

Destructiveretire
# On every controller
shred -u /home/ansible/.vault-pass
mv /home/ansible/.vault-pass-new /home/ansible/.vault-pass

# Remove the transitional dual-identity line from ansible.cfg
# Archive, then delete, the old password manager entry

Also remove the transitional vault_identity_list entry. A controller left holding both passwords indefinitely means the rotation achieved nothing: the old credential still opens everything.

Step 9: If this was exposure, rotate the contents

Re-read the callout at the top. If the password leaked, the plaintext leaked. Enumerate what the vault contained and rotate each item through its own procedure - database passwords, API tokens, certificates, service account keys. That work is tracked separately and it is the part that actually closes the exposure.

Rollback

Stage reachedRollback
Branch created, files rekeyed, not mergedDelete the branch. Repository unchanged.
Merged, a consumer cannot be updatedAdd the old password as a second vault_identity_list entry on that consumer as a stopgap, then fix it properly.
Merged and you want to reverse entirelyRekey back with the passwords swapped. Both are still known.
Old password destroyedNo rollback. Files encrypted under it are unrecoverable.
Configuration changerekey back
while read -r f; do
ansible-vault rekey \
  --vault-id prod@/home/ansible/.vault-pass-new \
  --new-vault-id prod@/home/ansible/.vault-pass \
  "$f"
done < vault-files.txt

Common patterns

SymptomLikely causeResolution
Decryption failed (no vault secrets were found that could decrypt)Wrong password, or the file’s vault ID is not in vault_identity_listhead -1 the file to read its ID; match the identity
One file rekeyed, others silently skippedThe file list was generated before someone merged a new encrypted fileRe-enumerate; that is what the freeze prevents
rekey refuses a fileIt is not entirely encrypted - it contains inline !vault blocksRe-encrypt those strings individually
Playbook fails several tasks in, not at startVault decryption happens when the variable is first neededRead which variable; that names the file
CI passes, operators failThe pipeline secret was updated, the password manager entry was notUpdate both; they are separate consumers
Old release branch cannot be deployedIts encrypted files were never rekeyed and the old password is goneUnrecoverable; this is why Step 8 checks refs
Rotation “done” but the old password still opens filesTransitional vault_identity_list never removedRemove it; re-run the must-fail check

Escalation

Escalate when:

  • The rotation is a response to exposure. The secrets inside need rotating and that is a security-owned workstream.
  • A file cannot be decrypted with the current password before you start.
  • CI cannot be updated inside the window.
  • Encrypted files exist on branches with no owner, and destroying the old password would strand them.

References

  1. Protecting sensitive data with Ansible Vault
  2. Managing vault passwords
  3. ansible-vault CLI