Key rotation (KEK / DEK)¶
How to rotate Trimus's encryption keys safely. Both the master key (KEK) and the per‑installation data encryption key (DEK) rotate online, without re‑encrypting your secrets, provider API keys, or OAuth tokens.
This page is the operational runbook. See secrets-and-encryption.md for the underlying envelope‑encryption model.
KEK vs DEK¶
Trimus uses envelope encryption (AES‑256‑GCM):
- KEK — the Key Encryption Key, the 32‑byte AES‑256 key you set
as
TRIMUS_ENCRYPTION_KEY. It never touches the database; its only job is to wrap (encrypt) the DEKs at rest. - DEK — the Data Encryption Key, a 32‑byte AES‑256 key that
actually encrypts your data (secrets, provider
api_key/custom_headers, OAuth tokens). Each DEK lives indata_encryption_keys, wrapped under the KEK. - One active DEK encrypts new writes; older DEKs stay
retiredso previously‑written rows still decrypt. The ciphertext envelope (enc:v2:<dek_id>:<blob>) records which DEK to use.
The payoff: rotating the KEK doesn't require re‑encrypting your
data. The DEK rows (a handful) are re‑wrapped under the new KEK; every
existing ciphertext in company_secret_versions, company_providers,
etc. is left untouched.
Why rotate?¶
| Scenario | What to rotate | Command |
|---|---|---|
| Preventive hygiene (e.g. quarterly per policy) | KEK | trimus-cli rotate-kek |
| You suspect the KEK leaked | KEK | trimus-cli rotate-kek |
| You suspect a DEK leaked (hostile actor briefly had DB read access) | DEK + rewrap | trimus-cli rotate-dek then trimus-cli rewrap-all |
| Full lifecycle drill (test your runbook) | Both | all three commands |
Pre‑flight: check vault status¶
Before any rotation, confirm the running server is using the KEK you think it is. From the server's environment:
Vault status
============
Active DEK: c7f85ead-da0b-418b-81d7-bf12cabab930
DEK count: 1 (active + retired)
KEK fingerprint: 70d3c69dc37efcb8
DEK registry:
* c7f85ead-da0b-418b-81d7-bf12cabab930 status=active created=2026-05-24T03:02:03Z
The KEK fingerprint (first 16 hex chars of SHA‑256(KEK)) lets you
verify that the env var on this host matches what's recorded against
the DEK rows in the database. A mismatch means either the env var is
wrong or the database is from a different installation. The same
fingerprint is shown at /admin/settings in the web UI — no shell
needed to read it. For machine‑readable output: vault-status --json.
Rotating the KEK¶
Replaces the master key without touching encrypted application data.
Step 1 — Generate a new KEK¶
Generate it yourself:
…or have the CLI generate, rotate, and report in one transaction (the new key prints to stdout exactly once — save it before continuing):
--yes skips the confirmation prompt; omit it to be asked.
Step 2 — If you generated externally, run with --new-key¶
NEW_KEK="<base64-encoded-32-bytes>"
DATABASE_URL=… TRIMUS_ENCRYPTION_KEY=<OLD_KEK> \
trimus-cli rotate-kek --new-key="$NEW_KEK"
The CLI loads existing DEKs under the OLD KEK, re‑wraps each under the
NEW KEK, and updates data_encryption_keys in a single transaction.
Application ciphertexts are not touched.
Step 3 — Update the deployment¶
Set TRIMUS_ENCRYPTION_KEY to the new value in your env file and
restart the server. The startup log shows the new fingerprint matching
vault-status. All secrets / providers / OAuth tokens decrypt because
the DEKs that decrypt them are now wrapped under the new KEK.
Step 4 — Verify¶
Same DEK ids, new KEK fingerprint. Done. If application reads start
failing, the KEK env var on the server is not the new value, or the
rotation transaction rolled back — run vault-status from a shell with
the key you just set; the output tells you which.
Rotating the DEK¶
For when you want every NEW write to use a fresh data key. Existing
rows keep decrypting via the retired DEK; new rows use the new active
DEK. Optionally follow with rewrap-all to migrate every existing row.
Step 1 — Create the new active DEK¶
DEK rotation complete.
Previous active (now retired): c7f85ead-da0b-418b-81d7-bf12cabab930
New active: 9a18edcc-7f44-4b8b-8b81-1c2d3e4f5061
The running server picks up the new DEK on its next encrypt path.
Step 2 (optional) — Bulk‑migrate existing rows¶
DATABASE_URL=… TRIMUS_ENCRYPTION_KEY=… trimus-cli rewrap-all --dry-run # preview
DATABASE_URL=… TRIMUS_ENCRYPTION_KEY=… trimus-cli rewrap-all # do it
rewrap-all is idempotent — rows already under the active DEK are
no‑ops. Coverage: company_secret_versions.encrypted_value,
company_providers.api_key, company_providers.custom_headers. OAuth
tokens and trigger HMAC secrets lazy‑migrate on their next rotation
path and are intentionally not covered by rewrap-all (same precedent
as the encrypt-secrets / encrypt-providers migration commands).
Step 3 — Verify¶
New DEK status=active; previous DEK status=retired with a
retired=<timestamp>. Both stay in the registry; both are usable for
reads.
What if a rotation fails partway?¶
rotate-kek and rotate-dek run in a single transaction — a crash
rolls back the entire change and the database remains under the old KEK
/ old active DEK. Re‑run after fixing the cause. rewrap-all walks rows
one‑by‑one without a wrapping transaction (it's idempotent), so partial
completion is safe; re‑running picks up where it left off.
Why no web button?¶
Destructive crypto is CLI‑only by design. A one‑click "re‑wrap the
master key" button is too much power for a stray ⌘+R or a confused
session. /admin/settings surfaces the current state and fingerprint
and points at the CLI; the rotation itself requires shell access and an
explicit invocation.
Common gotchas¶
- Different KEK in env than DB — the vault refuses to start if any
DEK row has a different
kek_fingerprintthan the loaded KEK. Symptom: server fails to start with KEK fingerprint mismatch. Fix: restore the previousTRIMUS_ENCRYPTION_KEY, or runrotate-kekto bring the database in sync. - Forgot to update the env var after
rotate-kek— server fails to read any DEK (sameKEK fingerprint mismatch). Fix: set the newTRIMUS_ENCRYPTION_KEY. - KEK lost — no recovery. All DEKs are wrapped under it; without it the database is opaque ciphertext. Back the KEK up like a TLS private key — offline, encrypted, restricted access.
- Whitespace / stray newline in the env var — looks identical
visually but the fingerprint differs from the DB. Compare with
vault-status --json.
See also¶
- secrets-and-encryption.md — what the keys protect and how the vault loads.
- backup-and-restore.md — the active DEK
encrypts every
*.dump.encbackup, so the KEK is step 1 of every restore.