Skip to content

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:

  1. Accept HTTPS on 443 with a real certificate.
  2. Forward to trimus-server on its plain HTTP port (default 3100).
  3. Set the reverse‑proxy headers: X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host.

Then on the server:

TRIMUS_SECURE_COOKIES=true
TRIMUS_PUBLIC_URL=https://trimus.example.com

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:

TRIMUS_CSRF_TRUSTED_ORIGINS="https://trimus.example.com,https://board.example.com"

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:

DATABASE_URL= trimus-cli unlock-user user@example.com

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-prompt is 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, enable TRIMUS_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_URL set to the public hostname.
  • TRIMUS_ENCRYPTION_KEY set before any provider/secret is created.
  • TRIMUS_CSRF_TRUSTED_ORIGINS set to the real origin(s).
  • TRIMUS_TRUSTED_PROXIES set 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