When working with load-balanced environments, all incoming requests appear to originate from the load balancer's IP address. This creates challenges for rate limiting since Nginx's limit_req
module by default uses the connection's source IP for limiting.
The standard solution involves extracting the client's real IP from the X-Forwarded-For
header. Here's how to implement this in Nginx:
http {
# Define rate limiting zone
limit_req_zone $http_x_forwarded_for zone=client_zone:10m rate=10r/s;
server {
location /api/ {
# Apply rate limiting
limit_req zone=client_zone burst=20 nodelay;
# Your normal proxy settings
proxy_pass http://backend;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
In complex architectures with multiple proxies, X-Forwarded-For
may contain multiple IPs. Use this more robust solution:
map $http_x_forwarded_for $real_client_ip {
"" $remote_addr;
~^(?P[0-9.]+),?.*$ $firstAddr;
}
limit_req_zone $real_client_ip zone=client_zone:10m rate=10r/s;
Remember that headers can be spoofed. Combine this with:
- Trusted proxy IP whitelisting
- Request validation
- Logging of suspicious patterns
Verify your setup with:
# Test with different X-Forwarded-For values
curl -H "X-Forwarded-For: 1.2.3.4" http://yourserver/api/
ab -n 100 -c 10 -H "X-Forwarded-For: 1.2.3.4" http://yourserver/api/
When implementing rate limiting in Nginx behind a load balancer, all incoming requests appear to originate from the load balancer's IP address. This renders standard IP-based rate limiting ineffective since we need to track individual client IPs from the X-Forwarded-For
header.
The X-Forwarded-For
header typically contains a comma-separated list of IP addresses in the format:
X-Forwarded-For: client_ip, proxy1_ip, proxy2_ip
The left-most address represents the original client IP.
Here's a complete solution using ngx_http_geo_module
and ngx_http_limit_req_module
:
# Define a mapping of X-Forwarded-For to $limit variable
geo $limit {
default "";
127.0.0.1 "";
# Extract first IP from X-Forwarded-For header
~^(?P<client_ip>[0-9.]+) $client_ip;
}
# Create shared memory zone for rate limiting
limit_req_zone $limit zone=req_per_ip:10m rate=10r/s;
# Apply rate limiting in server block
server {
listen 80;
location / {
limit_req zone=req_per_ip burst=20 nodelay;
proxy_pass http://backend;
# Important security headers
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
For environments with multiple proxy layers, use this more robust regex pattern:
geo $limit {
default "";
~^([0-9.]+)(?:, [0-9.]+)*$ $1;
}
Use these cURL commands to verify behavior:
# Should pass (10 requests per second)
for i in {1..9}; do curl -H "X-Forwarded-For: 1.2.3.$i" http://yourserver; done
# Should trigger rate limiting
for i in {1..11}; do curl -H "X-Forwarded-For: 1.2.3.4" http://yourserver; done
- Always validate X-Forwarded-For content to prevent header injection
- Consider using $binary_remote_addr with trusted proxies list for better performance
- Monitor false positives from NAT gateways or corporate networks