Files
nas-tools/specs/sprint-00-critical-security.md
Gan, Jimmy ddaf3c6cee security: Sprint 00 — critical security fixes (OPC WS auth, Transmission creds, SECRET_KEY, passkey rate limit)
- 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>
2026-05-03 13:31:25 +08:00

71 lines
4.2 KiB
Markdown

# 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=<valid_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)