Security¶
Putting Trimus on the public internet means turning on the things that are off‑by‑default for development and adding the boundary controls Trimus deliberately leaves to your edge. This page is the production checklist.
What Trimus enforces vs what you provide¶
| Control | Provided by Trimus | You provide |
|---|---|---|
| Tenant isolation (RLS) | ✅ staple_app role + WithTenantScope |
— |
| Encryption at rest for secrets/keys | ✅ KEK/DEK vault (AES‑256‑GCM) | the KEK, backed up |
| CSRF protection on form POSTs | ✅ CSRFProtect + origin allowlist |
trusted‑origins list |
| Rate limiting | ✅ per‑limiter RPM | sane RPM, trusted‑proxy CIDRs |
| Request body caps | ✅ 1 MB JSON / configurable uploads | — |
| Login lockout | ✅ per‑(email, IP) sliding window | thresholds |
| Log redaction | ✅ secrets/keys redacted in logs | log shipping |
| TLS / HTTPS | ❌ not terminated by Trimus | reverse proxy + cert |
| Security response headers (CSP, HSTS, X‑Frame‑Options, …) | ❌ not emitted by Trimus | reverse proxy |
Trimus does not emit CSP / HSTS / X‑Frame‑Options
The server does not set a Content‑Security‑Policy, HSTS, X‑Frame‑Options, Referrer‑Policy, or Permissions‑Policy header. If you need those (you do, for a public deployment), add them at your reverse proxy. Trimus's in‑app protections are CSRF, rate limiting, body caps, login lockout, and the RLS/vault data‑layer controls.
TLS / HTTPS¶
Trimus does not terminate TLS. Run a reverse proxy (nginx, Caddy, Traefik, ALB, Cloudflare) in front and have it:
- Accept HTTPS on 443 with a real certificate.
- Forward to
trimus-serveron its plain HTTP port (default3100). - Set the reverse‑proxy headers:
X-Forwarded-For,X-Forwarded-Proto,X-Forwarded-Host.
Then on the server:
TRIMUS_SECURE_COOKIES=true adds the Secure flag to session cookies —
browsers refuse to send them over plain HTTP. Forgetting this is the #1
production misconfiguration. TRIMUS_PUBLIC_URL is what Trimus uses for
OAuth redirects, webhook payload URLs, and agent‑facing callbacks; a
wrong value breaks OAuth callbacks.
A recommended response‑header block to add at the proxy:
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Sessions¶
User sessions are database-backed: login mints an opaque random token,
stored (hashed) in user_sessions and validated against the DB on every
request. There is no signing secret — sessions therefore survive
server restarts, and TRIMUS_SESSION_SECRET is removed (#155): it was
loaded but never used, and its old "sessions die on restart" warning was
incorrect. Expired sessions are reaped hourly.
The session cookie is HttpOnly + SameSite=Lax. With
TRIMUS_SECURE_COOKIES=true (or behind an X-Forwarded-Proto: https
proxy) it is also Secure and uses the hardened __Host- prefix
(#154), which browsers only accept on a Secure, Path=/, Domain-less
cookie — blocking cross-subdomain / non-TLS cookie injection. Reads
tolerate both the __Host- and legacy names, so enabling secure cookies
does not force a re-login.
Live membership re‑verification (SEC‑030)
The SSE hub periodically re‑checks company membership and drops live‑event streams for users whose membership was revoked, so a revoked user's open board tab stops receiving updates without waiting for their session to expire. API‑key / instance‑level actors stay valid for their connection lifetime.
CSRF¶
Form POSTs (the HTML UI) are CSRF‑protected. Cross‑origin browser POSTs are rejected unless the origin is on the allowlist:
The login form additionally carries a server-issued synchronizer token
(an HttpOnly __Host- cookie whose value is echoed in a hidden field and
verified on submit), so login is protected even if the Origin check is
bypassed (Referer stripping, a same-origin content-injection edge) —
defense in depth on top of the origin allowlist (#156).
Bare API callers (no browser, no Origin, no cookie) are unaffected —
they authenticate with Authorization: Bearer.
Rate limiting and trusted proxies (SEC‑014)¶
RATE_LIMIT_RPM=300 # per-limiter requests/minute (default)
TRIMUS_TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12
The limiter keys on the client IP. X-Forwarded-For is honoured only
when the direct caller IP falls inside a TRIMUS_TRUSTED_PROXIES
range — otherwise the header is ignored and the socket peer is used.
This is the secure default: without it, a request could spoof
X-Forwarded-For to dodge the limiter. An invalid CIDR is a boot
error. If you front Trimus with a proxy, you must set this or every
request appears to come from the proxy IP (and shares one bucket).
Login lockout (SEC‑013)¶
Per‑(email, IP) sliding‑window lockout backs off credential‑stuffing:
TRIMUS_LOGIN_LOCKOUT_THRESHOLD=... # consecutive failures
TRIMUS_LOGIN_LOCKOUT_WINDOW=15m # default failure window
TRIMUS_LOGIN_LOCKOUT_DURATION=30m # default lockout once tripped
Recover a locked account from a shell:
The client IP is keyed via the same trusted‑proxy logic as the rate limiter.
OAuth (Google, GitHub)¶
Optional sign‑in providers. Enable Google:
TRIMUS_OAUTH_GOOGLE_CLIENT_ID="..."
TRIMUS_OAUTH_GOOGLE_CLIENT_SECRET="..."
TRIMUS_OAUTH_REDIRECT_BASE_URL="https://trimus.example.com" # omit → TRIMUS_PUBLIC_URL
Provider‑side: create a Web‑application OAuth client; set the authorized
redirect URI to https://trimus.example.com/auth/google/callback; paste
the id + secret. GitHub is the same shape with TRIMUS_OAUTH_GITHUB_*
and callback /auth/github/callback. Setting both the id and secret
for a provider enables its login button after restart. First OAuth
sign‑in creates a user; subsequent sign‑ins look up by provider +
provider‑side user id.
Authentication boundaries¶
Three classes of caller:
| Caller | Authenticates with | Examples |
|---|---|---|
| Browser user | session cookie from /login or OAuth |
humans on the board |
| Agent | Authorization: Bearer sk_… (hire‑agent / admin UI) |
LLM agents, scripts |
| Worker | Authorization: Bearer sk_… (from /api/workers/register) |
remote worker processes |
Middleware extracts the actor (cookie or header), sets a context value
handlers read via auth.GetActor, and enforces role‑based access.
Cross‑company access returns 404, not 403, by design — don't leak
which IDs are real.
Companies are private by default (#142). A company is invisible to
non‑members until an instance admin marks it discoverable (company
settings → Directory Visibility): until then it's absent from
GET /companies, the sidebar, and the join directory, and join requests to it
return 404. Instance admins always see every company.
Instance admin is env‑authoritative (#142). Membership comes from the
TRIMUS_INSTANCE_ADMINS allowlist (see environment), not
the users.instance_role column — which no longer grants instance‑admin.
Operator alert DMs (cost‑budget, backup‑health, GEPA, escalations) use the same
allowlist.
Permissions matrix (rough)¶
| Action | Required role |
|---|---|
| Read company data | Viewer (or any role) |
| Create / update issues, agents, notes, … | Member |
| Provider config, secrets, member admin, egress, plugins | Admin (company‑scoped) |
| Create companies, instance settings/flags/skills/trash, audit dashboard | Instance admin |
| Resolve approvals | Human only — agents are blocked from self‑resolving |
Terminate own agent (DELETE /api/agents/me) |
Blocked (no self‑terminate) |
Ground truth is the route‑group middleware in cmd/server/routes.go.
API key hygiene¶
- API keys are 64 hex chars prefixed
sk_, stored as SHA‑256 hashes — the raw key is shown once at creation. - Lost key ⇒ issue a new one and revoke the old.
- Per‑agent keys: list at
/api/companies/{id}/agent-api-keys, revoke from the agent page. Worker keys: listed at/api/workers. - A leaked key grants the bearer all rights of the actor that owns it — revoke immediately if exposed.
Encryption at rest¶
See secrets-and-encryption.md. Set
TRIMUS_ENCRYPTION_KEY before any secret or provider key is created.
Without the key, encrypted writes are REFUSED (503)
There is no plaintext fallback (SEC‑010). An unset key leaves the vault uninitialised, the server still boots, but every encrypted‑write endpoint returns 503 until you provide a base64 32‑byte key. Back the key up; losing it makes all encrypted data unrecoverable.
Adapter & egress isolation¶
For multi‑tenant deployments, run agents in the sandboxed adapters
(claude_sandbox / codex_sandbox / hardened docker) on the
trimus-egress-allowlist network and enable the deny‑by‑default egress
DNS resolver (TRIMUS_EGRESS_DNS_ADDR). The unsandboxed
claude_local / codex_local adapters inherit the host environment —
including DATABASE_URL and TRIMUS_ENCRYPTION_KEY — so a process‑level
escape there has host‑level reach. See
adapter-isolation.md and
adapters.md.
Public ingress endpoints¶
A small set of endpoints are intentionally unauthenticated (each is rate‑limited):
POST /api/routine-triggers/public/{publicId}/fire— HMAC‑SHA256 signed webhook with replay protection (per‑trigger secret).POST /api/companies/{companyId}/plugins/webhooks/{path}— per‑plugin auth.POST /api/workers/register— remote‑worker bootstrap (a fresh worker has no key yet; register is what issues it).GET /api/workers/operating-promptis behind worker auth as of the #148 hardening — the prompt ships in the register response, so it's no longer anonymously disclosable. For untrusted networks, enableTRIMUS_REQUIRE_WORKER_APPROVAL=true(#210) so a registered worker holds an inert key and receives no prompt or tasks until an instance‑admin approves it — this is the recommended posture when the register endpoint isn't locked down at the proxy. See remote-workers.md.GET /companies/{id}/.well-known/agent-card.json(+ per‑agent) — A2A discovery.
If you can lock any of these down at the proxy (e.g. only allow a provider's IP ranges to a specific routine trigger), do.
Audit trail¶
Changes to issues, agents, comments, approvals, files, secrets, plugin
approvals, pairings, instance flags, and MCP tool calls write to
per‑resource audit tables. Instance admins get a consolidated,
cross‑company feed at /admin/audit with CSV export, per‑actor
timelines, and insights — see audit-dashboard.md.
CLI‑written audit rows are stamped actor=__cli__ (or rendered
"system" where the actor is NULL).
Production checklist¶
Before exposing Trimus beyond your VPN:
- HTTPS termination in front with a valid certificate.
-
TRIMUS_SECURE_COOKIES=true. -
TRIMUS_PUBLIC_URLset to the public hostname. -
TRIMUS_ENCRYPTION_KEYset before any provider/secret is created. -
TRIMUS_CSRF_TRUSTED_ORIGINSset to the real origin(s). -
TRIMUS_TRUSTED_PROXIESset to your proxy's CIDR(s). - Security response headers (CSP/HSTS/X‑Frame‑Options) added at the proxy.
- Postgres uses
sslmode=require(or stronger). - Rate limit reasonable for expected traffic.
- Login‑lockout thresholds reviewed.
- Multi‑tenant agents on sandboxed adapters + egress allowlist.
- OAuth providers configured if enabling them.
- Backups running for Postgres +
TRIMUS_HOME+TRIMUS_STORAGE_DIR(see backup-and-restore.md). - Monitoring set up — alert on
/readyz(see monitoring.md).
Where to next¶
- environment.md — env‑var reference.
- secrets-and-encryption.md — encryption‑at‑rest.
- adapter-isolation.md — runtime isolation per adapter.
- monitoring.md — health, metrics, and what to alert on.