| Asset | Attacker | Primary controls |
|---|---|---|
| Account credentials / sessions | Internet brute force, credential stuffing | scrypt (memory-hard), generic login errors, auth rate limit (20/min/IP), session revocation, audit log |
| API tokens | Token theft from disk/logs | Hashed at rest (SHA-256), one-time display, 0600 config file, redaction, instant revoke |
| Route ownership | Cross-user hijack, slot races, username squatting | Segment-aware routing, transactional allocation, reserved names, a- namespace reserved, unique lowercase usernames, offline routes stay owned |
| Quotas | Bypass attempts, double counting | Server-side metering only, fail-closed gate, no client control, persisted aggregates |
| Anonymous capacity | Abuse/flooding | 1800s TTL, per-IP caps, body-size caps, per-tunnel concurrency, rate limits |
| Server resources | Oversized/malformed input | Bounded bodies/frames/queues, runtime frame validation, connection limits, timeouts |
| Admin surface | Privilege escalation | Role checks on every /api/v1/admin/* (server-side only, never frontend guards), self-demotion/last-admin guards, audit of every privileged action |
| Internal network | SSRF via tunnel metadata | Tunnels never dial user-chosen destinations — only the CLI opens local connections; gateway opens no outbound sockets |
- Public internet → gateway: all input untrusted. Paths decoded once, matched on whole segments; system prefixes reserved; hop-by-hop headers stripped; header values capped.
- CLI → control plane: authenticated (token) or anonymous namespace; every frame runtime-validated (
packages/protocol/src/validate.ts); clients cannot register arbitrary usernames/route ids — the server assigns routes from the authenticated identity. - Local app → tunnel: user-controlled app responses pass through;
set-cookie/redirects preserved (that's the product), but response headers cross hop-by-hop stripping and size caps. - Store file: contains only hashes for credentials/token secrets; no recoverable tokens. Treat the volume as sensitive.
- Username rules: length 3–32,
a-z0-9-_, reserved list +a-prefix (unit.test.ts,security.test.ts). - Route parsing: segment-aware,
%2f/%2ecases tested; lookalike usernames never cross-serve (security.test.ts). - Free limit 3 concurrent tunnels; freed by explicit stop; race-safe allocation (integration tests).
- Anonymous expiry: server timestamps only; resume cannot extend; sweeper terminates and marks
expired; restart recovery (e2e + integration). - Quotas: upload=request bytes to local, download=response bytes to visitor; dashboard/API show the same convention; exhausted ⇒ 429
QUOTA_EXCEEDED_*; persisted (integration). - Cookies: HttpOnly, SameSite=Lax, Secure when base URL is https, 7-day expiry, per-session revoke.
- Tokens:
tmin_…random 160-bit, hashed, optional expiry, last-used stamp, revoke test in security suite. - Device login (
tmin login): user code from a 29-symbol alphabet (no0/O/1/I/L/U), dashless at rest, device code random 256-bit stored only as a SHA-256 hash; 600 s TTL; poll interval enforced (SLOW_DOWN); single-use (one token per approved code); approve/deny requires a signed-in session and is audited; unknown/expired codes fail closed (device.test.ts). - Web console pages are static clients of the REST API: the CSP allows the Tailwind CDN and
assets only (
PAGE_CSP), and no authorization decision is made client-side. - Admin: env-bootstrap once; no public admin registration; suspend kills live tunnels and blocks login/public routes (security suite); rename guarded (no live tunnels); audit rows for actions.
- Protocol: JSON frames validated; chunk size caps; heartbeat timeouts close stale sockets; malformed hello/reply frames rejected (
oversized … malformed WS framestest). - Maintenance mode: public gateway fails closed (503) while
/healthstays up. - Error hygiene: structured error bodies with codes + requestId; production 500s are generic (
INTERNAL_ERROR), no stack traces. - Claimed IPs:
X-Forwarded-Forhonored only whenTRUST_PROXY=true(documented for reverse-proxy deployments).
- No per-route access policies yet (basic-auth/bearer/IP allowlist/share tokens are Milestone H). A public URL currently means public access — do not expose sensitive apps expecting URL secrecy.
- Custom domains not implemented.
- Single-instance only. Do not run behind a load balancer with multiple app instances: live registry is process-local.
- Admin global quota
PATCHis runtime-only (not persisted across restarts). - Password reset requires email delivery integration (dev mode returns the token when
NODE_ENV!=production); plan accordingly for production. - Anonymous resume secrets are process-local (restart invalidates pending resumes; sessions still expire correctly).
- No 2FA, no email verification enforcement (optional email stored only).
- Reverse-proxy example (
deployment/Caddyfile) is provided but was not executed in the authoring sandbox; the app layer itself is tested directly.
- Set
SESSION_SECRET(≥32 chars) and a strongADMIN_PASSWORDbefore first production start; the server refuses weak production secrets. - Terminate TLS in front (Caddy example provided) and set
TRUST_PROXY=trueonly behind that proxy. - Back up
DATA_DIR(contains all durable state). RotateADMIN_PASSWORDvia admin reset + bootstrap policy documented in DECISIONS.md. npm auditat your discretion — the runtime dependency surface is one package (ws).