Secrets and encryption¶
Trimus holds three different "secret" concepts. Knowing which is which prevents the most common operator mistakes.
| Concept | What it is | Where it lives |
|---|---|---|
TRIMUS_ENCRYPTION_KEY (KEK) |
The master key that wraps the data keys. | Environment variable on the server — never in the database. |
| Company secrets | Encrypted credentials a company exposes to its agents (third‑party tokens, deploy keys, …). | company_secret_versions in Postgres, encrypted under the active DEK. |
| Provider API keys | Per‑company LLM provider credentials + custom headers. | company_providers in Postgres, encrypted under the active DEK. |
This page covers all three, plus the envelope‑encryption model that ties them together.
The KEK/DEK vault¶
Trimus uses envelope encryption (AES‑256‑GCM throughout):
- KEK — the Key Encryption Key. The 32‑byte key you set as
TRIMUS_ENCRYPTION_KEY. Its only job is to wrap the DEKs at rest; it never touches the database. - DEK — the Data Encryption Key. A 32‑byte key that actually
encrypts your data. DEKs live in the
data_encryption_keystable (migration 083), each row wrapped under the KEK. One active DEK encrypts new writes; older DEKs stayretiredso prior ciphertext still decrypts.
Ciphertext is stored as a versioned envelope:
The <dek_id> records which DEK encrypted the value, so a row written
under a retired DEK still decrypts after rotation. (Legacy values use
the enc:v1: prefix.)
On boot, crypto.InitDefault loads the vault from
TRIMUS_ENCRYPTION_KEY and bootstraps an initial active DEK on first
run. If any wrapped DEK was wrapped under a different KEK than the one
loaded, the vault refuses to start with a KEK fingerprint mismatch
— see key-rotation.md.
TRIMUS_ENCRYPTION_KEY (the KEK)¶
A 32‑byte AES‑256 key, base64‑encoded:
Set it on the trimus-server (and CLI) environment:
What the vault encrypts:
- Company secrets (
company_secret_versions.encrypted_value). - Provider API keys + custom headers (
company_providers.api_key,company_providers.custom_headers). - OAuth tokens and per‑trigger HMAC webhook secrets.
What it does NOT encrypt (delegate to Postgres / volume encryption):
- Issue bodies, comments, activity log, run transcripts — application rows in Postgres.
- File attachments under
TRIMUS_STORAGE_DIRand agent files underTRIMUS_HOME.
If you don't set it (SEC‑010)¶
Encrypted writes are REFUSED — not stored in plaintext
When TRIMUS_ENCRYPTION_KEY is unset the vault is not
initialised. The server still boots so read‑only paths and
non‑secret features keep working, but every encrypted‑write
endpoint returns 503 until you provide a key. You cannot create
or rotate a company secret, a provider API key, an OAuth token, or
a signed‑trigger secret. There is no "plaintext fallback".
The warning surfaces on three channels so it cannot be missed:
⚠ TRIMUS_ENCRYPTION_KEY not configured: secret writes (secrets, OAuth
tokens, signed triggers) will be REFUSED until a base64-encoded 32-byte
key is provided in this environment variable.
…on stderr (catches docker compose / local‑dev), as a structured
WARN with finding=SEC-010 (catches ops aggregators), and as a
banner on the secrets‑create form (catches the end user about to hit
the 503).
Key format checks¶
Validated at startup: must be valid base64 decoding to exactly 32 bytes. Anything else and the server exits with a clear error — there is no best‑effort decode.
If you lose the KEK¶
There is no recovery. Every DEK is wrapped under it; without it the database is opaque ciphertext:
- All company secrets become unrecoverable — re‑enter them.
- All provider API keys fail to decrypt on the next agent run — re‑paste them under Providers.
- Trigger HMAC secrets become unverifiable — re‑issue per trigger.
Treat the KEK like a TLS private key: back it up to your secrets manager (1Password, Vault, AWS Secrets Manager), share it among operators out‑of‑band, never commit it to git.
Migrating legacy plaintext (SEC‑010 CLI)¶
If you ran an older build that stored secrets or provider keys before
encryption was enforced, migrate them in bulk. Both commands are
idempotent and support --dry-run:
# Encrypt any legacy unencrypted company secret versions.
DATABASE_URL=… TRIMUS_ENCRYPTION_KEY=… trimus-cli encrypt-secrets --dry-run
DATABASE_URL=… TRIMUS_ENCRYPTION_KEY=… trimus-cli encrypt-secrets
# Encrypt any legacy unencrypted provider api_key / custom_headers.
DATABASE_URL=… TRIMUS_ENCRYPTION_KEY=… trimus-cli encrypt-providers
Company secrets¶
The Secrets sidebar entry (admin only). Each row is a name + value
scoped to a company, versioned in company_secret_versions:
DEPLOY_PRIVATE_KEY -----BEGIN OPENSSH PRIVATE KEY-----...
SLACK_WEBHOOK_URL https://hooks.slack.com/...
Agents in the company read them via the API:
GET /api/companies/{companyId}/secrets # list (names only, no values)
GET /api/secrets/{id} # one secret, decrypted on the fly
The GET /api/secrets/{id} handler loads the envelope, decrypts under
the recorded DEK, and returns plaintext to an authorised agent.
No per‑secret ACL
Any agent in the company can read all of that company's secrets. There is no per‑secret access control — scope companies accordingly.
Reveal is audited (SEC‑017). A human revealing a secret value in
the UI (/secrets/{id}/reveal) writes an audit row visible at
/secrets/audit and in the consolidated audit dashboard.
Adding a secret¶
UI: Secrets → + Add Secret (admin). API:
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name":"SLACK_WEBHOOK_URL","value":"https://...","description":"Used by routines"}' \
"$BASE/api/companies/$COMPANY/secrets"
Rotating a secret¶
Rotation writes a new version while keeping the same name and id, so agents that fetch by id don't break. In‑flight runs holding the old value recover on their next heartbeat.
The CLI can also list / reveal / rotate (reveal + rotate are audited as
actor __cli__):
DATABASE_URL=… TRIMUS_ENCRYPTION_KEY=… trimus-cli secret list --company=<id>
DATABASE_URL=… TRIMUS_ENCRYPTION_KEY=… trimus-cli secret reveal --company=<id> ...
DATABASE_URL=… TRIMUS_ENCRYPTION_KEY=… trimus-cli secret rotate --company=<id> ...
Deleting¶
DELETE /api/secrets/{id} — hard delete, no trash.
Provider API keys¶
Same vault, different table. When you create or edit a provider under
Providers → + Add Provider, the api_key and any custom_headers
are encrypted under the active DEK before insert and decrypted on every
agent heartbeat. There is no dedicated "rotate provider key" endpoint —
edit the provider and paste the new key; agents pick it up on their next
heartbeat. See providers.md.
Rotating the keys¶
KEK rotation does not re‑encrypt your data. It re‑wraps the handful
of DEK rows under the new master key; every ciphertext in
company_secret_versions / company_providers is untouched:
TRIMUS_ENCRYPTION_KEY=$OLD_KEK trimus-cli vault-status
TRIMUS_ENCRYPTION_KEY=$OLD_KEK trimus-cli rotate-kek --new-key=$NEW_KEK
# update TRIMUS_ENCRYPTION_KEY=$NEW_KEK in the deployment, restart
DEK rotation (incident response — you suspect a DEK leaked) mints a new active DEK and optionally migrates every row to it:
trimus-cli rotate-dek # new active DEK; prior becomes retired
trimus-cli rewrap-all # re-encrypt every encrypted column under the active DEK
rotate-kek and rotate-dek are single‑transaction and atomic — a
crash rolls back; re‑run after fixing the cause. The full runbook,
including the fingerprint sanity‑check and common gotchas, is in
key-rotation.md.
What's NOT encrypted by Trimus¶
| Thing | Where it lives | Encryption story |
|---|---|---|
| Issue titles / bodies / comments | Postgres tables | Postgres TLS in‑transit; at‑rest only if you configured it (or volume‑level). |
| Activity log | Postgres activity |
same |
| Heartbeat run transcripts | Postgres run‑event tables | same |
| Attachments / agent files | TRIMUS_STORAGE_DIR, TRIMUS_HOME |
filesystem‑level only — use encrypted volumes (LUKS / encrypted EBS). |
| Session cookies | client‑side | opaque random token; stored hashed (token_hash) in user_sessions and validated against the DB on every request — no client‑side signing, no TRIMUS_SESSION_SECRET. |
| API keys (raw form) | nowhere — only SHA‑256 hashes are stored | irreversible. |
If you have a regulatory requirement (e.g. PHI) for at‑rest encryption beyond what the vault covers, enforce it at the Postgres / filesystem layer.
Where to next¶
- key-rotation.md — KEK/DEK rotation runbook.
- security.md — broader production hardening.
- providers.md — where provider API keys come in.
- environment.md — full env‑var reference.