Some checks failed
CI / Build & test (Node 24) (push) Has been cancelled
CI / Pack & boot smoke (Node 22) (push) Has been cancelled
CI / No em/en dashes (push) Has been cancelled
CI / Build & test (Node 22) (push) Has been cancelled
Deploy site (production) / build-and-deploy (push) Successful in 20s
Remplace les 547 tirets cadratins (U+2014) et demi-cadratins (U+2013) des fichiers versionnés par la ponctuation contextuelle adaptée (point médian, deux-points, virgule, parenthèses ; tiret simple pour les plages), sur 122 fichiers (appli, vitrine, doc, tests, workflows, scripts). Ajoute le job CI « lint-dashes » (git grep -P) qui échoue si un tiret cadratin/demi-cadratin réapparaît, hors logo binaire et captures brutes du terminal (fidélité des fixtures de détection de dialogue).
4.4 KiB
4.4 KiB
Security Policy
Arboretum is a self-hosted daemon that serves a web dashboard to drive git worktrees and the Claude Code sessions running on them. A web terminal is remote code execution by design: that is the product, not a bug. Arboretum's security model is therefore built on structural guards (loopback-only binding, authenticated access, strict Origin checks) far more than on cryptography alone.
This document describes the threat model, the controls that are implemented, the deliberate
trade-offs, and how to report a vulnerability. For hardening a deployment in a regulated
environment, see docs/ENTERPRISE_DEPLOYMENT.md.
Threat model
- Single-user by design. Arboretum runs on the owner's machine and is meant for one operator. There is no multi-tenant isolation and no RBAC, and none is claimed.
- Loopback by default. The server binds
127.0.0.1;config.tsrefuses any non-loopback bind unless you pass--i-know-this-exposes-a-terminal. Remote access is expected via Tailscale Serve (TLS + tailnet identity), never by opening a port. - The terminal is RCE. Anyone who can authenticate can run code. The controls below exist to make sure only the authenticated operator reaches it, and that the surrounding surface (cookies, headers, data at rest) is hardened.
Implemented controls
| Area | Control | Where |
|---|---|---|
| Network | Loopback-only default; non-loopback refused without explicit flag | packages/server/src/config.ts |
| AuthN | Global guard on all /api/** and /ws (only login is public, and rate-limited) |
packages/server/src/app.ts |
| AuthN | Tokens stored hashed (SHA-256), compared in constant time; bootstrap token shown once | packages/server/src/auth/service.ts |
| Sessions | Cookie is an HMAC-SHA256 signed payload, HttpOnly + SameSite=Strict, Secure when HTTPS |
packages/server/src/routes/auth.ts |
| CSRF / WS | Strict Origin check on every /api/** and /ws request (anti cross-site WS hijacking) |
packages/server/src/app.ts |
| CSRF | Mutations carrying a body must be application/json |
packages/server/src/app.ts |
| Rate limit | Global login rate limit with exponential backoff (not per-IP, Tailscale fronts everything as 127.0.0.1) | packages/server/src/auth/service.ts |
| HTTP headers | CSP, X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy, Permissions-Policy, conditional HSTS, Cache-Control: no-store on API; Server header stripped |
packages/server/src/app.ts |
| Injection | All SQL is parameterized; all git calls use execFile (no shell); path-traversal guards |
packages/server/src/** |
| Data at rest | DB file/dir forced to 0o600/0o700; sensitive secrets (server secret, VAPID private key) encrypted with AES-256-GCM |
packages/server/src/db/index.ts, core/secret-box.ts |
| Audit | Persistent audit log of sensitive mutations (tokens, settings, secrets, push, groups) | packages/server/src/core/audit-log.ts |
| Privacy | GDPR export (/api/v1/data/export) and erasure (/api/v1/data/delete-my-data) |
packages/server/src/routes/data.ts |
| Install | Runs as a user service (systemd user unit / launchd LaunchAgent), never root | packages/server/src/cli/install.ts |
Deliberate trade-offs
- Long-lived API tokens. Tokens do not expire by age (CLI automation stability) but can be revoked
instantly, and
last_used_atis tracked. Review and rotate tokens periodically. - Encryption-at-rest key management. With no
ARBORETUM_SECRET_KEYset, the encryption key lives indataDir/secret.key(0o600) next to the database: this protects a leaked database copy (backup, WAL) but not a fulldataDircompromise. For strong protection, setARBORETUM_SECRET_KEYand store it separately from database backups. Full-DB SQLCipher is intentionally avoided (it breaks thenpxprebuilt portability). - No multi-user model. If you need multiple operators with isolation, Arboretum is not the right tool.
Reporting a vulnerability
Please report security issues privately: do not open a public issue.
- Email: security@johanleroy.fr (or
contact@johanleroy.fr). - Include a description, affected version, and reproduction steps.
- Expect an acknowledgement within 7 days and a coordinated disclosure window of up to 90 days.
Thank you for helping keep Arboretum users safe.