11 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 a strict-guard integration — Immich, Synology Photos, AirTrail, Dawarich, a document store bound through Document-Sync (Paperless-ngx, Papra, Nextcloud, OpenCloud, Synology Drive), a notification webhook or ntfy — is on your LAN. See Internal-Network-Access. Note: loopback (127.x,::1) addresses are always blocked regardless of this setting, and so are link-local (169.254.x) addresses, apart from a single one you name inALLOW_LINK_LOCAL_IPS, which the strict guard then treats as internal. See A link-local address you need.
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)
Every form that sets a password (registration, including through an invite link, the new password asked for on first login, the password reset, a password change in Settings > Account, and the admin's Create User and Edit User dialogs) lists these rules as a checklist under the field and ticks each one off while you type, using the server's own policy.
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 governs the core integrations reached through the strict guard — Immich, Synology Photos, AirTrail, Dawarich, the Document-Sync stores, notification webhooks and ntfy — not plugin egress). 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.Likewise leave
TREK_PLUGINS_IGNORE_TREK_RANGEunset. It turns the plugin TREK-version gate into a warning so a plugin whose author has not updated itstrekrange can still be installed and activated — the admin panel warns at every step, but a plugin running on a TREK it was never tested against can misbehave and, in rare cases, corrupt data. Set it only for a specific plugin you need, and remove it once the author ships a release that admits your TREK.
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.