FastAPI Proxy Headers
When deploying FastAPI applications in production environments, they often run behind proxies, load balancers, or reverse proxies like Nginx, Traefik, or AWS Application Load Balancer. In these setups, understanding how to properly handle proxy headers becomes crucial for security and functionality.
Introduction to Proxy Headers
In a typical deployment architecture, client requests pass through one or more intermediary servers (proxies) before reaching your FastAPI application:
Client → Proxy/Load Balancer → FastAPI Application
When this happens, your FastAPI application sees the proxy's IP address as the client IP instead of the actual client's IP address. Similarly, information about the original protocol (HTTP vs HTTPS) can be lost.
Proxies solve this by adding special headers to the forwarded request:
X-Forwarded-For
: Contains the original client IP addressX-Forwarded-Proto
: Indicates the protocol of the original request (HTTP/HTTPS)X-Forwarded-Host
: The original host requested by the clientForwarded
: A standardized header that can contain all of the above information
Why Proxy Headers Matter
Handling proxy headers correctly is important for several reasons:
- Security: Accurate client identification for rate limiting and security policies
- Logging: Proper tracking of client origins in logs
- URL Generation: Creating correct absolute URLs in responses
- SSL Enforcement: Redirecting users to HTTPS when needed
Configuring FastAPI for Proxy Headers
FastAPI provides a built-in way to handle proxy headers through the trusted_hosts
parameter in its configuration.
Basic Setup
Here's how to configure a FastAPI application to trust proxy headers:
from fastapi import FastAPI
from fastapi.middleware.trustedhost import TrustedHostMiddleware
app = FastAPI()
# Add middleware to trust host headers
app.add_middleware(
TrustedHostMiddleware, allowed_hosts=["example.com", "*.example.com"]
)
@app.get("/")
async def root():
return {"message": "Hello World"}
This configuration ensures that requests are only accepted from the specified hosts, helping prevent HTTP Host header attacks.
Working with Client IP Addresses
To access the client's true IP address in your FastAPI handlers, you'll need to look at the appropriate headers. Here's how:
from fastapi import FastAPI, Request
app = FastAPI()
@app.get("/client-info")
async def get_client_info(request: Request):
# Get client IP considering X-Forwarded-For header
client_host = request.client.host
# Get headers to see all forwarded information
forwarded_for = request.headers.get("x-forwarded-for")
forwarded_proto = request.headers.get("x-forwarded-proto")
return {
"client_host": client_host,
"forwarded_for": forwarded_for,
"forwarded_proto": forwarded_proto,
"is_secure": request.url.scheme == "https"
}
When running behind a proxy, this endpoint would show the forwarded information rather than the proxy's details.
Using Starlette's ProxyHeadersMiddleware
FastAPI is built on top of Starlette, which provides a helpful middleware for handling proxy headers. This middleware automatically adjusts the client.host
and url.scheme
based on the proxy headers:
from fastapi import FastAPI, Request
from starlette.middleware.proxy_headers import ProxyHeadersMiddleware
app = FastAPI()
# Add the ProxyHeadersMiddleware to trust forwarded headers
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts=["*"])
@app.get("/client-address")
async def get_client_address(request: Request):
return {
"client_host": request.client.host,
"scheme": request.url.scheme,
"url": str(request.url)
}
With this middleware, request.client.host
will correctly show the client's IP from the X-Forwarded-For
header, and request.url.scheme
will properly reflect whether the original request was HTTP or HTTPS.
Setting trusted_hosts=["*"]
trusts all proxy headers from any source. In production, you should restrict this to only your actual proxy servers' IP addresses to prevent IP spoofing attacks.
Real-World Example: Rate Limiting Based on Client IP
A practical application of correctly handling proxy headers is implementing rate limiting based on the actual client IP, not the proxy's IP:
from fastapi import FastAPI, Request, Response, Depends
from starlette.middleware.proxy_headers import ProxyHeadersMiddleware
import time
from typing import Dict, List, Tuple
app = FastAPI()
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts=["10.0.0.1", "proxy.internal"])
# Simple in-memory rate limiting store
# In production, use Redis or another distributed store
rate_limit_store: Dict[str, List[Tuple[float, int]]] = {}
RATE_LIMIT = 5 # requests
TIME_WINDOW = 10 # seconds
async def check_rate_limit(request: Request, response: Response):
client_ip = request.client.host
now = time.time()
# Initialize or clean old requests
if client_ip not in rate_limit_store:
rate_limit_store[client_ip] = []
else:
# Remove entries older than the time window
rate_limit_store[client_ip] = [
(timestamp, count) for timestamp, count in rate_limit_store[client_ip]
if now - timestamp < TIME_WINDOW
]
# Count recent requests
recent_requests = sum(count for _, count in rate_limit_store[client_ip])
# Update headers with rate limit info
response.headers["X-Rate-Limit"] = str(RATE_LIMIT)
response.headers["X-Rate-Limit-Remaining"] = str(max(0, RATE_LIMIT - recent_requests))
# If there are recent requests, add to them, otherwise create new entry
if rate_limit_store[client_ip] and rate_limit_store[client_ip][-1][0] == now:
rate_limit_store[client_ip][-1] = (now, rate_limit_store[client_ip][-1][1] + 1)
else:
rate_limit_store[client_ip].append((now, 1))
# Check if rate limit exceeded
if recent_requests >= RATE_LIMIT:
return {"error": "Rate limit exceeded", "retry_after": TIME_WINDOW}
return None
@app.get("/api/resource")
async def protected_resource(rate_limit_check=Depends(check_rate_limit)):
if rate_limit_check:
return rate_limit_check
return {"data": "This is protected resource data"}
This implementation:
- Correctly identifies the true client IP by using
request.client.host
after theProxyHeadersMiddleware
processes the proxy headers - Applies rate limiting based on the actual client IP, not the proxy's IP
- Returns rate limit information in the response headers
HTTPS Detection with Proxy Headers
Another common scenario is detecting whether the original client request was made over HTTPS, which is important for security-related features:
from fastapi import FastAPI, Request
app = FastAPI()
@app.get("/check-https")
async def check_https(request: Request):
# With ProxyHeadersMiddleware, this will correctly reflect the original scheme
is_https = request.url.scheme == "https"
# Manual check of the X-Forwarded-Proto header
forwarded_proto = request.headers.get("x-forwarded-proto")
manually_detected_https = forwarded_proto == "https" if forwarded_proto else None
return {
"is_https": is_https,
"scheme": request.url.scheme,
"manually_detected_https": manually_detected_https,
"forwarded_proto": forwarded_proto
}
Configuring with Uvicorn
When running FastAPI applications with Uvicorn, you can also configure proxy header handling at the server level:
import uvicorn
from fastapi import FastAPI
app = FastAPI()
if __name__ == "__main__":
uvicorn.run(
"main:app",
host="0.0.0.0",
port=8000,
forwarded_allow_ips=["10.0.0.1", "proxy.internal"],
proxy_headers=True
)
The forwarded_allow_ips
parameter specifies which IPs are allowed to set proxy headers, and proxy_headers=True
enables the processing of these headers. This achieves the same effect as using the ProxyHeadersMiddleware
.
Security Considerations
When working with proxy headers, keep these security considerations in mind:
- Trust only your proxies: Configure
trusted_hosts
orforwarded_allow_ips
to only include your actual proxy servers' IP addresses. - Validate headers: Don't blindly trust proxy headers without verification.
- Consider using standard headers: The newer
Forwarded
header is more secure than the olderX-Forwarded-*
headers. - Layer your security: Don't rely solely on header inspection for security measures.
Example: Full Configuration with Nginx
Here's a complete example showing both the Nginx configuration and FastAPI application:
Nginx configuration:
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://localhost:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
FastAPI application:
from fastapi import FastAPI, Request
from starlette.middleware.proxy_headers import ProxyHeadersMiddleware
app = FastAPI()
# Trust headers from our Nginx proxy
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts=["127.0.0.1"])
@app.get("/")
async def root(request: Request):
return {
"client_ip": request.client.host, # Will show the real client IP
"scheme": request.url.scheme, # Will show http or https correctly
"url": str(request.url) # Will construct URLs with correct scheme
}
# Run with: uvicorn app:app --host 0.0.0.0 --port 8000
Summary
Properly handling proxy headers in FastAPI applications is essential when deploying behind proxies or load balancers. By using the appropriate middleware and understanding how these headers work, you can:
- Correctly identify client IP addresses
- Detect the original request protocol (HTTP vs HTTPS)
- Implement security measures like rate limiting based on actual client information
- Generate correct URLs in your responses
The key components to remember are:
- The
ProxyHeadersMiddleware
from Starlette - Configuring trusted proxy sources
- Accessing client information through the
request
object - Being cautious about which proxy headers you trust
Additional Resources and Exercises
Further Reading
- FastAPI official documentation on HTTPS and proxies
- Starlette's ProxyHeadersMiddleware documentation
- Mozilla Web Security Guidelines on proxy headers
- RFC 7239: Forwarded HTTP Extension
Exercises
-
Basic Proxy Setup: Configure FastAPI with ProxyHeadersMiddleware and create an endpoint that displays all forwarded headers.
-
Rate Limiter: Expand the rate limiter example to use Redis for distributed rate limiting in a multi-server environment.
-
Security Exercise: Create middleware that validates that the
X-Forwarded-For
header follows the expected pattern and rejects suspicious requests. -
HTTPS Redirector: Build middleware that redirects all HTTP requests to HTTPS by checking the
X-Forwarded-Proto
header. -
Logging Enhancement: Create custom logging middleware that includes the original client IP from proxy headers in all log entries.
By mastering proxy headers in FastAPI, you'll be well-prepared to deploy your applications in professional, production environments with enhanced security and proper client identification.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)