Skip to content

feat(api): add optional fastapi-guard security middleware - #5664

Merged
qinxuye merged 4 commits into
xorbitsai:mainfrom
rennf93:feat/fastapi-guard-security-middleware
Oct 11, 2026
Merged

qinxuye merged 4 commits into
xorbitsai:mainfrom
rennf93:feat/fastapi-guard-security-middleware

Conversation

@rennf93

@rennf93 rennf93 commented Oct 10, 2026

Copy link
Copy Markdown
Contributor

Closes #5663

Summary

Opt-in fastapi-guard security middleware wired into RESTfulAPI.serve() right after the audited_ip_middleware block, so the existing XINFERENCE_ALLOWED_IPS allowlist still runs first and the audit middleware still wraps everything. Default off: XINFERENCE_GUARD_ENABLED unset means no imports, no middleware, zero overhead. Enabled-but-not-installed raises a clear error naming the extra install command (same shape as the OTEL block's error handling).

What Changes

  • New xinference/api/guard_integration.py: env-driven fastapi-guard wiring (attach_guard(app), _build_guard_config()). Config is read at call time for the same fork-safety reason is_auth_advanced() reads at call time
  • xinference/api/restful_api.py: one attach_guard(self._app) call in serve() between the IP-allowlist middleware and the OTEL setup
  • New guard extra in pyproject.toml (fastapi-guard>=8.1.0,<9.0.0), following the otel extra's lazy-import pattern; the core dependency set doesn't grow
  • When enabled: IP block/allow lists (CIDR), general per-request rate limiting with auto-ban, user-agent blocking, penetration-attempt detection, passive/log-only mode, IPInfo geo/cloud-provider blocking (XINFERENCE_GUARD_IPINFO_TOKEN), Redis-backed shared counters and bans (XINFERENCE_GUARD_REDIS_URL - relevant for multi-instance deployments behind xinference-router; the single supervisor process matches the existing auth rate limiter's per-process design)
  • Explicit enable_redis=False unless a URL is configured: an opt-in feature must not require a running Redis

Test Plan

  • New xinference/api/tests/test_guard_integration.py: disabled-by-default no-op, fail-loud without the package, blocked IP 403, rate-limit 200/200/429, passive mode never blocks, Redis-off default. Env state is snapshotted/restored per test; each scenario uses its own TEST-NET client IP because guard's rate-limit state is process-wide. Runs without services (--noconftest locally since the root conftest spawns a cluster; in CI the standard job env covers it)
  • Module behavior validated against fastapi-guard 8.1.0 (blocked 403, 200/200/429, passive 200, defaults sanity)
  • black 25.1.0, ruff 0.15.22 (line-length 100), isort (profile=black), mypy: all clean on the touched files

Off by default (XINFERENCE_GUARD_ENABLED=1 to enable): IP block/allow
lists, general rate limiting with auto-ban, user-agent blocking,
penetration detection, passive mode, and optional Redis-backed shared
state. Ships as a 'guard' extra; nothing is imported or attached when
disabled. Complements the XINFERENCE_ALLOWED_IPS allowlist (which stays
first in line) and the auth rate limiter (which stays auth-scoped).
@XprobeBot XprobeBot added this to the v3.x milestone Oct 10, 2026

@qinxuye qinxuye 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.

Reviewed current head 2961609d724840f8dbabb6d2a37a110da15163c4. The optional/lazy integration is useful, but the four inline issues need addressing before approval: client-IP spoofing bypasses the new controls; default body scanning rejects legitimate inference input; guard rejections bypass the existing audit/IP wrapper; and the default response policy disables Web UI microphone recording.

Validation: with fastapi-guard==8.1.0 and its allowed guard-core==4.3.2, the focused guard/audit/streaming/metrics tests produced 36 passed, 1 skipped. I additionally exercised the middleware assembled by RESTfulAPI.serve() through Uvicorn's loaded ASGI application, which reproduces the failures below; the existing bare-app guard tests do not cover that integration. Basic streaming still worked in this probe. Please add the regression cases described in each thread so these behaviors are covered together.

Also, the repository's pinned Black 25.1.0 reports that both new Python files would be reformatted; please run the configured formatting hooks. This is separate from the four behavioral findings.

Current GitHub checks show the two ReadTheDocs builds passing, but no Python CI results are visible for this head. The PR is mergeable; this review is a COMMENT, not an approval.

Comment thread xinference/api/guard_integration.py Outdated
Comment thread xinference/api/guard_integration.py
Comment thread xinference/api/restful_api.py Outdated
Comment thread xinference/api/guard_integration.py
… with review

Review-response pass (qinxuye's four findings):

- Client identity: serve() launched Uvicorn with forwarded_allow_ips="*",
  so Uvicorn rewrote scope["client"] from any peer's X-Forwarded-For
  before middleware ran and the guard's proxy trust was moot. When the
  guard is enabled, only proxies listed in XINFERENCE_GUARD_TRUSTED_PROXIES
  may rewrite the client (loopback-only otherwise); Uvicorn and guard read
  the same list, and guard's own trusted_proxies no longer falls back to
  RFC1918 ranges. Guard off keeps the historical behavior.
- Body scanning: penetration detection no longer reads or signature-scans
  prompt bodies by default (model input is data, not commands this API
  executes; SQL/HTML/shell chat text was rejected with 400). URL, query,
  and header screening stay on; XINFERENCE_GUARD_SCAN_BODY opts in.
- Ordering: attach_guard runs before the audited IP middleware registers,
  so guard nests inside it and its rejections keep audit recording and
  X-Request-ID, with the XINFERENCE_ALLOWED_IPS allowlist still first.
- Headers: the default Permissions-Policy now allows same-origin
  microphone/camera so the Web UI audio recorder keeps working
  (XINFERENCE_GUARD_PERMISSIONS_POLICY overrides the whole policy).

Regression tests through a real Uvicorn server thread: untrusted peers
cannot spoof or rotate X-Forwarded-For past blocklist/limiter, configured
proxies are honored, guard rejections stay audited, ordinary
code/security prompts pass with scanning off (and are blocked when opted
in), and the Permissions-Policy preserves microphone access.
@rennf93

rennf93 commented Oct 11, 2026

Copy link
Copy Markdown
Contributor Author

All four findings are addressed in 06b5268, each with a regression test (replies in-thread): Uvicorn/guard now share one explicitly trusted client-IP resolution (XINFERENCE_GUARD_TRUSTED_PROXIES, loopback-only by default when the guard is enabled, "*" behavior untouched when disabled), prompt bodies are no longer signature-scanned by default (XINFERENCE_GUARD_SCAN_BODY opts in), the guard nests inside the audited IP middleware (rejections keep audit and X-Request-ID), and the default Permissions-Policy preserves same-origin microphone/camera for the Web UI. Both new files are reformatted with the repo-pinned Black 25.1.0. Local run of the focused guard suite with fastapi-guard 8.1.0 / guard-core 4.3.2: 13 passed, 1 skipped.

@qinxuye qinxuye 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.

Re-reviewed 06b5268. The client-IP trust, inference-body scanning, and microphone-policy fixes are verified, and those three threads are resolved. The audit/request-ID part of the middleware-order fix also works. One part of the existing ordering finding remains:

[P2] Keep CORS outside guard-generated rejection responses

In RESTfulAPI.serve(), attach_guard() still runs after CORS registration (current code). The effective order is audit/IP -> SecurityMiddleware -> CORSMiddleware. Therefore guard-generated early responses never reach CORS.

I reproduced this with the actual serve() middleware assembly, XINFERENCE_GUARD_ENABLED=true, and Origin: https://client.example: an allowed request returns 200 with Access-Control-Allow-Origin, but an IP-blocked request returns 403 without it. With XINFERENCE_GUARD_RATE_LIMIT=1, the second request returns 429 with Retry-After: 60 but likewise lacks Access-Control-Allow-Origin. Both rejection responses are audited and carry X-Request-ID, so the audit fix is confirmed; the missing CORS handling is the remaining issue. Cross-origin browser clients cannot read these error responses and receive a CORS/network failure instead of the intended denial or rate-limit status.

Please register guard before the CORS middleware while keeping the existing audit/IP wrapper outside both. Add regressions using the production middleware assembly and an Origin header for guard-generated 403 and 429 responses: check CORS headers, audit recording, and request IDs, and retain the existing allowlist-first behavior. This is the unfinished CORS portion of the original ordering thread, which remains open.

Focused local validation: 44 passed, 1 skipped.

qinxuye's re-review reproduced that guard still registered after
CORSMiddleware, so guard-generated 403/429 responses left without
Access-Control-Allow-Origin and cross-origin browser clients got an
opaque network failure instead of the denial. attach_guard now runs
first in serve(), making the effective chain audit/IP -> CORS -> guard
-> app: guard rejections gain CORS headers, keep audit recording and
X-Request-ID, and the XINFERENCE_ALLOWED_IPS allowlist still rejects
before the guard sees the request.

Regressions drive the production assembly with an Origin header: a
guard 403 and a guard 429 both assert CORS headers plus X-Request-ID,
and the allowlist-first case asserts the existing IP check still denies
ahead of the guard.

@qinxuye qinxuye 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.

LGTM

@qinxuye
qinxuye merged commit 5af808b into xorbitsai:main Oct 11, 2026
19 checks passed
@rennf93

rennf93 commented Oct 11, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the two rounds of hands-on review - the Uvicorn-level probes caught real gaps that the unit suite couldn't see (proxy-header identity, CORS on guard rejections), and both rounds made the middleware better. The fixes shipped straight back into every other fastapi-guard integration we maintain. Pleasure working through it.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Opt-in request security middleware for the RESTful API (general rate limiting, IP blocklists, payload screening)

3 participants