Files
cyclone/docs/superpowers/specs/2026-06-21-cyclone-security-hardening-design.md
T
Tyler c835996bd6 feat(sp19): security hardening + rich health probe
Three pure-ASGI middlewares close completeness-review gaps §3.1.4
(no body/rate limits) and §3.1.25 (no security headers):

- BodySizeLimitMiddleware — rejects oversized uploads (50 MB
  default, CYCLONE_MAX_BODY_BYTES override). 413 on over-cap
  Content-Length and on chunked reads that cross the cap.
- RateLimitMiddleware — sliding-window per-IP limiter (300/min
  default, CYCLONE_RATE_LIMIT_PER_MIN override). 429 over the
  window. /api/health is exempt.
- SecurityHeadersMiddleware — stamps X-Content-Type-Options,
  X-Frame-Options, Referrer-Policy, Permissions-Policy, and a
  strict Content-Security-Policy on every response.

Every 413/429 also writes a tamper-evident api.request_rejected
event into the SP11 audit chain so an operator can correlate
rejections with the SP18 JSON logs.

GET /api/health is rewritten to return a subsystem snapshot:
DB connectivity (SELECT 1), MFT scheduler state, backup scheduler
state, live pubsub subscriber counts, last batch id + timestamp.
Returns status='degraded' if any subsystem is unhappy; per-subsystem
errors surfaced in the respective dict.

Cyclone.pubsub.EventBus.stats() — new method for live subscriber
counts.

13 new tests (test_security.py) + 1 updated (test_api.py health
endpoint). All 883 backend tests pass.
2026-06-21 10:21:01 -06:00

3.1 KiB

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.