How ciqadamq, pg_replica, and the Weido server-backend source their secrets
from hyperion-vault, and how a rotation reaches the live systems. This is the
runbook to go from the default state (secrets in env) to fully vault-managed.
| Secret | Format | Auto-rotates | Read by | How a rotation is applied |
|---|---|---|---|---|
ciqada/api-token |
opaque | yes | ciqadamq broker + server-backend client | consumers re-read and refresh |
ciqada/pepper |
opaque | no (static) | ciqadamq broker | read once at boot; not rotated |
pg/app |
userpass | yes | server-backend DB pools | vault calls replica.rotate_credential on the primary |
pg/replicator |
userpass | no — keep manual | bootstrapped from REPL_PASS env at initdb |
manual only (see Limitations) |
Secret reads (GET /v1/secrets/{name}) are gated by VAULT_ALLOWED_IPS
only — no token. Create/rotate need an admin bearer token.
Read path. Each consumer reads its secret(s) over the mesh by IP. There is no bootstrap token needed to read a token — reads are network-gated.
Rotation path (Postgres). The vault is the source of truth and the generator.
A secret may carry a target (see API.md).
For a userpass secret with target: pg_replica, on rotation the leader:
- generates the new password,
- connects (as the
login_secretrole) to eachtarget.hostsnode and runsSELECT replica.rotate_credential(role, new_password)— the function no-ops on standbys (returns false) and runsALTER ROLE … PASSWORD …on the primary, returning true, - commits the new version only after the primary accepted it.
So the vault never holds Postgres open or guesses the primary — pg_replica's
rotate_credential does the apply where it's writable.
Rotation path (ciqada). Nothing is pushed. The token rotates; ciqadamq
re-reads it from the vault on an auth-miss (a presented token it doesn't
recognize) and accepts the current and previous token during the grace
window; clients re-read on a 401. The pepper is static — read once at boot,
not rotated.
Consumer behavior.
- ciqadamq — at boot (when
VAULT_ADDRis set) readsciqada/api-tokenandciqada/pepper. The token refreshes on an auth-miss (re-reads from the vault and re-checks; accepts current+previous); the pepper is static. Falls back to env (API_TOKEN/AUTH_PEPPER) if the vault is unset/unreachable. - server-backend —
utils/vault/creds.mtsreadspg/appandciqada/api-token(cached, env fallback). The drizzle pool uses an async password callback; the parade pools (hyperiondb-client) rebuild themselves on an auth-failure (28P01) — fetch the fresh password, recreate the pool, retry. No polling. - pg_replica — exposes
replica.rotate_credential(role, password). It does not call the vault; the vault calls in.
Starting point: secrets live in env, the vault cluster is running, and the new binaries are built but the integration is not yet active.
- Vault reachable from every consumer;
VAULT_BOOTSTRAP_TOKENavailable for the provisioning call. VAULT_ALLOWED_IPSincludes the ciqadamq nodes andserver-backend(so reads are permitted), and the vault can reach the Postgres nodes on:5432(so it can runALTER ROLE).- The pg_replica extension build that includes
replica.rotate_credentialis deployed (it is ashared_preload_librariesextension, so this needs a Postgres restart).
From server-backend, with the current env values present (so the vault matches
what is already live), run:
yarn vault:provision # tsx --env-file=.env ./scripts/vaultProvision.mtsIt seeds (idempotent — existing secrets are skipped):
ciqada/api-token,ciqada/pepperfromCIQADA_API_TOKEN/CIQADA_PEPPERpg/appfromPG_USER/PG_PASS, withtarget: pg_replicapg/replicatorfromREPL_PASS
PG_DB must be set when provisioning: it becomes target.database, the database
the vault connects to — it must be the DB where CREATE EXTENSION pg_replica
ran, or replica.rotate_credential will not be found.
Provision
pg/replicatorasmanual, notautomatic, until passfile sync lands — see Limitations. (The script seeds itautomatic; change itskindtomanualor set no rotation interval.)
- ciqadamq: set
VAULT_ADDR=http://<vault-ip>:8205on each broker container (already in thedocker-compose*.yml). With it set, the broker reads from the vault; theAPI_TOKEN/AUTH_PEPPERenv are no longer required. - server-backend: keep
SERVER_PRIVATE_IPandVAULT_BOOTSTRAP_TOKEN. The code readspg/appandciqada/api-tokenfrom the vault automatically. KeepPG_USER/PG_DB(identity, not secret) andPG_PASS(still feedsDATABASE_URLand is the fallback). - pg_replica: nothing to configure — the function is always available once the extension build is deployed.
Deploy the new binaries (vault with the target apply, ciqadamq with the vault
client, pg_replica with rotate_credential, server-backend with the vault-backed
clients).
- ciqadamq logs:
api token loaded from vault secret 'ciqada/api-token'. - server-backend can read:
curlan endpoint that hits ciqada / Postgres. - In staging, force a rotation of
pg/appand confirm: the vault audit showsrotate ok, a new connection authenticates, andSELECT replica.status()still reports a healthy primary.
Only once vault reads are confirmed in prod, remove the now-unused env from the
app/ciqada containers: CIQADA_API_TOKEN (app services), API_TOKEN +
AUTH_PEPPER (ciqada containers). Keep everything on the paradedb
container (the entrypoint requires it and it bootstraps the DB) and keep
PG_PASS (DATABASE_URL). Removing a fallback makes the vault a hard dependency
at boot — provision first.
| Where | Setting | Meaning |
|---|---|---|
| ciqadamq | VAULT_ADDR |
vault base URL; empty = stay on env |
| ciqadamq | API_TOKEN, AUTH_PEPPER |
fallback only when the vault is unset/unreachable |
| vault | VAULT_ALLOWED_IPS |
must include consumer IPs (reads are IP-gated) |
| server-backend | SERVER_PRIVATE_IP, VAULT_BOOTSTRAP_TOKEN |
vault address + token for create/rotate |
| server-backend | PG_USER, PG_DB, PG_PASS |
DB identity; PG_PASS also feeds DATABASE_URL and is the fallback |
secret target |
hosts, database, role, login_secret |
how the vault reaches the pg cluster to apply a rotation |
Default rotation cadence (provisioning): interval 30 days, grace 1 day.
pg/replicatormust not auto-rotate.rotate_credentialrunsALTER ROLEon the primary andpg_authidreplicates over WAL, but each node's local libpq passfile (used by walreceiver /pg_basebackup/pg_rewind) is a plain file that is not replicated. Rotating the replicator password would therefore break streaming replication on the next standby reconnect. Keep itmanualand coordinate passfile updates by hand, until per-node passfile sync is added.- Rare consistency window. If
rotate_credentialsucceeds but the vault's raft commit then fails, the vault and Postgres briefly disagree; the next rotation tick reconciles. - Manual
PUTdoes not apply to Postgres. Only rotation (scheduled orPOST /v1/secrets/{name}/rotate) runs the target. To change a managed password by hand, rotate — don'tPUT. - drizzle path uses the vault only when
PG_HOST/PG_PORTare set; otherwise it falls back toDATABASE_URL. The parade pools always use the vault.