Passive mode vs fail-secure
The rollout playbook - deploy fastapi-guard into a live system without bricking it on day one, and without leaving it muted forever.
Cold-flipping a security middleware on production is how trust in the security middleware dies. Two failure modes, both end the same way: the team disables the thing.
Two ways a security rollout goes wrong
You brick day one. Defaults block legitimate traffic on first deploy. False positives flood support. Infrastructure jitters (Redis hiccup, network blip) return 500s on healthy requests. Engineering disables the middleware "until we figure it out", and never reverses the decision.
You leave it muted forever. passive_mode=True ships to production "just for now." Dashboard fills with detection events nobody acts on. Real attacks show up as colorful charts and nothing else. Security theater.
The playbook: stage in. Verify before each promotion. Three knobs do the work.
Three independent switches
These three settings control completely different aspects of the request lifecycle. Setting one doesn't imply the others. They compose freely.
passive_mode - block or just observe?
When True: detections compute and emit to the agent, but the request is NOT blocked. A [PASSIVE MODE] marker is appended to the log line. Bans are not applied. All detectors run normally (SQLi, command injection, SSRF, file inclusion, behavior rules). The dashboard shows events as if nothing changed.
WARNING [PASSIVE MODE] sqli detected · path=/search · ip=203.0.113.42 · pattern=union-selectfail_secure - what to do when the pipeline itself breaks
Different from passive_mode: this isn't about detections, it's about what happens when a check can't run, Redis down, detector timeout, agent unreachable.
- True (default since fastapi-guard 7.0.0; recommended for prod) → pipeline exception returns HTTP 500 "Security check failed". Request dropped. Safe: an attacker can't bypass detection by inducing infra failure. Surface visible.
- False → failing check is skipped, request proceeds as if the check returned clean. Availability-first, but an attacker who can stress your pipeline can bypass detection.
- Redis-only refinement (guard-core ≥3.5.0):
redis_fail_open=Truelets the request through when the failure is specifically a Redis error, while everything else still honorsfail_secure. Off by default. See Redis backend & failure modes.
enforce_https - 301, not 403
Most HTTPS-enforcement middleware return 403 and call it done. fastapi-guard returns a 301 redirect, the right answer for legitimate clients on the wrong scheme.
GET http://api.example.com/items?id=42
→ HTTP/1.1 301 Moved Permanently
Location: https://api.example.com/items?id=42Browsers cache the redirect, bookmarks update, API clients retry on the right scheme. An HTTPS-required attack still doesn't get through. The redirect doesn't carry the original body.
The matrix
fail_secure=False | fail_secure=True | |
|---|---|---|
passive_mode=True | logged, not blocked / skipped on infra fail (Stage 0 - dev) | logged, not blocked / 500 on infra fail (Stage 1 - staging) |
passive_mode=False | blocked / skipped on infra fail (Debug only) | blocked / 500 on infra fail (★ Stage 3 - prod, recommended) |
The four-stage rollout
Promote forward only after the previous stage has been stable for a real window, hours in dev, days in staging, days in canary.
| Stage | Config | Intent |
|---|---|---|
| 0 · Dev | passive_mode=True, fail_secure=False | Log everything. Block nothing. Find out what your traffic actually looks like. |
| 1 · Staging | passive_mode=True, fail_secure=True | Still log-only on detections. Hard-fail on infra problems so you see them before prod does. |
| 2 · Prod canary | passive_mode=False, fail_secure=True (5% traffic) | Real enforcement, small audience. Watch the dashboard for false positives. |
| 3 · Prod full | passive_mode=False, fail_secure=True, enforce_https=True | Full enforcement, full HTTPS. Standing ops from here. |
Verify on the dashboard between each promotion.
Production configuration
SecurityConfig(
log_request_level="INFO",
log_suspicious_level="WARNING",
passive_mode=False,
fail_secure=True,
enforce_https=False, # flip to True once trust_x_forwarded_proto is verified
)Flip enforce_https to True only after you've verified trust_x_forwarded_proto is correct for your topology (see Trusted proxies), otherwise you'll get redirect loops.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| App returns 500 on healthy requests after deploy | fail_secure=True with Redis or detector unreachable | Fix the dependency (preferred). If it's Redis, redis_fail_open=True is the narrower temporary escape; fail_secure=False is the blunt one. Don't leave either on. |
| Detections show in dashboard but attacks aren't blocked | Still in passive_mode=True | Flip passive_mode=False. If you're not ready, you're not in Stage 3 yet, that's fine. |
| Browsers loop between http:// and https:// | enforce_https=True while trust_x_forwarded_proto=False, TLS terminates at LB | Set trust_x_forwarded_proto=True. Backend will trust the LB's header and skip the redirect. |