security: HSTS + per-IP rate limit on canopy-web BFF (bypasses ApiServer::router hardening)
Surfaced
External audit re-verified 2026-05-27.
Finding
canopy-web/src/main.rs:247-251 deliberately bypasses ApiServer::router() because that router applies JWT auth + /v1 nesting which are wrong for a browser-facing BFF. But the bypass also loses the security middleware that ApiServer carries:
| Middleware | ApiServer (program services) | canopy-web (browser-facing BFF) |
|---|---|---|
HSTS (Strict-Transport-Security) |
✓ max-age=31536000; includeSubDomains; preload (crates/canopy-api/src/lib.rs:175-180) |
✗ absent (grep STRICT_TRANSPORT services/canopy-web/src/main.rs empty) |
Per-IP rate limit (governor) |
✓ default 6000 rpm, IP-keyed (crates/canopy-api/src/lib.rs:131-137) |
✗ absent (grep -rn 'governor|rate_limit|RateLimit' services/canopy-web/ empty) |
| Existing canopy-web layers | — | ✓ X-Content-Type-Options=nosniff, X-Frame-Options=DENY, strict CSP, DefaultBodyLimit |
The most-exposed surface in the system — login, OAuth callback, session cookie issuance, htmx endpoints — has no per-IP rate limit and no HSTS.
Concrete attack surfaces
/login,/auth/callback,/logout,/auth/discover(services/canopy-web/src/main.rs:258-269) — brute force / enumeration unprotected.- HSTS absence means the first plaintext-HTTP visit (or a downgrade attack) is unprotected. For an eligibility system carrying FTI / HIPAA traffic, this is an ATO-relevant gap.
Acceptance criteria
- Lift
build_rate_limiter+ middleware out ofcanopy-apiso canopy-web can layer them on its own router (or inline a per-canopy-web rate-limiter — the layer is small). - Add
SetResponseHeaderLayerforStrict-Transport-Securityalongside the existing X-Frame-Options block atservices/canopy-web/src/main.rs:302-308. - Stricter rate-limit on
/login+/auth/*than on app routes (e.g. 30/min/IP vs 6000/min/IP). Config-driven viaCANOPY_WEB__RATE_LIMIT_RPM+CANOPY_WEB__AUTH_RATE_LIMIT_RPM. - Integration test:
/login31st request from same IP within 60s returns 429.
Why priority::high
- ATO-relevant. Public-visible (BFF reachable from the internet in any non-dev deploy).
- Both fixes are ~5-line additions; the engineering pattern already exists in
canopy-api.
Labels
type::security, priority::high, workflow::ready, service::web, program::infrastructure