Skip to content

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-headers on the CLI, or proxy_headers=False in uvicorn.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

  1. When a request arrives, FastAPI Guard checks if it's from a trusted proxy
  2. If not from a trusted proxy, the direct connecting IP is always used
  3. If from a trusted proxy, the X-Forwarded-For header is parsed to extract the original client IP
  4. 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

  1. Be specific: Only include the exact IPs or ranges of your known proxies
  2. Use correct depth: Configure based on your actual proxy chain
  3. Regular audits: Periodically review your trusted proxy list
  4. Test configuration: Verify correct IP extraction in your environment