Implementing Nginx Rate Limiting Based on X-Forwarded-For Header for Load Balanced Applications


1 views

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