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.