Vault Operations #
Ansible Vault protects sensitive variables (passwords, API keys, certificates) stored in the inventory. Each environment has its own set of vault.yml files that must be encrypted at rest and decrypted only while editing.
Repository: ansible-inventory-deevnet
Setup #
After cloning the inventory repository, run the one-time hook setup:
cd ansible-inventory-deevnet
make install-hooks
This runs git config core.hooksPath hooks, pointing Git at the version-controlled hooks/ directory. The hooks stay in sync with the repo automatically β no copying required. This must be run once per clone.
Encrypting and Decrypting #
Decrypt all vault files #
make unvault
Iterates over every vault.yml in the repo and decrypts any that are currently encrypted. Already-decrypted files are skipped.
Encrypt all vault files #
make vault
Iterates over every vault.yml in the repo and encrypts any that are currently in plaintext. Already-encrypted files are skipped.
Typical editing workflow #
make unvault # decrypt vault files
# edit secrets as needed
make vault # re-encrypt before committing
git add -u && git commit
Secrets a change produces #
Most secrets here were generated by an operator, so a lost one is regenerated. Some are not: the recovery key and AppRole credentials from OpenBao’s first init, a Proxmox API token secret, a vendor key a device shows once. The moment one of those exists it exists in exactly one place, and that place is a decrypted working tree β a file git has been told to reject. Nothing is holding it.
Lock it in before doing anything else with the change:
# 1. write the value into its vault.yml
make vault # 2. encrypt
git add -u && git commit # 3. the hook verifies the staged blob is encrypted
git push # 4. now it is off this machine
# 5. ONLY NOW delete the file the change wrote it to
Steps 3, 4 and 5 are one action, not a tidy-up for later. A change that carries on with a once-only secret still sitting in a decrypted file is one interrupted command away from having destroyed it.
This is not hypothetical. CHG-0010 lost OpenBao’s recovery key and Ansible’s AppRole exactly
this way: they were written into the decrypted vault file, the init output was deleted as the change
record instructed, and a git reset --hard run eight minutes later to work around a failed
fast-forward discarded the plaintext. With the root token revoked and no recovery key, OpenBao could
no longer be administered and had to be rebuilt.
While the tree is decrypted #
A decrypted vault file is an uncommitted modification to every vault file in the repository, and
it is uncommitted by design. Any git command that discards uncommitted work discards the plaintext
with it β and because plaintext is never staged, it is not in the object database, so there is
nothing to recover. Not the reflog, not git stash list, not git fsck --lost-found.
Never run, while decrypted:
| Command | What it does to plaintext |
|---|---|
git reset --hard | Discards it, silently and unrecoverably |
git checkout -- ., git restore . | The same |
git clean -fd | Removes a new vault file that has never been committed |
git checkout <branch>, git pull | Refuses, or carries the plaintext onto the other branch |
If a pull will not fast-forward or a branch switch is blocked, encrypt and commit first:
make vault && git add -u && git commit
git pull --rebase
Do not reach for git stash. It would put the plaintext in the object database, which is the one
thing the pre-commit hook exists to prevent, and it is not covered by the hook because nothing is
staged.
Keep the decrypted window as short as the work allows, and close it with make vault and a commit
rather than leaving it open across a session.
Example vaults #
Beside every vault.yml is a plaintext vault.example.yml: the same keys with every value empty,
and a comment on each key saying what it is and how it is generated. They are how a new site finds
out which secrets it needs without decrypting anyone else’s vaults.
Starting a new site’s vaults #
cd ansible-inventory-deevnet
make empty-vault SITE=<site> # vault.yml from every example under <site>/
It never overwrites a vault: a vault.yml that already exists is skipped and reported as
kept, and the copy itself is --no-clobber. Running it again is safe. Then fill in each value,
following its comment, and encrypt before committing anything:
make vault
A secret that is shown only once (a Proxmox token, an OpenBao init) follows Secrets a change produces: encrypted, committed and pushed before going further.
Keeping the examples current #
When a vault gains or loses a key, regenerate the examples and commit them in the same change:
make unvault
make vault-examples # rewrites every vault.example.yml from the decrypted vaults
make vault
The generator empties every value and writes nothing if a comment holds something that looks like a
secret. scripts/vault-examples.yml lists comment paragraphs that stay only in the encrypted vault,
and hints for keys whose vault has no comment. Read the diff before committing: the files are public.
Changing the vault password #
One vault password covers every vault.yml and every device backup under backups/, so a rekey
does all of them in one run. make backup-router and make backup-wireless seal backups with
the same password, and check it against a vault.yml on origin/main before sealing.
Run it in a terminal, never where the prompt’s input could be captured, and on a branch from main:
cd ansible-inventory-deevnet
ansible-vault rekey $(git ls-files | grep -E '(^|/)vault\.yml$|\.vault$')
It asks for the old password once and the new one twice. Then check the new password opens everything, printing nothing:
ansible-inventory -i mobile --list --ask-vault-pass > /dev/null && echo vault.yml ok
for f in backups/mobile/*/*.vault; do
ansible-vault decrypt --ask-vault-pass --output - "$f" > /dev/null && echo "$f ok"
done
--output /dev/null does not work: ansible-vault refuses a destination whose directory it cannot
write. If ANSIBLE_VAULT_PASSWORD_FILE is set where you work, update that file too.
Merge the rekey before the next device backup. Until main holds the rekeyed files, sealing
checks the new password against old ciphertext and refuses.
A rekey protects only what is committed from then on. Every earlier commit in this public repository still decrypts with the old password, so a rekey is not a substitute for rotating a secret.
Pre-commit Guard #
The hooks/pre-commit script runs automatically on every commit. It checks each staged vault.yml file by inspecting the staged blob (git show ":$file"), not the working tree. If any staged vault file does not begin with $ANSIBLE_VAULT, the commit is rejected with an error message. It also rejects any staged vault.example.yml that carries a value: examples hold keys only.
If your commit is blocked:
make vault
git add -u
git commit
Vault Files #
The following vault.yml files exist across the inventory:
| Path | Scope |
|---|---|
mobile/group_vars/all/vault.yml | Site-wide secrets (mobile) |
mobile/group_vars/routers/vault.yml | Router credentials (mobile) |
mobile/group_vars/switches/vault.yml | Switch credentials (mobile) |
mobile/group_vars/network_controllers/vault.yml | Omada controller credentials (mobile) |
mobile/group_vars/openbao/vault.yml | OpenBao’s bootstrap secrets: unseal share 1, Ansible’s AppRole, the Transit key (mobile) |
mobile/group_vars/deevnet_api/vault.yml | Deevnet API operator token, database password, token MAC key, Proxmox token (mobile) |
mobile/group_vars/mqtt_brokers/vault.yml | The broker’s Erlang cookie, its database passwords, the log bridge’s broker account (mobile) |
mobile/group_vars/observability_store/vault.yml | The log store’s operator read token, Grafana’s secret key (mobile) |
mobile/host_vars/dv02hyp001p01/vault.yml | dv02hyp001p01 secrets (mobile) |
mobile/host_vars/dv02hyp002p02/vault.yml | dv02hyp002p02 secrets (mobile) |
home/group_vars/all/vault.yml | The skeleton of the reserved home site, which is not built. make vault still finds it |
This list is descriptive, not a definition. make vault and make unvault discover their targets
with find . -name 'vault.yml', so a new file is picked up without being added here β regenerate
the list with that same command rather than trusting this table to be current.