How to Configure Nginx set_real_ip_from with Dynamic AWS ELB IP Addresses for Client IP Preservation


1 views

When running Nginx behind AWS Elastic Load Balancer, we face a fundamental challenge: ELB IP addresses are dynamic and change over time, while Nginx's set_real_ip_from directive requires static IP addresses. This creates a maintenance nightmare for preserving original client IP addresses in your logs and applications.

The core issue stems from these technical realities:

  • ELB operates with a pool of IP addresses that may change without notice
  • HttpRealIpModule requires explicit trust of proxy IP addresses
  • AWS recommends against using static IP records for ELBs
  • X-Forwarded-For headers contain the original client IP

Here are three approaches to solve this problem:

1. Using AWS IP Ranges JSON

AWS publishes all their IP ranges in a JSON file that can be programmatically processed:


# Example shell script to update nginx config
#!/bin/bash
curl -s https://ip-ranges.amazonaws.com/ip-ranges.json | \
jq -r '.prefixes[] | select(.service=="EC2") | .ip_prefix' | \
while read ip; do
  echo "set_real_ip_from $ip;" >> /etc/nginx/conf.d/elb_ips.conf
done
nginx -t && nginx -s reload

2. Configuration with GeoIP Module

Combine with GeoIP module for more robust filtering:


http {
    geo $trusted_elb {
        default 0;
        # AWS us-east-1
        3.80.0.0/12 1;
        3.208.0.0/12 1;
        # Add more ranges as needed
    }

    server {
        real_ip_header X-Forwarded-For;
        set_real_ip_from 0.0.0.0/0 if=$trusted_elb;
    }
}

3. Dynamic Configuration with Lua

For more advanced setups, use Lua to handle dynamic IPs:


http {
    lua_shared_dict elb_ips 1m;
    init_by_lua_block {
        local http = require "resty.http"
        local httpc = http.new()
        local res, err = httpc:request_uri("https://ip-ranges.amazonaws.com/ip-ranges.json")
        
        if not res then
            ngx.log(ngx.ERR, "failed to fetch AWS IP ranges: ", err)
            return
        end
        
        local cjson = require "cjson"
        local data = cjson.decode(res.body)
        
        for _, prefix in ipairs(data.prefixes) do
            if prefix.service == "EC2" then
                ngx.shared.elb_ips:set(prefix.ip_prefix, true)
            end
        end
    }

    server {
        access_by_lua_block {
            local ip = ngx.var.remote_addr
            if ngx.shared.elb_ips:get(ip) then
                ngx.var.real_ip_header = "X-Forwarded-For"
            end
        }
    }
}

Whichever solution you choose, remember to:

  • Set up regular cron jobs to update IP ranges
  • Monitor for configuration changes in AWS
  • Test your configuration after updates
  • Consider using Terraform or CloudFormation to manage this

Each approach has different performance characteristics:

Method Memory Usage CPU Overhead Maintenance Complexity
Static Config Low None High
GeoIP Module Medium Low Medium
Lua Dynamic Medium-High Medium Low

When using Nginx behind AWS Elastic Load Balancer (ELB), the HttpRealIpModule becomes essential for preserving original client IPs. However, ELB's fundamental architecture introduces a significant challenge:

# Typical nginx real_ip configuration that WON'T work with ELB:
set_real_ip_from 192.0.2.1;
real_ip_header X-Forwarded-For;

Amazon explicitly warns against hardcoding ELB IPs because they can change during scaling events or maintenance. Here's why this breaks traditional approaches:

  • ELB IP ranges can change without notice
  • Multiple IPs may be active simultaneously
  • Different regions have different IP pools

Option 1: Using AWS IP Ranges

The most reliable method is to reference AWS's published IP ranges:

# Download and update the AWS IP ranges regularly
set_real_ip_from 3.80.0.0/12;
set_real_ip_from 3.208.0.0/12;
set_real_ip_from 15.0.0.0/8;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

You should automate the update process with a cron job:

#!/bin/bash
# Update AWS IP ranges weekly
curl -s https://ip-ranges.amazonaws.com/ip-ranges.json | \
jq -r '.prefixes[] | select(.service=="EC2") | .ip_prefix' > /etc/nginx/aws-ips.conf
nginx -t && nginx -s reload

Option 2: Cloudflare-Style Trusted Proxies

For more dynamic environments, implement a pattern similar to Cloudflare's:

# /etc/nginx/conf.d/real-ip.conf
geo $realip_trusted {
    default 0;
    include aws-ips.conf;
    1;
}

server {
    # ...
    set_real_ip_from $realip_trusted;
    real_ip_header X-Forwarded-For;
    real_ip_recursive on;
}

When implementing this in production environments:

  • Always test with nginx -t before reloading
  • Monitor /var/log/nginx/error.log for Real IP module errors
  • Consider using Terraform or CloudFormation to manage configurations

For Terraform users, here's a snippet to maintain the configuration:

resource "aws_s3_bucket_object" "nginx_config" {
  bucket = "your-config-bucket"
  key    = "nginx/real_ip.conf"
  content = templatefile("${path.module}/templates/real_ip.conf.tpl", {
    aws_cidrs = jsondecode(data.http.aws_ip_ranges.body).prefixes[*].ip_prefix
  })
}

After implementation, verify with:

curl -H "X-Forwarded-For: 1.2.3.4, 10.0.0.1" http://your-domain.com

# Check Nginx logs for:
log_format main '$remote_addr - $remote_user [$time_local] "$request"';

The logs should show the original client IP (1.2.3.4) rather than the ELB IP.