Dynamic Postgres Credentials
Replace your service's long-lived DB_PASSWORD with short-lived credentials minted on demand. Each service instance gets its own DB user, valid for hours. If a key leaks, the blast radius is one user and one hour, not the whole fleet forever.
By the end:
- A ScaiVault dynamic engine configured for your Postgres.
- A
readonlyrole that mints users with the right permissions. - A reporting service that obtains and revokes credentials per workload.
- Root credentials rotated automatically every 7 days.
What you need#
- A Postgres database with admin access (so you can grant ScaiVault a role that can
CREATE ROLE). - ScaiVault token with
dynamic:manage,secrets:write,admin. - Connection details for the DB (hostname, port, database name).
1. Create a privileged ScaiVault DB user#
Inside Postgres, give ScaiVault a role with just enough power to mint and revoke users:
1 2 3 4 5 | |
You'll rotate this temporary password to a strong one inside ScaiVault in a moment.
2. Store root credentials in ScaiVault#
1 2 3 4 5 6 7 8 9 10 | |
Bind a policy so only the dynamic engine machinery can read this path. Nothing else should ever see the DB root.
1 2 3 4 5 6 7 8 9 10 11 | |
(Binding to the right system identity for the dynamic-engine subsystem depends on your deployment — typically a built-in system:dynamic-engines identity. Check with scaivault auth whoami while running as the engine to confirm.)
3. Configure the dynamic engine#
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
ScaiVault reads the root creds, substitutes them into the URL, and opens a test connection. If you get 400 invalid_config, the credentials or the URL are wrong — fix and PATCH the engine.
Confirm health:
1 2 3 4 | |
4. Define a role#
The role's creation_statements run when a lease is generated; revocation_statements run when it expires or is explicitly revoked.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | |
Two non-obvious details in the revocation:
REASSIGN OWNED BYbeforeDROP. If the temporary user happened to create any objects (sessions, temp tables) it owns them;DROP ROLEthen fails.REASSIGNmoves ownership toscaivault_adminfirst.IF EXISTSonDROP ROLE. Belt-and-braces — if revocation runs twice for any reason, it shouldn't error.
5. Generate credentials from your service#
The pattern: borrow a lease, do work, revoke. Don't keep leases around longer than the work needs.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | |
Every run gets a fresh user. Postgres logs will show v_readonly_<random> connections — you can correlate to the lease in ScaiVault audit by username.
6. Pre-warm pools (optional)#
For services with steady traffic, generating a credential per request is expensive (~50ms for the CREATE ROLE). Two solutions:
Connection pool with renewal. Hold one lease for the lifetime of the pool. Renew it before expiry. Use the same credential for many connections.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | |
One credential per pod, renewed. For Kubernetes services, fetch on startup, renew via sidecar, revoke on shutdown.
7. Auto-rotate the root credential#
The DB root sitting at infra/postgres/reporting/root is itself a secret. Put it on a rotation policy so it doesn't sit static forever.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
With auto_generate: true and a value-generation policy attached, ScaiVault picks a new strong password on rotation. But it can't update Postgres for you — wire a webhook on secret.rotated for infra/postgres/reporting/root that runs:
1 | |
Two things to coordinate:
- The dynamic engine refreshes its in-memory credential on the next lease generation after the rotation. There's a small window where it might use the old password — minimize this by short-lived caching in the engine (configurable via engine
config.credentials_cache_ttl: 30s). - The webhook ALTER ROLE must run before the grace period closes. 24h is generous; align it with your operations window.
8. Observability#
scaivault dynamic leases list --engine reporting-db --json shows current active leases. If the count grows unbounded, some caller isn't revoking — find the role and prefix:
1 2 | |
Audit log:
1 2 | |
What you have now#
- One DB root credential, stored in ScaiVault, rotated weekly.
- Every workload gets a fresh Postgres user, valid for one hour by default.
- Leases revoke automatically; expired users disappear from the DB.
- Audit log shows exactly who borrowed which credentials for how long.
- A leaked credential is valid for at most one hour and used by exactly one workload.
What's next#
- Dynamic Secrets guide — every engine type.
- Rotation tutorial — pattern for non-auto-generated credentials.
- Cookbook — quick recipes for adjacent tasks.