infra/secrets/<env>/secrets.enc.yaml (platform)
and infra/secrets/<env>/tofu.enc.env (OpenTofu), both SOPS files encrypted to the age keys of
<input I14> (../deploy/secrets.md). Conventions are in
overview.md.
Rotate when a value may have been exposed (a node compromised, a file or identity leaked, a
person with access left), when a provider forces it, or on a schedule the operators set.
Rotating everything in a file is the answer to a leaked file or identity.
Everything at once
To replace every value of a file (a leaked file or identity, a person with access leaving, a handover), useinfra/secrets/rotate-secrets.py <env> and playbooks/rotate-all.yml: they apply
every step of the table below in the right order and prove each previous credential is refused
(../handover/rebuild.md section 7). The rest of this page is for
replacing one value.
The general pattern
-
Edit the value in the SOPS file on the controller:
Generate random values the way
infra/secrets/new-secrets.pydoes (24 random bytes as hex for passwords, 32 random bytes in standard base64 for the control plane keys): -
Apply with a converge at the release that runs now:
site.ymlrewrites the env files in/etc/veristar/env/, the NATS and Patroni configs, and runsplatformctl db rolesanddb umami-retention-role. -
Make the services use it. Compose recreates a service whose configuration changed; check
that each affected container was recreated after the change (
sudo docker compose ps, columnCREATED) and recreate it if not: - Commit the changed SOPS file.
Per secret
The certificates of the private network are not in
secrets.enc.yaml: they live in
ca.enc.yaml and tls.enc.yaml beside it and have their own section,
Internal CA and node certificates.
Keys of tofu.enc.env (CLOUDFLARE_API_TOKEN; Azure uses the operator’s az login, which
az logout ends) are only used on the controller: edit with
sops --config infra/secrets/.sops.yaml edit infra/secrets/staging/tofu.enc.env, run
make staging-plan to prove the new value works (expected: No changes.), and revoke the old
one at the provider. TF_VAR_state_passphrase needs OpenTofu’s fallback key provider for one
apply (../deploy/opentofu.md section 3.5).
Procedures for the extra steps
A NATS user password and its hash
nats_passwords.<user>, the second to nats_password_hashes.<user>
(the same cost and prefix new-secrets.py uses).
A PostgreSQL role password (Umami, superuser, replication)
On the database leader, inside psql,\password sends only a SCRAM verifier, so the password
never appears in logs or history:
postgres for the superuser, replicator for replication.) Then put the same value in the SOPS
file and converge. For the superuser and replication passwords also reload Patroni on both core
nodes after the converge, which is what the core role’s handler does:
patronictl list that the replica is still streaming.
The Umami admin password
On a core node, with the current and the new password typed in (not echoed):change password 200. Then write the new value to umami_admin_password, converge,
and recreate controlplane and jobs on both core nodes.
The Umami user password key
After the converge and the recreation ofcontrolplane and jobs, Umami still holds today’s
passwords derived from the old key, and umami-rotate skips a day it already recorded. Clear the
record and run the task once (overview.md, “Running a jobs task by hand”):
Internal CA and node certificates
Every link between nodes is TLS with certificates of one internal CA per environment (finding O-26 of https://github.com/justmert/veristar/blob/main/docs/audit/functional-audit-ops.md). The files:make internal-ca ENV=<env> (playbooks/internal-ca.yml, community.crypto 3.5.1 from the pinned
Ansible 14.6.0) issues only what is missing or due: a node certificate is issued again when the
node is new, its names changed (node name, private address), or it is valid for less than 30
more days. Keys are generated in the playbook’s memory and written only through SOPS. A second
run reports 0 certificate(s) issued. Node certificates last 365 days, the CA 10 years.
Each container that reads a certificate carries the label sa.tls (a hash of the node’s
certificate and the CA bundle), so site.yml recreates exactly those containers when either
changes. A converge after a reissue therefore restarts NATS, etcd, PostgreSQL (Patroni), HAProxy,
the relay, the exporters, Prometheus, Alertmanager and the services on each node; deploy with
deploy.yml (one node at a time, drained, the Patroni leader switched over first) rather than
site.yml in production.
Renew node certificates (yearly, or when the make internal-ca report shows certificates
due; also after adding a node or changing a private address):
N certificate(s) issued, ca unchanged, then every node healthy in turn. Peers keep
working through the rollout because the CA is unchanged.
Rotate the CA (the CA key may have been exposed, or before it expires). Two converges, so no
link ever meets a certificate its peer does not trust:
make internal-ca ENV=staging CA_ROTATE=new: a new CA signs every node certificate again; the old CA certificate moves toprevious_ca_certsand stays inca_bundle. Expected:N certificate(s) issued, ca new, 1 retired ca(s) still trusted.- Roll it out (
make staging-deploy ROLLING=1 ...). During the rollout, nodes with old certificates and nodes with new ones trust each other through the bundle. make internal-ca ENV=staging CA_ROTATE=retire: drops the retired CAs from the bundle (ca unchanged, 0 retired ca(s) still trusted), and roll out again.
make internal-ca; its certificate
leaves tls.enc.yaml. Its certificate is still valid until it expires, and nothing revokes it
(there is no CRL), so after a node was compromised rotate the CA as above.
The operators’ certificate (operator.cert, CN sa-<env>-operator) is renewed with the node
certificates. Extract it only into a private directory on your own machine, for example to read
Patroni or NATS monitoring through an SSH tunnel, and delete it afterwards:
Exercised
| 2026-11-13 | O-26:
make internal-ca ENV=local on the local converge’s inventory, first run then second run | first run: CA and 8 certificates issued; second run 0 certificate(s) issued, ca unchanged, changed=0 | this page |
Not yet exercised: any rotation through site.yml on deployed nodes (no staging), the NATS,
Patroni, Umami key and APP_SECRET rotations, and the provider keys. The phase 16 drills and the
local converge never rotated a value. These need staging; the multi-node topology may not run on
the shared host.