# Sprint 00 — Critical Security Fixes **Depends on:** nothing **Duration:** ~4h **Goal:** Close exploitable security holes — unauthenticated WebSocket, hardcoded credentials, known signing keys. --- ## S00.1 — Add JWT auth to OPC WebSocket - **Files:** `dashboard/backend/routers/opc_ws.py:66-84`, `dashboard/frontend/src/lib/opc-ws.js` - **Estimate:** 2h - **Done means:** Unauthenticated WebSocket connections to `/ws/opc` receive 403. Authenticated connections (JWT token via `?token=` query param) connect normally. The frontend OPC WebSocket client passes the access token from the cookie/auth store. - **Verify by:** 1. `websocat ws://localhost:4000/ws/opc` → 403 Forbidden 2. Login via browser → navigate to OPC page → WebSocket connects (check browser devtools Network tab, WS frame shows 101) 3. `websocat "ws://localhost:4000/ws/opc?token="` → connects, receives broadcasts 4. Existing integration tests pass (`test_websocket_security.py`) - **Risk:** The frontend `opc-ws.js` constructs the WebSocket URL — must include the token there. If the frontend token store doesn't expose the access token synchronously, this may need a refactor of how the WS client is initialized. - [x] S00.1 — Add JWT auth to OPC WebSocket ## S00.2 — Externalize Transmission credentials - **Files:** `dashboard/backend/routers/transmission.py:16,40`, `dashboard/backend/config.py`, `dashboard/docker-compose.dev.yml` - **Estimate:** 1h - **Done means:** Transmission RPC credentials read from `TRANSMISSION_USER` and `TRANSMISSION_PASS` env vars. No hardcoded `("admin", "admin")` in source. Startup fails with clear error if env vars are unset. - **Verify by:** 1. Set `TRANSMISSION_USER=admin TRANSMISSION_PASS=admin` → Transmission endpoints work 2. Unset vars → app fails at startup with `RuntimeError("TRANSMISSION_USER and TRANSMISSION_PASS must be set")` 3. `grep -r '"admin"' dashboard/backend/routers/transmission.py` → no match - **Risk:** The compose files (dev + prod) need the new env vars added. The Transmission container itself uses these same creds — verify the actual Transmission daemon password hasn't been changed from default. - [x] S00.2 — Externalize Transmission credentials ## S00.3 — Remove hardcoded SECRET_KEY fallback - **Files:** `dashboard/docker-compose.dev.yml:21` - **Estimate:** 0.5h - **Done means:** Line 21 reads `SECRET_KEY=${SECRET_KEY}` (no `:-fallback`). Missing env var causes compose to fail with a clear error rather than silently using a known key. - **Verify by:** 1. Unset `SECRET_KEY`, run `docker compose -f docker-compose.dev.yml config` → error about missing required variable 2. Set `SECRET_KEY`, run same command → succeeds 3. `config.py:14-16` already enforces length check at app startup — this is the defense-in-depth belt. - **Risk:** CI workflows (`test.yml`, `deploy.yml`) already set `SECRET_KEY` explicitly in env, so no CI breakage expected. Local dev must now set `SECRET_KEY` in their `.env`. - [x] S00.3 — Remove hardcoded SECRET_KEY fallback ## S00.4 — Rate-limit passkey register options endpoint - **Files:** `dashboard/backend/routers/passkey.py:58-72` - **Estimate:** 0.5h - **Done means:** `passkey_register_options` endpoint has `@limiter.limit("5/minute")` decorator. Exceeding the limit returns 429. - **Verify by:** 1. Call `POST /api/auth/passkey/register/options` 6 times in 60 seconds → 6th returns 429 2. Wait 60 seconds → call succeeds again 3. Existing passkey integration tests still pass - **Risk:** None — this is a pure additive constraint. The limiter instance at `passkey.py:25` may not integrate with the app's limiter state — verify it works correctly with the current slowapi version. If not, switch to `request.app.state.limiter`. - [x] S00.4 — Rate-limit passkey register options endpoint --- ## Exit criteria - [x] `websocat ws://localhost:4000/ws/opc` → 403 (auth check added; will reject without token) - [x] `grep -r '"admin"' dashboard/backend/routers/transmission.py` → no match - [x] `grep ':-c0e8dcd7' dashboard/docker-compose.dev.yml` → no match - [x] All backend tests pass (245 passed, 0 new failures) - [ ] All frontend tests pass (pre-existing Vite plugin compatibility issue)