-
Notifications
You must be signed in to change notification settings - Fork 0
Security Model
What this coordinator defends, how, and where the edges are.
To report a vulnerability, use private vulnerability reporting — never a public issue. See
SECURITY.md.
browser ──TLS──▶ reverse proxy ──loopback──▶ coordinator ──WebSocket──▶ agents
│
└── SQLite + files (one volume)
- The operator is the owner. This is a single-owner deployment. There is no multi-tenant boundary to breach because there is one tenant.
-
Agents are semi-trusted. They hold a token that authorises connecting and
running work. They do not get to decide their own dispatch share — the
coordinator owns
active_sessions, and an agent's claimed capacity is capped by the ceiling in its token. Never point an agent you do not control at a coordinator you care about. - The sites you crawl are hostile. Their content is data, never instruction.
The coordinator refuses to start in production when:
- any signing secret is blank, under 32 characters, or a shipped default —
HS256 signs happily with an empty key, so a half-filled
.envwould otherwise leave the token-signing key publicly known; -
SECRET_ENCRYPTION_KEYis missing, or is one of the known-compromised values that were once committed to source control; -
CORS_ORIGINSis*while credentials are enabled; -
WRIT_PUBLIC_URLis unset.
It warns loudly but starts when WRIT_PUBLIC_URL is plaintext http:// on a
routable host, and when REQUIRE_ADMIN_MFA is off.
- Passwords are hashed with Argon2.
- Failed logins are throttled on both the IP and the account — one host spraying many accounts and many hosts hammering one account are different attacks and both are covered. A non-existent account still pays a constant-time dummy verify, so timing does not leak whether an account exists.
- Repeat offenders are IP-banned automatically, checked early in the request pipeline.
- Second factors: TOTP and passkeys (WebAuthn). Set
REQUIRE_ADMIN_MFA=trueafter enrolling, or you will lock yourself out of the admin surface. - Refresh tokens rotate, with a short grace window so a browser reload during an in-flight refresh does not log you out.
| Host allowlist | Spoofed Host headers are rejected in production. Derived from WRIT_PUBLIC_URL; extra names via ALLOWED_HOSTS or Settings → Network, which applies live. |
| Rate limiting | A per-IP ceiling across everything, plus much stricter per-path limits on login, password reset, pairing and webhooks. |
| Body size caps | 1 MB by default; larger only on the routes that genuinely stream. |
| Security headers | HSTS in production, X-Content-Type-Options, X-Frame-Options: DENY, a CSP with no external origins, Referrer-Policy, Permissions-Policy. |
| CSP | The header and the SPA's <meta> CSP are kept identical in substance — a browser enforces both and takes the intersection, so drift silently breaks the app. Fonts are self-hosted; nothing phones a CDN. |
Dataset renders that echo scraped third-party content back at you are served
under their own stricter default-src 'none', which the global policy never
overwrites.
Screening is on in every environment, including development — it is
deliberately decoupled from ENVIRONMENT so a dev instance cannot be talked into
reading cloud metadata. Private, loopback, link-local and metadata ranges are
blocked; schemes are checked; DNS is pinned and redirects re-validated so a
target cannot resolve safe and then rebind.
ALLOW_PRIVATE_TARGETS=true opens the private-range check only — set it only
if you genuinely need to monitor intranet apps. Every other screen stays active.
Stored credentials are encrypted with a Fernet key (SECRET_ENCRYPTION_KEY).
The key lives in .env; the ciphertext lives in the database. Keep them apart —
see Backup and restore.
Password fields detected during recording are never written into a workflow as a
literal value, and one-time-code fields are recorded as a resilient twofa step
rather than the code you typed.
Both services run non-root, with a read-only root filesystem, cap_drop: ALL, and no-new-privileges. The only writable location is the /data volume;
/tmp is a noexec tmpfs. An attacker who reaches a file-write primitive cannot
modify the application code and cannot persist across a restart.
The coordinator publishes on loopback only by default. doc-extract also
publishes on loopback — its shared secret is the only thing in front of it, so
never expose port 8092 on a public interface.
-
/metricsis unauthenticated whenENABLE_METRICS=true— that is what Prometheus expects. It discloses your route topology and traffic shape. Scrape it over a private network or behind your proxy's auth. - The generic API rate limiter fails open. On a limiter outage availability wins over throttling. Money- and authz-sensitive paths fail closed instead.
- Single owner, single worker. There is no horizontal scaling story for the coordinator; it is not a gap so much as the design.
-
AGPL §13: if you modify this and let others use it over a network, you owe
them your source.
GET /api/aboutmakes that offer for you — pointWRIT_SOURCE_URLat your own repository if you patch it.
- TLS terminated,
WRIT_PUBLIC_URLis thehttps://URL -
FORWARDED_ALLOW_IPSnames your proxy and is not* - Every admin has a second factor, then
REQUIRE_ADMIN_MFA=true -
SECRET_ENCRYPTION_KEYbacked up off-host - Firewall allows 80/443 inbound and nothing else
-
ALLOW_PRIVATE_TARGETSandALLOW_INSECURE_DEVboth false -
.envischmod 600and not committed
usewrit/writ · AGPL-3.0-only · Issues · Discussions · Report a vulnerability
Getting started
Using it
Integrations
Operations
Reference