Annual rotation of the bootstrap-tier secrets
- When
- Once a year, when SecretRotator announces a bootstrap marker: Telegram warns 28, 21 and 14 days before it falls due and every night from 13 days, and
secret-rotator uilists its box. At once on suspicion that a workstation or Roboform is compromised.
The bootstrap tier is what OpenBao cannot hold because it gates OpenBao or decrypts the repo.
SecretRotator holds six as the marker keys rotator/bootstrap/* (AnsibleSpecs
secret-rotation/catalog.md), each external at 365 d: it never touches their values, and
pressing Done on a key's box in secret-rotator ui records its rotation
(external-key-due.md).
Each section below ends with that Done. Four rotate on this card; the seal key and the backup age key
rotate only on compromise (end of card). The vault rekey comes first, so everything re-vaulted
after it lands under the new passphrase.
Before you start #
- Roboformthe "operator workstation" entry (the ansible-vault passphrase),
homelab-ca JWK provisioner password, the GitHub login - A checkout with the vault unlocked
ANSIBLE_VAULT_PASSWORD_FILEpointing atansible/.vault_pass, per operator-workstation.md - SSH to srviac, to edit
/etc/iac/secrets.yaml(layout:secrets.example.yaml) - A window with no
IaC/Scheduled *job due.IaC/Apply,IaC/Scheduled Drift(daily,H 11) andIaC/Scheduled Certs(Fridays,H 4) run Ansible on srviac and read vaulted values; while the repo and srviac disagree on the passphrase, they fail. secret-rotator uion srviacthe VS Code task secret-rotator ui (srviac), for the Done that ends each section
1. The ansible-vault passphrase #
Not yet exercised
No rekey has been run on this repo. Three files are vaulted: roles/openbao/files/static.key
whole, and four inline !vault | values in inventories/prd/group_vars/all/vips.yml
(vrrp_auth_password, internal_tls_jwk_provisioner_password) and
inventories/prd/group_vars/openbao.yml (openbao_admin_role_id,
openbao_admin_secret_id). ansible-vault rekey takes the
whole-file vault; the inline values are re-encrypted one by one with encrypt_string, the
form each file's own header gives. Paths below are under ansible/.
-
New passphraseat least 20 random characters or 8 diceware words. Save it in Roboform as a second entry marked
(new)until step 10. -
With the old passphrase still in
.vault_pass, read each inline value back. One at a time, the shape openbao.md §1 uses; keep the output in a file under/dev/shm/, nowhere else:cd ansible && poetry run ansible srvvault1 -m debug -a 'msg="{{ <variable> }}"' -
Rekey the whole-file vault. The old passphrase comes from
ANSIBLE_VAULT_PASSWORD_FILE; type the new one at its prompt:poetry run ansible-vault rekey roles/openbao/files/static.key -
Switch
.vault_passto the new passphrase, typed at a hidden prompt so it stays out of the shell history:read -rs NEW && printf '%s' "$NEW" > .vault_pass && unset NEW. -
Re-encrypt each of the five values under the new passphrase and replace its
!vault |block in the file. Paste the value at the hidden prompt, never on the command line:read -rs VALUE && printf '%s' "$VALUE" | poetry run ansible-vault encrypt_string --stdin-name <variable>; unset VALUE -
Prove it: step 2's read of each variable again, now under the new passphrase. Then
shred -uthe scratch file.git diffmust touch only the three vault files. Commit; do not push yet. -
srviacin
/etc/iac/secrets.yaml, thefiles:entry/etc/iac/ansible_vault_passgets the new passphrase as itscontent:. No restart: everyiacrun reads the file fresh. -
Push.
IaC/Build-Mainruns by itself. Then startIaC/Scheduled Driftby hand: it converges nothing, and a vault error there means the repo and srviac disagree. -
KubeCoderthe catalog key
ansible-vault-passwordinkv/eso/prd/kubecoder/prd/catalogis what every Ansible environment reads at start as/run/secrets/ansible-vault-password.patch, neverput:putreplaces the bag whole (argocd.md does the same for another key). With the passphrase in$VALUEfromread -rs VALUE, never on the command line:. scripts/bao-login.sh && printf %s "$VALUE" | cexec iac bao kv patch -mount=kv eso/prd/kubecoder/prd/catalog ansible-vault-password=- cexec iac kubectl --kubeconfig ~/.kube/config-prd-write --context prd annotate externalsecret -n kubecoder-prd kubecoder-secret-catalog force-sync=$(date +%s) --overwriteThen
kc env restarteach Ansible environment: a running one keeps the old passphrase until it restarts. One that still has~/.ansible/vault-passholds the old passphrase in plain text; delete it. -
Roboformthe "operator workstation" entry gets the new passphrase; delete the
(new)copy. Any other workstation with anansible/.vault_passgets it too. -
secret-rotator uipress Done on two boxes, one for each key this section rotated:rotator/bootstrap/ansible-vault-passphrase#ansible-vault-passphraseandeso/prd/kubecoder/prd/catalog#ansible-vault-password.
2. JWK provisioner password #
- step-ca-bootstrap.md § JWK provisioner password rotation:
new password, re-encrypt the provisioner on the CA, re-vault
internal_tls_jwk_provisioner_password, force one re-issue, update Roboform. secret-rotator uiDone onrotator/bootstrap/jwk-provisioner-password#jwk-provisioner-password.
3. GitHub PAT for TerraformState #
- iac-agent.md §
GIT_API_TOKEN: mint a PAT with the same scope (repoclassic, orContents: Read and writeonTerraformState), paste it into/etc/iac/secrets.yamlon srviac. No restart. - Revoke the old PAT at GitHub.
secret-rotator uiDone onrotator/bootstrap/git-api-token#git-api-token.
4. Jenkins inbound-agent secret #
- iac-agent.md §
JENKINS_AGENT_SECRET: regenerate on the controller, paste into/etc/iac/secrets.yaml,systemctl restart jenkins-agenton srviac. secret-rotator uiDone onrotator/bootstrap/jenkins-agent-secret#jenkins-agent-secret.
5. Record it #
- Add a row to the run log below.
Not on this card
The static seal key. Rotation is a seal migration on all three srvvault nodes, sketched
in openbao.md §5
and never written out: new key, bump openbao_seal_current_key_id, the old key id declared
until every node has migrated. On compromise only.
The backup age key. Its private half lives only in Roboform; every kept backup (OpenBao,
postgres-pas, YouTrack) is encrypted to the public half in backup-server's
backup-server-age-key ConfigMap. A rotation keeps the old private key until those age out.
No procedure exists. On compromise only. The Terraform state age key is a different key,
kv/iac/tf-backend, and re-encrypting every state is a project, not a rotation.
Run log #
| Date | Rotated | Notes |
|---|---|---|
| — | none yet | the vault rekey has never been exercised |