Skip to main content
How to replace each secret of an environment, what changes for users while you do, and what Ansible applies by itself. The secrets live in 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), use infra/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

  1. Edit the value in the SOPS file on the controller:
    Generate random values the way infra/secrets/new-secrets.py does (24 random bytes as hex for passwords, 32 random bytes in standard base64 for the control plane keys):
  2. Apply with a converge at the release that runs now:
    site.yml rewrites the env files in /etc/veristar/env/, the NATS and Patroni configs, and runs platformctl db roles and db umami-retention-role.
  3. 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, column CREATED) and recreate it if not:
  4. 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

The first line goes to 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:
and check with 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):
Expected: 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 of controlplane 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”):
Every Umami session token is invalidated by the password change; users re-enter through the console’s SSO.

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):
Expected: 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:
  1. make internal-ca ENV=staging CA_ROTATE=new: a new CA signs every node certificate again; the old CA certificate moves to previous_ca_certs and stays in ca_bundle. Expected: N certificate(s) issued, ca new, 1 retired ca(s) still trusted.
  2. 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.
  3. 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.
If the old CA key was exposed, do step 3 straight after step 2: until then a certificate the attacker signed with the old key is still trusted. Remove a node: delete it from the inventory and run 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.