# M0 Security Hardening — Progress Report **Status: Complete** **Build: `cargo build --workspace` — zero errors** **Tests: `cargo test --workspace` — 10 passed, 0 failed, 2 ignored** --- ## 0.1 — Secrets & Credential Hygiene | Fix | File | Detail | |-----|------|--------| | Remove JWT secret logging | `auth/src/middleware.rs` | `tracing::info!` with secret value → `tracing::debug!` without value | | Remove confirmation token logging | `auth/src/handlers.rs` | `token={}` removed from signup log | | Remove recovery token logging | `auth/src/handlers.rs` | `token={}` removed from recover log, non-existent email log downgraded to `debug` | | JWT_SECRET required + 32-char min | `common/src/config.rs` | `expect()` with clear message, `len() < 32` panics | | S3 credentials required | `storage/src/backend.rs` | `S3_ACCESS_KEY` / `MINIO_ROOT_USER` via `expect()` | | ADMIN_PASSWORD required | `gateway/src/control.rs` | Login handler reads `ADMIN_PASSWORD` env var, panics if unset | ## 0.2 — Authentication & Authorization | Fix | File | Detail | |-----|------|--------| | Session-based admin auth | `gateway/src/admin_auth.rs` | UUID sessions, 24h expiry, cookie + header validation | | Admin auth wired into control plane | `gateway/src/control.rs` | `from_fn_with_state(admin_auth_state, ...)` | | Login endpoint | `gateway/src/control.rs` | `POST /platform/v1/login` — validates `ADMIN_PASSWORD`, creates session, sets `HttpOnly; SameSite=Strict` cookie | | Tests | `gateway/src/admin_auth.rs` | 5 passing tests for session accept/reject/dashboard/login bypass | ## 0.3 — Injection & Input Sanitization | Fix | File | Detail | |-----|------|--------| | SQL injection in `SET LOCAL role` | `data_api/src/handlers.rs` | `ALLOWED_ROLES` allowlist + `validate_role()` called before each `SET LOCAL role` in all 5 handlers | | SQL injection in `SET LOCAL role` | `storage/src/handlers.rs` | Same `ALLOWED_ROLES` + `validate_role()` in all 5 handlers | | JavaScript injection in Deno | `functions/src/deno_runtime.rs` | Payload/headers double-serialized; JS uses `JSON.parse()` to decode safely | | Path traversal in TUS uploads | `storage/src/tus.rs` | `validate_upload_id()` requires valid UUID; `get_upload_path()` and `get_info_path()` return `Result` | ## 0.4 — Token & Session Security | Fix | File | Detail | |-----|------|--------| | Signup: gate tokens on confirmation | `auth/src/handlers.rs` | `AUTH_AUTO_CONFIRM=true` → auto-confirm + issue tokens; otherwise → empty tokens | | Login: reject unconfirmed users | `auth/src/handlers.rs` | `email_confirmed_at.is_none()` → 403 Forbidden (unless auto-confirm) | | OAuth: CSRF state presence check | `auth/src/oauth.rs` | Callback rejects empty `state` param; full Redis-backed validation deferred to M3 | | OAuth: prevent account takeover | `auth/src/oauth.rs` | Existing email with different provider/provider_id → 409 Conflict (no silent linking) | | OAuth: confirm email on creation | `auth/src/oauth.rs` | New OAuth users get `email_confirmed_at = now()` | ## 0.5 — CORS & Transport Security | Fix | File | Detail | |-----|------|--------| | Restrict CORS origins (control) | `gateway/src/control.rs` | `ALLOWED_ORIGINS` env var parsed → `AllowOrigin::list(...)`, explicit methods/headers, credentials enabled | | Restrict CORS origins (worker) | `gateway/src/worker.rs` | Same `ALLOWED_ORIGINS` → `AllowOrigin::list(...)`, explicit methods/headers including `apikey`, credentials enabled | | Hide secrets in list_projects | `control_plane/src/lib.rs` | `ProjectSummary` struct (id, name, status, created_at) — no `db_url`, `jwt_secret`, `anon_key`, `service_role_key` | --- ## Additional Fixes (pre-existing build issues resolved) | Fix | File | Detail | |-----|------|--------| | Markdown corruption in 5 files | `auth/src/handlers.rs`, `data_api/src/handlers.rs`, `storage/src/handlers.rs`, `gateway/src/control.rs`, `gateway/src/worker.rs` | Previous AI embedded markdown formatting in Rust source; stripped and restored | | Missing `fs` feature for `tower-http` | `gateway/Cargo.toml` | Added `"fs"` feature for `ServeDir` | | Missing `redis` workspace dep | `Cargo.toml`, `common/Cargo.toml`, `gateway/Cargo.toml` | Added `redis = { version = "0.25", features = ["tokio-comp", "aio"] }` | | Missing `uuid`/`chrono` deps | `gateway/Cargo.toml`, `common/Cargo.toml` | Added workspace deps | | Cache module not exported | `common/src/lib.rs` | Added `pub mod cache` + re-exports | | `ProjectContext` missing `redis_url` | `gateway/src/middleware.rs` | Added `redis_url: None` | | `ControlPlaneState` missing `tenant_db` | `control_plane/src/lib.rs`, `gateway/src/main.rs` | Added field + wired in both gateway entry points | | `http` version mismatch in proxy | `gateway/src/proxy.rs` | Converted between `reqwest` (http 0.2) and `axum` (http 1.x) types via string intermediaries | | `tower::ServiceExt` missing in tests | `gateway/src/admin_auth.rs` | Added import; added `tower` dev-dependency | --- ## Deferred to Later Milestones - **M1**: Argon2 hashing for `ADMIN_PASSWORD` (currently plaintext comparison) - **M3**: Redis-backed CSRF state for OAuth flows - **M3**: Redis-backed admin sessions (currently in-memory) - **M3**: Proper OAuth identity linking with `identities` table