Proxy Security¶
When your application is behind a proxy, load balancer, or CDN, properly handling the X-Forwarded-For header is critical for security. FastAPI Guard implements a secure approach to prevent IP spoofing attacks.
The Problem¶
The X-Forwarded-For header is sent by proxies to identify the original client IP, but since it can be manipulated by clients, it poses a security risk if trusted blindly.
Common security issues include:
- IP spoofing to bypass IP-based access controls
- False attribution in security logs
- Bypassing rate limiting and IP bans
Prerequisite: Your App Server Must Not Pre-Resolve the Client¶
FastAPI Guard reads the connecting IP from request.client.host, which is whatever your ASGI server put in scope["client"] — by the time a request reaches SecurityMiddleware, the server has already run. Several ASGI/WSGI servers rewrite that value themselves from X-Forwarded-For before any application code runs. uvicorn is the clearest example: it defaults to proxy_headers=True with forwarded_allow_ips="127.0.0.1", so a reverse proxy connecting from loopback (a same-host proxy_pass, the common case) has its X-Forwarded-For applied to scope["client"] upstream of FastAPI Guard.
When that happens, trusted_proxies unset does not mean "X-Forwarded-For is never trusted" — the server already trusted it, so an attacker's own forged header value becomes the connecting IP as far as rate limiting, IP bans, and every other check are concerned. guard-core detects the fingerprint of this condition (the connecting IP appearing inside its own X-Forwarded-For chain, which a real proxy never produces) and logs one warning naming the fix, but it cannot recover the true peer once the server has already overwritten it.
Fix: disable the server's own forwarded-header handling and let trusted_proxies be the single authority:
- uvicorn:
--no-proxy-headerson the CLI, orproxy_headers=Falseinuvicorn.run(...). - Gunicorn, Hypercorn, and other WSGI/ASGI servers have equivalent forwarded-header/proxy-trust settings — disable them the same way.
With the server's own handling off, its access log will show the proxy's address rather than the original client — that's expected, since X-Forwarded-For is no longer applied before the request reaches your application.
Secure Configuration¶
FastAPI Guard implements a secure-by-default approach where X-Forwarded-For headers are only trusted from explicitly configured trusted proxies:
config = SecurityConfig(
trusted_proxies=["10.0.0.1", "192.168.1.0/24"], # List of trusted proxy IPs/ranges
trusted_proxy_depth=1, # Number of proxies in the chain (default: 1)
trust_x_forwarded_proto=True, # Whether to trust X-Forwarded-Proto for HTTPS detection
)
How It Works¶
- When a request arrives, FastAPI Guard checks if it's from a trusted proxy
- If not from a trusted proxy, the direct connecting IP is always used
- If from a trusted proxy, the X-Forwarded-For header is parsed to extract the original client IP
- The extracted IP is then used for all security checks
Configuration Options¶
trusted_proxies¶
List of IP addresses or CIDR ranges that are allowed to set X-Forwarded-For headers:
config = SecurityConfig(
trusted_proxies=[
"10.0.0.1", # Single IP
"192.168.1.0/24", # CIDR range
"172.16.0.0/16" # Another CIDR range
]
)
If empty (default), X-Forwarded-For headers will not be trusted at all.
trusted_proxy_depth¶
Controls how the client IP is extracted from the X-Forwarded-For header:
config = SecurityConfig(
trusted_proxies=["10.0.0.1"],
trusted_proxy_depth=2 # Assumes two proxies in the chain
)
The X-Forwarded-For format is: client, proxy1, proxy2, ... (leftmost is the original client)
- With depth=1 (default): Assumes one proxy in chain, uses leftmost IP as client
- With depth=2: Assumes two proxies in chain, still uses leftmost IP
- Higher values handle more complex proxy chains
trust_x_forwarded_proto¶
Whether to trust the X-Forwarded-Proto header for HTTPS detection:
config = SecurityConfig(
trusted_proxies=["10.0.0.1"],
trust_x_forwarded_proto=True # Trust X-Forwarded-Proto from trusted proxies
)
This only applies when the request comes from a trusted proxy.
Real-World Examples¶
Single Reverse Proxy¶
config = SecurityConfig(
trusted_proxies=["10.0.0.1"], # Your Nginx/HAProxy IP
trusted_proxy_depth=1, # One proxy
trust_x_forwarded_proto=True # Trust HTTPS status from proxy
)
Load Balancer + Proxy¶
config = SecurityConfig(
trusted_proxies=[
"10.0.0.1", # Load balancer IP
"192.168.1.0/24" # Internal proxy subnet
],
trusted_proxy_depth=2, # Two proxies in chain
trust_x_forwarded_proto=True
)
Cloud Provider Load Balancer¶
config = SecurityConfig(
trusted_proxies=[
"10.0.0.0/8" # Cloud provider's internal IP range
],
trusted_proxy_depth=1,
trust_x_forwarded_proto=True
)
Best Practices¶
- Be specific: Only include the exact IPs or ranges of your known proxies
- Use correct depth: Configure based on your actual proxy chain
- Regular audits: Periodically review your trusted proxy list
- Test configuration: Verify correct IP extraction in your environment