Skip to content

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 in data_encryption_keys, wrapped under the KEK.
  • One active DEK encrypts new writes; older DEKs stay retired so 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:

DATABASE_URL= TRIMUS_ENCRYPTION_KEY= trimus-cli vault-status
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:

openssl rand -base64 32

…or have the CLI generate, rotate, and report in one transaction (the new key prints to stdout exactly once — save it before continuing):

DATABASE_URL= TRIMUS_ENCRYPTION_KEY= trimus-cli rotate-kek --generate --yes

--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

trimus-cli vault-status

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

DATABASE_URL= TRIMUS_ENCRYPTION_KEY= trimus-cli rotate-dek --yes
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

trimus-cli vault-status

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_fingerprint than the loaded KEK. Symptom: server fails to start with KEK fingerprint mismatch. Fix: restore the previous TRIMUS_ENCRYPTION_KEY, or run rotate-kek to bring the database in sync.
  • Forgot to update the env var after rotate-kek — server fails to read any DEK (same KEK fingerprint mismatch). Fix: set the new TRIMUS_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