* docs(wiki): correct the install and operations pages for 4.0.0 Quick Start passed docker an empty ENCRYPTION_KEY: an assignment prefix is not expanded on the same command line, so the server silently generated its own key instead of taking the one the page told you to set. The tag examples pinned to the 3.x line, the documented tmpfs size did not match the shipped compose file, and several .env values were described as reaching the container when compose never interpolates them. * docs(wiki): match the MCP reference to the registered surface The scope table was missing the whole Collections group and the totals said 27 scopes across 13 groups where the code registers 29 across 14. The Cloudflare link on the setup page pointed at an anchor on its own page instead of the heading on Troubleshooting. * docs(wiki): correct the plugin and admin pages Three links to Plugin-Development carried a .md suffix, which does not resolve on the wiki. The admin pages described tabs, permissions and storage behaviour that had moved on. * docs(wiki): bring the feature pages back in line with the app Costs still documented the old panel, where categories could be added, renamed, reordered and deleted; they are fixed now and the panel is a day-grouped ledger with settlements. Trip sharing, journeys, maps, packing, reservations and the account pages had all drifted in the same way. * docs(wiki): cover the pages the first pass did not reach The MCP catalog listed 168 of the 201 registered tools; the settlement tools and rate_place were missing here and the Collections family from the addon page. Several resource rows claimed a trips:* scope where the handler asks for trips:read. * docs(wiki): tighten the corrections a review picked apart The audit-log resource is the row id, not the credential id; an admin password reset does not bump password_version the way the page implied; and a few of the new paragraphs described behaviour more confidently than the code supports. * docs(wiki): fix what a last read-through caught Costs shows every figure in your display currency, not the trip's, and an expense with no payer still counts against the members it is split between. Studio's cutoff is a 1024px viewport rather than phones alone, and the Integrations tab is gated on the providers an admin has configured. * docs(wiki): the last few the review caught A passkey satisfies the MFA policy everywhere except the WebSocket handshake, which still asks for TOTP, so a passkey-only account under that policy gets no live updates. Mobile optimizes a day from two places, not three. The ctx.db row carried an unescaped pipe that split the table, and the Backups table linked with a wiki link whose pipe did the same.
9.9 KiB
Security Hardening
A production TREK deployment checklist. All items reference actual TREK configuration options.
Encryption & Secrets
- Set a strong
ENCRYPTION_KEY(generate withopenssl rand -hex 32). See Encryption-Key-Rotation. - Back up
ENCRYPTION_KEYseparately from the database backup ZIP — losing it makes all stored API keys and secrets unreadable. Stored secrets use AES-256-GCM encryption derived from this key. - Rotate
ENCRYPTION_KEYif it may have been exposed. See Encryption-Key-Rotation.
HTTPS & Network
- Run TREK behind a TLS-terminating reverse proxy (nginx, Caddy, Traefik). See Reverse-Proxy.
- Set
TRUST_PROXY=1so client IPs are captured correctly in the audit log. InNODE_ENV=productionthis defaults to1automatically, but set it explicitly if you use a non-standard proxy hop count. - Set
FORCE_HTTPS=trueto 301-redirect HTTP to HTTPS and addupgrade-insecure-requeststo the CSP. Your proxy must sendX-Forwarded-Proto: https(or terminate TLS on the same connection), otherwise the redirect fires on every request and loops. - Know that HSTS (
max-age=31536000) does not depend onFORCE_HTTPS: it is sent wheneverFORCE_HTTPS=trueorNODE_ENV=production, which the Docker image sets by default — so an instance behind Traefik, Caddy or a Cloudflare Tunnel advertises HSTS without settingFORCE_HTTPSat all. SetHSTS_INCLUDE_SUBDOMAINS=trueto addincludeSubDomains; it is off by default so an install on an apex domain does not force HTTPS onto sibling subdomains you may still serve over plain HTTP. - Keep
ALLOW_INTERNAL_NETWORK=falseunless Immich or Synology is on your LAN. See Internal-Network-Access. Note: loopback (127.x,::1) and link-local (169.254.x) addresses are always blocked regardless of this setting.
Authentication
- Enable two-factor authentication for your admin account. See Two-Factor-Authentication.
- Require MFA for all users if your use case demands it: Admin Panel → Settings → Require two-factor authentication (2FA). Note: you must secure your own admin account first, with either TOTP or a registered passkey — the server refuses the toggle otherwise. A passkey satisfies the policy for everyone else too, so nobody is forced onto TOTP specifically.
- Disable open registration if you control who can access the instance. See Admin-Users-and-Invites.
- Rotate the JWT signing secret if a session may have been leaked: Admin Panel → Settings → Danger Zone → Rotate (
POST /api/admin/rotate-jwt-secret). This invalidates all active sessions immediately, including your own.
Session Security
TREK stores sessions as JWTs in an httpOnly trek_session cookie (SameSite=Lax). A normal login expires after SESSION_DURATION (default 24 hours) and rides in a browser-session cookie that the browser drops when it closes; ticking Remember me issues a persistent cookie whose lifetime and JWT expiry are SESSION_DURATION_REMEMBER (default 30 days) — shorten it if a 30-day window is too wide for your threat model. The secure flag is set automatically when NODE_ENV=production, when FORCE_HTTPS=true, or when Express sees that the request arrived over TLS (X-Forwarded-Proto: https) — the last of those needs trust proxy active, which is automatic in production and otherwise means setting TRUST_PROXY yourself. Tokens are also accepted via Authorization: Bearer header for MCP and API clients.
- Ensure
FORCE_HTTPS=true(orNODE_ENV=production) so thetrek_sessioncookie carries thesecureflag and is never sent over plain HTTP. - Set
COOKIE_SECURE=falseonly as a temporary escape hatch for LAN testing without TLS — do not use in production.
Password Policy
TREK enforces a minimum password policy on all registrations and password changes:
- Minimum 8 characters
- Must contain uppercase, lowercase, digit, and special character
- Common passwords and fully-repetitive strings are rejected
- Passwords are hashed with bcrypt (cost factor 12)
No configuration is required; this policy is always active.
Rate Limiting
Built-in in-memory rate limits protect authentication endpoints:
| Endpoint | Limit | Window |
|---|---|---|
| Login / Register / Invite | 10 attempts | 15 minutes |
| MFA verify-login / enable | 5 attempts | 15 minutes |
| Password change | 5 attempts | 15 minutes |
| MCP token creation | 5 attempts | 15 minutes |
These limits are per source IP. If TREK is behind a reverse proxy, set TRUST_PROXY so the real client IP is used rather than the proxy's IP.
Content Security Policy
Helmet applies a strict CSP on all responses. Key directives:
default-src 'self'script-src 'self' 'wasm-unsafe-eval' 'unsafe-eval'(nounsafe-inline;'unsafe-eval'is required by the in-browser HEIC converter, which initialises throughnew Function())object-src 'self'(so same-origin file previews can embed PDFs via<object>/<embed>)frame-src 'self'(for the sandboxed plugin frames at/plugin-frame/*, which run at an opaque origin under their own CSP)frameAncestors 'self'(prevents clickjacking from external frames)upgrade-insecure-requests(added automatically whenFORCE_HTTPS=true)
Plugin Runtime Hardening
Installed plugins run untrusted third-party code. TREK contains a plugin in several independent layers so a hostile or buggy plugin can neither read TREK's data nor take the instance down. Nothing here needs configuration — it is all on by default — but the escape hatches below exist for tuning.
- Leave the plugin system's defaults in place. It is on by default but installed plugins still have to be activated one by one, so no third-party code runs until an admin turns a specific plugin on. Set
TREK_PLUGINS_ENABLED=false(acceptsfalse/0/off/no) to switch the whole system off — installed plugins stay on disk, deactivated, and the runtime is idle. - Keep the OS permission jail enabled (the default). In production each plugin runs in an isolated child process launched with Node's
--permissionmodel: filesystem writes,child_process, worker threads and native addons are denied outright, and reads are scoped to just the plugin's own code — so a plugin cannot readtrek.dbor the secret files, or shell out. The child's environment is scrubbed (noJWT_SECRET, no DB credentials). SettingTREK_PLUGIN_PERMISSIONS=offdisables this jail (isolation then falls back to crash-only) and logs a loud warning — only ever do this on a machine you fully trust. - Rely on the private-egress block (SSRF backstop). Even a plugin that declared an outbound host cannot reach a destination that resolves to a loopback, private, link-local, ULA, carrier-grade-NAT, cloud-metadata (
169.254.169.254), multicast or reserved address — the guard re-checks the resolved IP, so a plugin can't pivot to internal services or DNS-rebind to them. This is independent ofALLOW_INTERNAL_NETWORK(which only governs core Immich/Synology features). The one escape hatch isTREK_PLUGIN_ALLOW_PRIVATE_EGRESS=on, for a plugin that has to reach a service on your LAN (a Gotify, an ntfy, an Ollama). It is instance-wide rather than per plugin, and it lifts the block entirely rather than narrowing it: a declared host may then resolve to any address, the169.254.169.254metadata IP included, and unix-socket or named-pipe connects (docker.sock, a local database socket) are permitted without even being declared, since the OS jail does not gate socket connects. Leave it unset unless a specific plugin genuinely needs a LAN target. - The supervisor caps each plugin's resident memory (default 300 MB,
TREK_PLUGIN_MAX_RSS_MB) — measured host-side from the OS, never the plugin's self-report — and kills a plugin that blows the ceiling or stops sending heartbeats; repeat offenders auto-disable. Everyctx.*capability call is also rate-limited at the dispatch boundary (a token bucket: ~60-call burst, 20 calls/sec sustained, 16 concurrent;TREK_PLUGIN_RPC_BURST/TREK_PLUGIN_RPC_PER_SEC/TREK_PLUGIN_RPC_INFLIGHT), so one plugin in a tight loop gets throttled instead of freezing the instance. - Review the capability audit if you grant plugins broad data access. Every host-mediated core-data read and broadcast a plugin makes is recorded at the RPC boundary against the real acting user (not a value the plugin supplies) in a per-plugin, hash-chained, tamper-evident log. Admins see it per plugin; each user can see "what have plugins done in my name?". Retention is capped per plugin (default 20 000 rows,
TREK_PLUGIN_AUDIT_MAX_ROWS).
The developer dev-link feature (
TREK_PLUGINS_DEV_LINK=1) loads unsigned local code and, undernpm run dev, runs with the OS jail off — keep it off on any instance that isn't a throwaway dev box you control. See Plugins and Plugin Permissions.
Backups
- Enable auto-backup with an appropriate retention window. See Backups.
- Store backups off-site — copy backup ZIPs to a separate location outside the TREK host.
Monitoring
- Review the audit log periodically for unexpected logins or admin changes. See Audit-Log.
- Check for TREK updates regularly. See Admin-GitHub-Releases and Updating.