- Add JWT token auth to OPC WebSocket (unauthenticated → 403, per-user tracking) - Externalize Transmission RPC credentials to TRANSMISSION_USER/PASS env vars - Remove hardcoded SECRET_KEY fallback from dev compose - Rate-limit passkey register options endpoint at 5/minute - Add PDD (docs/improvement-plan.md) and sprint specs (specs/) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
4.2 KiB
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/opcreceive 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:
websocat ws://localhost:4000/ws/opc→ 403 Forbidden- Login via browser → navigate to OPC page → WebSocket connects (check browser devtools Network tab, WS frame shows 101)
websocat "ws://localhost:4000/ws/opc?token=<valid_token>"→ connects, receives broadcasts- Existing integration tests pass (
test_websocket_security.py)
-
Risk: The frontend
opc-ws.jsconstructs 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. -
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_USERandTRANSMISSION_PASSenv vars. No hardcoded("admin", "admin")in source. Startup fails with clear error if env vars are unset. -
Verify by:
- Set
TRANSMISSION_USER=admin TRANSMISSION_PASS=admin→ Transmission endpoints work - Unset vars → app fails at startup with
RuntimeError("TRANSMISSION_USER and TRANSMISSION_PASS must be set") grep -r '"admin"' dashboard/backend/routers/transmission.py→ no match
- Set
-
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.
-
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:
- Unset
SECRET_KEY, rundocker compose -f docker-compose.dev.yml config→ error about missing required variable - Set
SECRET_KEY, run same command → succeeds config.py:14-16already enforces length check at app startup — this is the defense-in-depth belt.
- Unset
-
Risk: CI workflows (
test.yml,deploy.yml) already setSECRET_KEYexplicitly in env, so no CI breakage expected. Local dev must now setSECRET_KEYin their.env. -
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_optionsendpoint has@limiter.limit("5/minute")decorator. Exceeding the limit returns 429. -
Verify by:
- Call
POST /api/auth/passkey/register/options6 times in 60 seconds → 6th returns 429 - Wait 60 seconds → call succeeds again
- Existing passkey integration tests still pass
- Call
-
Risk: None — this is a pure additive constraint. The limiter instance at
passkey.py:25may not integrate with the app's limiter state — verify it works correctly with the current slowapi version. If not, switch torequest.app.state.limiter. -
S00.4 — Rate-limit passkey register options endpoint
Exit criteria
websocat ws://localhost:4000/ws/opc→ 403 (auth check added; will reject without token)grep -r '"admin"' dashboard/backend/routers/transmission.py→ no matchgrep ':-c0e8dcd7' dashboard/docker-compose.dev.yml→ no match- All backend tests pass (245 passed, 0 new failures)
- All frontend tests pass (pre-existing Vite plugin compatibility issue)