Skip to content

Add optional fastapi-guard security middleware for env servers (default off) - #1506

Open
rennf93 wants to merge 16 commits into
openenv:mainfrom
rennf93:feat/fastapi-guard-security-middleware
Open

rennf93 wants to merge 16 commits into
openenv:mainfrom
rennf93:feat/fastapi-guard-security-middleware

Conversation

@rennf93

@rennf93 rennf93 commented Oct 27, 2026

Copy link
Copy Markdown

Closes #1360

Up to date version of #1361, which closed when #1492 merged (its body says "resolve #1361", a closing keyword). Same branch, merged with main at 693777b, @burtenshaw's lifespan commits (67e82bd, ea5bbc9) included.

Summary

Opt-in fastapi-guard middleware wired into both app factories (create_fastapi_app and the gradio web-interface path), covering HTTP requests and WebSocket handshakes. Default off: with OPENENV_GUARD_ENABLED unset, attach_guard() is a no-op and env servers run exactly as before. Enabling without the extra installed raises a clear ImportError with the install command.

Passive mode and Redis outages (the open thread on #1361)

Decision: passive mode lets infrastructure failures through too, not only policy violations, matching upstream's "passive mode never blocks". Passive mode now sets fastapi-guard's redis_fail_open, and the WebSocket startup gate follows the same passive branch as every other refusal.

  1. Passive mode, Redis unreachable at startup or after it: HTTP requests and WebSocket handshakes are logged and let through. Before this commit, HTTP answered 503 at startup and 500 after it, and a WebSocket was closed with 1013 at startup.
  2. Active mode is unchanged: WebSocket handshakes are closed with 1013 (HTTP 403 on the upgrade), HTTP answers 503 with Retry-After while the guard can't initialize and 500 after a successful start. That 500 is fastapi-guard's fail_secure answer on 8.x; I'm changing it to 503 with Retry-After in fastapi-guard, OpenEnv needs no change for it.
  3. Non-Redis detection errors keep their current behavior in both modes: no blanket close on 1013.
  4. The module docstring states both modes.

Type of Change

  • New feature
  • Bug fix
  • Breaking change
  • Documentation
  • New environment
  • Refactoring

Alignment Checklist

  • I have read .claude/docs/PRINCIPLES.md and this PR aligns with our principles
  • I have checked .claude/docs/INVARIANTS.md and no invariants are violated (no API signature or wire-type change, env-var only config, nothing secret logged)
  • I have run /pre-submit-pr (or bash .claude/hooks/lint.sh and tests) and addressed all issues

RFC Status

  • Not required (additive, default-off feature)
  • RFC exists: #___
  • RFC needed (will create before merge)

What you get when enabled

pip install "openenv[security]" plus OPENENV_GUARD_ENABLED=1:

  • rate limiting with auto-ban: OPENENV_GUARD_RATE_LIMIT / _RATE_LIMIT_WINDOW (defaults 100 req / 60s), _AUTO_BAN_THRESHOLD / _AUTO_BAN_DURATION (defaults 10 / 300s); repeated 429s feed the ban (OPENENV_GUARD_RATE_LIMIT_AUTO_BAN=0 keeps 429-only)
  • IP block/allow lists, CIDR supported: OPENENV_GUARD_BLOCKED_IPS / _ALLOWED_IPS
  • user-agent blocking: OPENENV_GUARD_BLOCKED_USER_AGENTS
  • penetration-attempt detection on path, query and headers; request bodies are model input, so body scanning is opt-in (OPENENV_GUARD_SCAN_BODY)
  • WebSocket handshakes on /ws, /mcp, /harness and /ws/ui go through fastapi-guard's guard_websocket: ban check, IP and country lists, a rate-limit bucket separate from HTTP, and penetration detection on the handshake. A refused handshake is closed before accept(), so the client gets HTTP 403 on the upgrade and no socket. Messages on an accepted socket are not screened, and a ban issued mid-session applies from the next handshake. With Redis configured, the guard initializes at startup (the app lifespan runs inside fastapi-guard's guard_lifespan), and while Redis is unreachable handshakes are closed with 1013 in active mode
  • passive/log-only mode (OPENENV_GUARD_PASSIVE_MODE), see above; optional security headers (OPENENV_GUARD_SECURITY_HEADERS), enforce-HTTPS (OPENENV_GUARD_ENFORCE_HTTPS), custom log file/format, and blocked/allowed countries plus cloud-provider blocking (countries require IPINFO_TOKEN)
  • trusted proxies for client IP resolution: OPENENV_GUARD_TRUSTED_PROXIES / _TRUSTED_PROXY_DEPTH. No proxy is trusted by default, so X-Forwarded-For is ignored and the client is the TCP peer; behind a proxy, list it or every client shares the proxy's IP. X-Forwarded-Proto is only honored from a trusted proxy, so enforce-HTTPS behind a TLS-terminating proxy needs it listed too
  • Redis for limits and bans shared across workers: OPENENV_GUARD_REDIS_URL, in-memory otherwise. The library's localhost default is disabled; an opt-in feature shouldn't require a running Redis
  • /health, /schema, /metadata and the docs paths are excluded from detection checks so orchestrators can keep probing, but IP lists and rate limiting still apply on them (fastapi-guard's enforced_on_excluded_paths)

Test Plan

  • pytest tests/core/test_guard_security.py: 23 passed on fastapi-guard 8.0.3 / guard-core 4.3.2 (the versions in uv.lock) and on 8.1.2 / 4.4.0 (latest on PyPI). The new test_passive_mode_admits_while_redis_unreachable fails on the previous head (the handshake is closed, WebSocketDisconnect).

  • Live check after startup, uvicorn 0.54.0, websockets client 15.0.1, real Redis behind a TCP proxy that I close (outage) and reopen (recovery), /health and a /ws reset at each step:

    mode Redis up Redis down Redis back
    active 200, observation 500, refused (403) 200, observation
    passive, previous head 200, observation 500, observation 200, observation
    passive, this PR 200, observation 200, observation 200, observation

    Same rows on 8.0.3 / 4.3.2 and on 8.1.2 / 4.4.0.

  • usort, ruff format and ruff check clean on the changed files. On the whole tree, ruff format flags src/openenv/core/harness/README.md and usort flags tests/envs/test_grid_world.py and tests/envs/test_julia_env.py, all from main; this PR doesn't touch them.

  • I didn't rerun the full suite locally for this round: the merge only brings main, and the new commit touches security.py and its tests. fix(qed): keep spawned verifier imports lightweight #1491 and test(qed): cover worker import isolation and busy-worker recovery #1492, which fix the test (3.11) qed timeout that was red on Add optional fastapi-guard security middleware for env servers (default off) #1361, are in the merge.

Claude Code Review

N/A

rennf93 and others added 16 commits October 9, 2026 10:41
Off by default (OPENENV_GUARD_ENABLED=1 to enable): IP block/allow
lists, rate limiting with auto-ban, user-agent blocking, and
penetration detection, wired into both create_fastapi_app and the
gradio web-interface path. Ships as a 'security' extra; in-memory
state by default, Redis via OPENENV_GUARD_REDIS_URL.
…loud blocking, and log options

Brings the OPENENV_GUARD_* surface to parity: passive/log-only mode,
optional security headers, enforce-HTTPS, blocked/allowed countries and
cloud-provider blocking, custom log file/format, and IPINFO_TOKEN geo
lookups. Country rules require IPINFO_TOKEN (guard-core resolves a geo
handler from it).
…enforcement, strict booleans, rate-limit auto-ban knob

- web_interface no longer attaches guard twice (create_fastapi_app
  already covers it) and attach_guard is idempotent via app.state
- enforce_https now sets trust_x_forwarded_proto by default so
  TLS-terminating proxies are read correctly (explicit override via
  OPENENV_GUARD_TRUST_X_FORWARDED_PROTO)
- invalid boolean env values now fail loudly instead of silently
  disabling security
- new OPENENV_GUARD_RATE_LIMIT_AUTO_BAN knob
- Trusted proxies default to empty: trusting RFC1918 ranges let any LAN
  peer spoof X-Forwarded-For past the blocklist and rate limits. Behind
  a reverse proxy, list it in OPENENV_GUARD_TRUSTED_PROXIES.
- Penetration-detection body scanning is now opt-in via
  OPENENV_GUARD_SCAN_BODY: request bodies are model input (observations,
  prompts, completions), not commands, so signature scans false-positived
  on ordinary content and rejected legitimate requests with 400. URL,
  query, and header screening stay on.
- Rate-limit auto-ban defaults on; OPENENV_GUARD_RATE_LIMIT_AUTO_BAN=0
  keeps 429-only enforcement. _env_bool gained a keyword-only default.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Opt-in request security for env servers (rate limiting, IP lists, payload screening)

2 participants