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.

4 min read · verified against [email protected]

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-select

fail_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=True lets the request through when the failure is specifically a Redis error, while everything else still honors fail_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=42

Browsers 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=Falsefail_secure=True
passive_mode=Truelogged, not blocked / skipped on infra fail (Stage 0 - dev)logged, not blocked / 500 on infra fail (Stage 1 - staging)
passive_mode=Falseblocked / 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.

StageConfigIntent
0 · Devpassive_mode=True, fail_secure=FalseLog everything. Block nothing. Find out what your traffic actually looks like.
1 · Stagingpassive_mode=True, fail_secure=TrueStill log-only on detections. Hard-fail on infra problems so you see them before prod does.
2 · Prod canarypassive_mode=False, fail_secure=True (5% traffic)Real enforcement, small audience. Watch the dashboard for false positives.
3 · Prod fullpassive_mode=False, fail_secure=True, enforce_https=TrueFull 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

SymptomLikely causeFix
App returns 500 on healthy requests after deployfail_secure=True with Redis or detector unreachableFix 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 blockedStill in passive_mode=TrueFlip 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 LBSet trust_x_forwarded_proto=True. Backend will trust the LB's header and skip the redirect.