# SP19 — Security Hardening + Health Probe

**Date:** 2026-06-21
**Branch:** `sp19-security-hardening`
**Status:** Shipped
**Scope:** Backend only. No frontend changes.

---

## 1. Why this exists

Cyclone's completeness review (`docs/reviews/2026-06-20-cyclone-completeness-review.md`
§3.1.4, §3.1.25, §3.2.24) flags three concrete gaps in the local-only /
single-operator threat model:

* **3.1.4 — No request size limits / no rate limits.** FastAPI defaults
  accept any body size and any request rate. A 4 GB file upload OOMs
  the parser; a flood of requests holds the GIL. Even for a
  `127.0.0.1`-only tool, the operator's machine might be exposed via
  Tailscale, ngrok, or a misconfigured firewall.
* **3.1.25 — No CSP / security headers.** CORS is set; the rest of
  FastAPI's defaults are in play. `X-Content-Type-Options: nosniff`,
  `X-Frame-Options: DENY`, a `Content-Security-Policy` that locks the
  Vite origin, and `Referrer-Policy: same-origin` should be on by
  default.
* **3.2.24 — `/api/health` is shallow.** It returns only the package
  version. A real liveness probe should report DB connectivity, last
  batch timestamp, pubsub subscriber count, and whether the MFT
  scheduler is running.

SP19 closes all three.

## 2. Operator surface

| Env var | Default | Meaning |
|---------|---------|---------|
| `CYCLONE_MAX_BODY_BYTES` | `52428800` (50 MB) | Reject any request whose `Content-Length` exceeds this. |
| `CYCLONE_RATE_LIMIT_PER_MIN` | `300` | Per-IP requests/minute (sliding window). |
| `CYCLONE_HEALTH_INCLUDE_DETAILS` | `true` | Include DB / scheduler / pubsub details in `/api/health`. |

CLI: no new flags. The values come from env vars only.

## 3. Files

* `cyclone/security.py` — new module (~150 LOC).
  * `BodySizeLimitMiddleware` — rejects oversized requests before
    they're parsed.
  * `RateLimitMiddleware` — token-bucket per IP, in-memory.
  * `SecurityHeadersMiddleware` — adds the static response headers.
  * `get_health_snapshot()` — gathers DB / scheduler / pubsub info.
* `cyclone.api` lifespan attaches the three middlewares in the
  FastAPI `add_middleware` chain. `api_routers/health.py` is
  rewritten to use `get_health_snapshot()`.
* Audit log: every rejection (size / rate / method) writes a
  `api.request_rejected` event so an operator can correlate with the
  SP11 hash chain.

## 4. Threat model

The SP19 hardening is sized for Cyclone's actual exposure:

* **In scope:** a misconfigured Tailscale / ngrok / accidental LAN
  bind, a buggy cron job that POSTs a 4 GB file, a port-scanner that
  scrapes the API.
* **Out of scope:** an attacker with shell on the operator's
  machine, a compromised dependency (handled separately by uv
  pinning), a network MitM (handled by SP12 SQLCipher + TLS at the
  reverse proxy layer).

## 5. Tests

* `test_security.py` — 12 tests
  (body size accept/reject, rate limit allow/deny/recover, headers
  present, health snapshot shape, audit-log fired on reject).
* `test_api_health.py` — 4 tests (200 ok, DB-down reports unhealthy,
  scheduler-running reported, pubsub subscriber count surfaced).

Total: 16 new tests.
