# Security Guide Layered security model for Linear Void. ## Threat Model ``` ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ Client │───▶│Cloudflare│───▶│ nginx │───▶│ Backend │ │ (public) │ │ WAF+CDN │ │ proxy │ │ chat │ └──────────┘ └──────────┘ └────┬─────┘ └────┬─────┘ │ │ ┌──────┴──────┐ ┌─────┴──────┐ │ /health │ │ /api/admin │ │ /metrics │ │ /ws (chat) │ │ /hls/* │ │ /api/chat/* │ │ (public) │ │ (auth req) │ └─────────────┘ └────────────┘ ``` ## Authentication & Sessions - Login: `POST /api/admin/login` → cookie `linearvoid_admin_session` - Cookie flags: `HttpOnly`, `SameSite=Strict`, `Secure` (over HTTPS) - Server-side expiry via `ADMIN_SESSION_TTL_HOURS` - Rate limit: 5 attempts/IP/5min via Redis ## Transport Security (TLS/HSTS) - Cloudflare TLS mode: **Full (strict)** — valid origin cert required - nginx listens on 80 internally; Cloudflare handles TLS termination - HSTS header set by Cloudflare: `max-age=31536000; includeSubDomains; preload` - WSS upgrade path: `/ws` through nginx → chat:3000 ## Network Isolation - All backend services on internal Docker network (`linearvoid-net`) - Only nginx port exposed to host (443/80) - Database, Redis, exporters inaccessible from outside - `docker-compose.prod.yml` keeps ports internal via `expose:` not `ports:` ## Reverse Proxy Controls - `auth_request /api/admin/auth` gate on all `/admin` and `/api/admin` paths - Explicit `client_max_body_size 64m` for resumable upload chunks - WebSocket upgrade headers: `Upgrade`, `Connection` preserved - CORS headers on HLS: `Access-Control-Allow-Origin *` - XSS protection: CSP headers served by Cloudflare ## Rate Limiting | Resource | Limit | Window | | ----------------- | ----- | ------ | | WebSocket connect | 5/IP | 60s | | Chat messages | 10/IP | 10s | | Admin login | 5/IP | 5min | Implemented via Redis `INCR` + `EXPIRE`. ## Secrets Management - `.env` — local dev only, **gitignored** - Gitea Actions secrets (`CI_*` prefix) for CI/CD - Never commit: passwords, tokens, API keys, R2 credentials - Production `.env` provisioned manually on host, never checked in - Rotate admin password quarterly ## Data Protection - PostgreSQL: chat history, moderation logs, media metadata - Redis: append-only persistence for presence, rate limits, sessions - HLS segments: ephemeral, regenerated by playout - Backups: `scripts/backup-db.sh` (daily), `scripts/backup-weekly.sh`, `scripts/verify-backups.sh` ## Hardening Checklist - [ ] HTTPS/WSS enforced end-to-end (Cloudflare Full strict) - [ ] Strong admin credentials configured - [ ] Secrets only in env/secrets stores (never committed) - [ ] Proxy auth and websocket headers validated - [ ] Upload limits and MIME validation active - [ ] Rate limiting active (WS, messages, login) - [ ] Cloudflare WAF/rate-limit rules active - [ ] Backup/restore drills completed - [ ] Security alerts routed (Prometheus alertmanager) ## Incident Response 1. Detect — alerts (ChatDown, RedisDown, HighChatLatency) or user reports 2. Triage — edge (Cloudflare), proxy (nginx), app (chat/playout), data (Redis/Postgres) 3. Contain — rate limit, credential rotate, service restart, slate mode activation 4. Recover — verify health endpoints, check Prometheus targets, validate HLS output 5. Record — timeline + corrective actions in `docs/incident-runbook.md`