Secrets Rotation
The CLI rotates per-instance secrets in place, updates the tenant .env, restarts
the application once, verifies the new values, and optionally saves them to a
password manager. New values are never printed — only masked hints (ab****cd).
Commands
flo secrets status <id> # list rotatable secrets + state
flo secrets rotate <id> dbPassword oidcCerts # rotate specific keys
flo secrets rotate <id> --all # rotate everything
flo secrets rotate <id> --all --skip emailPassword --save-to 1password
flo secrets rotate <id> --all --dry-run # preview, change nothing
| Option | Description |
|---|---|
--all | Rotate every secret for the instance |
--dry-run | Preview what would be rotated without changes |
--skip <keys> | With --all: comma-separated keys to skip (e.g. emailPassword,auditHashSalt) |
--save-to <manager> | Password manager: 1password or keepassxc (auto-detected if omitted) |
--vault <name> | Target vault/database name |
--confirm | Skip interactive confirmations — except emailPassword, which always prompts |
Valid keys: dbPassword, oidcCerts, auditHashSalt, emailPassword,
strapiApiKey.
What rotates
Rotators run in a fixed order: DB → OIDC → audit salt → email → Strapi.
| Key | Label | Auto | What it does | Verification |
|---|---|---|---|---|
dbPassword | Database password | yes | ALTER USER … PASSWORD inside the Postgres container; updates DB_CONNECTION_STRING in .env | pg_isready + test query after restart |
oidcCerts | OIDC certificates | yes | Generates new signing + encryption passwords together (they share a lifecycle) and regenerates the OIDC certs; updates OIDC_*_PASSWORD | /.well-known/openid-configuration returns 200 |
auditHashSalt | Audit log hash salt | yes | Generates a new salt and appends it to the chain; writes AUDIT_HASH_SALTS=old,new + AuditLog__HashSalt=new | no-op (chain semantics) |
emailPassword | Email provider password | no | Requires secure manual input; updates EMAIL_SENDER_EMAIL_PSW | manual |
strapiApiKey | Strapi API keys | yes | For every linked Strapi instance: creates new read-only + blog-editor tokens, updates STRAPI_API_KEY / BLOG_EDITOR_STRAPI_API_KEY and the tenant registry, deletes the old tokens after verification | GET /api with the new token returns 200 |
Notes:
auditHashSaltrotation does not invalidate old audit hashes: the previous salts stay in the chain and the backend tries each one, so historical correlation keeps working.- Rotating
oidcCertsinvalidates previously issued OIDC tokens. emailPasswordhas no automation path — the rotator prompts even with--confirm, and skips itself when the tenant has no email provider.
Rotation flow
- The tenant registry is snapshotted for rollback.
- Each rotator generates the new value and applies it to the infrastructure (PostgreSQL, OIDC certs, Strapi API).
- The
.envfile is regenerated and merged so custom variables are preserved. - The application container is restarted once with
docker compose up -d --force-recreate. - Every rotator verifies its secret (default 36 retries × 5 s ≈ 3 minutes).
- On failure the orchestrator rolls back: registry snapshot, previous
.env, and restart — plus a best-effort infrastructure rollback (for example, changing the DB password back).
Because of the restart, expect a short (~5–10 s) application downtime per rotation.
Password manager sync
- 1Password: uses
op; items are created as logins with one concealed field per secret and rotation metadata in the notes. - KeePassXC: uses
keepassxc-cli; the database path comes fromsecrets.keepassDbPathin~/.flo/config.json(or is prompted), fields are stored as custom attributes. - Auto-detection checks 1Password first, then KeePassXC. If no manager is
available (or configured as
none), the command warns that the secrets exist only in the encrypted registry.
Config keys: secrets.passwordManager (1password | keepassxc | none),
secrets.vault, secrets.keepassDbPath.
Encryption at rest
Tenant secrets are encrypted with AES-256-GCM before they are written to the
registry or .env:
- Format:
enc:<salt>:<iv>:<authTag>:<ciphertext>(base64 parts), 32-byte salt and IV, 16-byte auth tag. - Key derivation: scrypt from the operator's
~/.flo/master.key(created at first run with mode0600; permissions are verified and repaired). - Field detection is name-based (
password,secret,key,token,apikey, plusdb_connection_stringandemail_sender_email_psw), shared with theconfig env showmasker so the two never drift. - Backup archives use the same algorithm with a separate
FLOBK1file format (see Backups).
Safety and caveats
- Always start with
--dry-runon a production tenant. - Rotation requires the instance to be resolvable and reachable; a failed
verification rolls back, but infrastructure rollback is best-effort — check
flo secrets statusandflo instance healthafter a failure. - Do not rotate while a DR failover incident is active; re-mirror secrets to the
standby afterwards with
flo failover sync-secrets <id>(see Failover and Disaster Recovery). - Secret values are never logged; adapters receive plaintext only for the save operation and CLI state is cleared afterwards.
- VPS login passwords and SSH keys are a separate surface:
flo vps rotate-password [vpsName](--hardento disable password auth,--key-pathto reuse a key,--save-to/--vaultto store the result).