Backend security hardening for a small public server

Audit-driven; goal: one user can't exhaust the box or starve the shared Traefik stack.

Resource containment:
- compose: mem_limit 512m, memswap_limit, pids_limit 200, cpus 1.0 (blast radius = container)
- Enforce per-user cloud quota (default 30MB, admin override) on /api/save + /api/characters → 413
- Cap published-character size (2MB); cap rooms (200), images/room (40), image bytes/room (40MB)
- Cap WS image dataUrl length in the wire schema; per-IP WS connection cap (20)
- MAX_USERS registration ceiling → 503 when full

Availability + correctness:
- Async crypto.scrypt (was scryptSync) so password hashing can't freeze the event loop;
  constant-time on unknown users to avoid username probing
- trustProxy so req.ip is the real client behind Traefik — rate limits are per-IP, not global
- Tighter auth-endpoint bucket (10/min) on /register + /login; prune stale rate buckets
- O(1) token->user index; log persist() failures instead of swallowing them
- Trim /healthz info; surface quota in /api/usage + the admin dashboard storage bar
- Client surfaces 413 (quota) / 429 (rate) clearly

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 20:37:14 +02:00
parent 8fd530df73
commit 0892b8f19c
12 changed files with 244 additions and 41 deletions
+10
View File
@@ -17,10 +17,20 @@ services:
- ALLOWED_ORIGINS=https://ttrpg.briggen.dev
# comma-separated usernames that can see the admin panel (set to your account)
- ADMIN_USERS=nilsb
# default per-user cloud storage cap (bytes); admins override per user in the dashboard
- DEFAULT_QUOTA_BYTES=31457280 # 30 MB
# hard ceiling on accounts so open registration can't fill the disk
- MAX_USERS=500
volumes:
- ttrpg-data:/data
security_opt:
- no-new-privileges:true
# Hard caps so a runaway session can't starve the host or the shared Traefik
# stack — the blast radius stays this container, then `restart: unless-stopped`.
mem_limit: 512m
memswap_limit: 512m
pids_limit: 200
cpus: 1.0
labels:
- "traefik.enable=true"
- "traefik.http.routers.ttrpg.rule=Host(`ttrpg.briggen.dev`)"