How to Preserve Original $remote_addr While Using Nginx Real-IP Module with AWS ELB


2 views

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