Skip to content

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

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

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

Conversation

@rennf93

@rennf93 rennf93 commented Oct 9, 2026 •

Copy link
Copy Markdown

Closes #1360

Implementation for the discussion in #1360.

Summary

Opt-in fastapi-guard middleware wired into both app factories (create_fastapi_app and the gradio web-interface path). 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, so misconfiguration is loud instead of silent.

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; happy to write an RFC first if you'd rather)
  • 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)
  • IP block/allow lists, CIDR supported: OPENENV_GUARD_BLOCKED_IPS / _ALLOWED_IPS
  • user-agent blocking: OPENENV_GUARD_BLOCKED_USER_AGENTS
  • penetration-attempt detection, on whenever the middleware is on
  • passive/log-only mode (OPENENV_GUARD_PASSIVE_MODE), 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 (private ranges by default, matching container and Space deployments)
  • Redis for limits and bans shared across workers: OPENENV_GUARD_REDIS_URL, in-memory otherwise. I explicitly disabled the library's localhost default; 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. That's fastapi-guard's enforced_on_excluded_paths design and I think it's the right call: excluded shouldn't mean free pass.

Test Plan

  • uv run --extra security pytest tests/core/test_guard_security.py: 5 tests, all green (default-off no-op, middleware wiring, blocked IP 403, IP lists on excluded paths, rate-limit 429). Rate-limit state in the library is process-wide, so each test drives its own TEST-NET client IP to stay hermetic.
  • test_task_api, test_state_schema_subclass and test_reset_observation_schema pass. One adjacent test (test_reference_echo_explicitly_advertises_its_base_reset_observation) fails on main with this PR's changes stashed too, so it's pre-existing and unrelated.
  • usort + ruff format + ruff check clean on src/ and tests/. Repo-wide .claude/hooks/lint.sh also flags ~58 files under envs/ that already fail on main; I left those alone.

Claude Code Review

N/A

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.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/openenv/core/env_server/web_interface.py Outdated
Comment thread pyproject.toml
# penetration detection) for env servers. No effect unless
# OPENENV_GUARD_ENABLED=1.
"fastapi-guard>=8.0.3,<9.0.0",
]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redis extra dependency missing

Medium Severity

Setting OPENENV_GUARD_REDIS_URL turns on enable_redis, but the security extra only installs fastapi-guard. Redis support lives in guard-core[redis], and enabling Redis without that extra raises a SecurityConfig validation error at startup. The documented shared-worker Redis path cannot work from pip install "openenv[security]" alone.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9e3c9db. Configure here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not actually needed: redis is a core dependency of guard-core, which fastapi-guard depends on - I verified against the installed packages: guard-core's base metadata lists redis (the guard-core[redis] extra is a no-op alias for the same package), and pip install fastapi-guard pulls redis 8.1.0 transitively. SecurityConfig(enable_redis=True, redis_url=...) constructs cleanly from the plain security extra. The documented Redis path works exactly as written.

…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).

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

kwargs["block_cloud_providers"] = frozenset(cloud_providers)

if ipinfo_token := os.environ.get("IPINFO_TOKEN"):
kwargs["ipinfo_token"] = ipinfo_token

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Geo rules skip required handler

Medium Severity

Country and cloud-provider lists are applied without constructing geo_ip_handler as IPInfoManager. fastapi-guard skips those checks when the handler is missing, so OPENENV_GUARD_BLOCKED_COUNTRIES, OPENENV_GUARD_ALLOWED_COUNTRIES, and OPENENV_GUARD_BLOCK_CLOUD_PROVIDERS can take no effect even when IPINFO_TOKEN is set via the deprecated ipinfo_token field.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c5ff4c7. Configure here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid - fixed. trust_x_forwarded_proto now defaults to following enforce_https (exactly the case you describe: a TLS-terminating proxy makes every request look like plain HTTP otherwise), with an explicit OPENENV_GUARD_TRUST_X_FORWARDED_PROTO override.

Comment thread src/openenv/core/env_server/security.py
…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

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

enabled = os.environ.get("OPENENV_GUARD_ENABLED", "").strip().lower()
if enabled not in ("1", "true", "yes"):
return
if getattr(app.state, "guard_attached", False):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enable flag ignores on value

Low Severity

attach_guard still treats only 1/true/yes as enabled, while _env_bool now accepts on for every other OPENENV_GUARD_* flag. OPENENV_GUARD_ENABLED=on therefore leaves the middleware off with no error, so a consistent boolean spelling fails to enable protection.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5b767da. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 4 total unresolved issues (including 3 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 9278c59. Configure here.

) from exc

app.add_middleware(SecurityMiddleware, config=_build_security_config())
app.state.guard_attached = True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WebSocket routes bypass guard protections

High Severity

attach_guard only installs SecurityMiddleware, which fastapi-guard documents as HTTP-only. Env-server traffic on /ws, /mcp, /harness, and /ws/ui never hits IP lists, rate limits, bans, or detection. Those checks require Depends(guard_websocket) on each WebSocket route, which is not applied, so a blocked or limited client can still drive the primary Gym/MCP/harness APIs.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9278c59. Configure here.

rennf93 and others added 3 commits October 11, 2026 07:10
- 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)

1 participant