When working with Nginx behind an AWS Elastic Load Balancer (ELB), many developers need to extract the original client IP while also maintaining visibility into intermediate hops. The standard Real-IP module configuration replaces $remote_addr
completely, making it impossible to track the ELB's IP address in logs.
In a typical AWS deployment:
[Client (1.2.3.4)] → [ELB (10.0.0.1)] → [Nginx (10.0.0.2)] → [App Server]
With standard Real-IP configuration, $remote_addr
changes from the ELB's IP (10.0.0.1) to the client's IP (1.2.3.4), losing the critical ELB information.
We can solve this by capturing the original $remote_addr
before Real-IP processing occurs:
# Capture original remote_addr before Real-IP module modifies it
set $original_remote_addr $remote_addr;
# Standard Real-IP configuration
real_ip_header X-Forwarded-For;
set_real_ip_from 10.0.0.0/8;
real_ip_recursive on;
Now you can include both IPs in your log format:
log_format extended '$remote_addr - $original_remote_addr [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
access_log /var/log/nginx/access.log extended;
For complex deployments with multiple proxies, consider:
map $http_x_forwarded_for $client_ip {
default "";
"~^(?P[^,]+)" $first_ip;
}
map $http_x_forwarded_for $proxy_chain {
default "";
"~.+$" "$http_x_forwarded_for";
}
Here's a complete server block example:
server {
listen 80;
server_name example.com;
set $original_remote_addr $remote_addr;
real_ip_header X-Forwarded-For;
set_real_ip_from 10.0.0.0/8;
real_ip_recursive on;
location / {
proxy_pass http://backend;
proxy_set_header X-Original-Remote-Addr $original_remote_addr;
proxy_set_header X-Real-IP $remote_addr;
}
}
Create a test endpoint to inspect headers:
location /ip-test {
return 200 "Client: $remote_addr\nELB: $original_remote_addr\n";
}
When implementing client IP tracking in multi-tier architectures using Nginx's Real-IP module, we face a fundamental tradeoff: while real_ip_header X-Forwarded-For
correctly identifies the originating client IP, it overwrites $remote_addr
- destroying information about intermediate hops like AWS ELBs. This creates debugging blind spots in production environments.
Since Nginx 1.13.0, we can access the pre-RealIP $remote_addr
through the $connection
variable:
map $remote_addr $original_remote_addr {
default $remote_addr;
"~^[0-9]" $connection_remote_addr;
}
server {
real_ip_header X-Forwarded-For;
set_real_ip_from 10.0.0.0/8;
real_ip_recursive on;
# Preserve both IPs in logs
log_format extended '$remote_addr - $original_remote_addr [$time_local] '
'"$request" $status $body_bytes_sent';
access_log /var/log/nginx/access.log extended;
}
For AWS environments specifically, here's a complete configuration that handles both IP preservation and security:
http {
# AWS ELB IP ranges (updated quarterly)
geo $elb_ip {
ranges;
default 0;
3.80.0.0/12 1;
35.168.0.0/13 1;
# Add current AWS IP ranges here
}
map $elb_ip $is_elb {
1 "elb";
default "direct";
}
server {
real_ip_header X-Forwarded-For;
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;
real_ip_recursive on;
# Custom log format captures full chain
log_format ip_chain '$remote_addr|$http_x_forwarded_for|'
'$connection_remote_addr|$is_elb';
location / {
proxy_set_header X-Original-Remote $connection_remote_addr;
proxy_pass http://backend;
}
}
}
For complex troubleshooting, consider these approaches:
- Create a debug endpoint that returns all IP-related headers and variables
- Implement geo-IP lookups for each address in the chain
- Use TCP dump when debugging edge cases
Remember that:
- The original
$remote_addr
should never be used for authentication - Always validate
X-Forwarded-For
against trusted proxies - Consider rate limiting based on the actual client IP