When running a website behind a load balancer and Cloudflare, handling client IPs in Nginx can be tricky. Different setups use different headers to pass the real IP:
- Custom Domains via Cloudflare: Cloudflare sends the real client IP in the
CF-Connecting-IP
header. - Direct CNAME to Load Balancer: The load balancer sets the real client IP in the
X-Forwarded-For
header.
What is the challenge? Nginx’s real_ip_header
directive doesn’t support variables. This limitation means you can’t conditionally choose between headers like X-Forwarded-For
and CF-Connecting-IP
.
Understanging the problem
- Cloudflare Users: Their connections come first through Cloudflare, which sets the
CF-Connecting-IP
header/ - Non-Cloudflare Users: Their connections pass through the load balancer, which sets
X-Forwarded-For
. - Nginx’s limitation:
real_ip_header
accepts only one static value and does not support conditional evaluation.
For a detailed explanation of this limitation, check out this Medium post.
My Solution without extra modules
We can use Nginx’s map
directive to dynamically decide which header to trust. Here’s how:
1. Get real_ip from X-Forwarded-For
header
Since traffic anyways(even for CloudFlare users) goes through NLB, it sets X-Forwarded-For
header. If NLB receives traffic directly from the user it gets user IP, if from CloudFlare - CloudFlare IP. So the first step get real_ip from NLB.
set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
set_real_ip_from
is set to 10.0.0.0/8
block, because it’s a common practice to specify trusted proxies (like the NLB) to ensure only these proxies can influence the real_ip_header.
2. Use geo
directive to detectl CloudFlare IPs
Use geo
directive of ngx_http_geo_module
module to create variable with values depending on the client IP address. The geo
directive allows us to check the client’s IP address and assign a value to a variable ($use_x_cf_connecting_ip
), depending on whether it belongs to Cloudflare.
# Cloudflare
geo $use_x_cf_connecting_ip {
default 0;
173.245.48.0/20 1;
103.21.244.0/22 1;
103.22.200.0/22 1;
# ... and other CloudFlare IPs
}
Cloudflare regularly updates its IP ranges, please be sure to use actual Cloudflare IP ranges
3. Define a map
for Header Selection
The map
directive evaluates the value of $use_x_cf_connecting_ip
. If it’s 1
, meaning the IP matches Cloudflare’s range, the config uses the CF-Connecting-IP
header. Otherwise, it defaults to X-Forwarded-For
.
map $use_x_cf_connecting_ip $real_ip {
default $http_x_forwarded_for; # Fallback to X-Forwarded-For
"1" $http_cf_connecting_ip; # Use CF-Connecting-IP if present
}
4. Use the Calculated IP for Backend and Logs
- Pass the dynamically selected
$real_ip
to the backend. - Use it in Nginx logs for accurate tracking.
# Pass to backend
fastcgi_param REMOTE_ADDR $real_ip;
# Use in logs
log_format main '{"remote_ip": "$real_ip", "request": "$request"}';
Full Configuration Example
Here’s the full Nginx configuration with the solution implemented:
http {
set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
# Cloudflare
geo $use_x_cf_connecting_ip {
default 0;
173.245.48.0/20 1;
103.21.244.0/22 1;
103.22.200.0/22 1;
# ... and other CloudFlare IPs
}
map $use_x_cf_connecting_ip $real_ip {
default $http_x_forwarded_for;
"1" $http_cf_connecting_ip;
}
}
Closing Thoughts
If you’re running a hybrid setup like this, leveraging Nginx’s map
and geo
directives provides a clean and efficient way to handle client IPs. This approach ensures your logs, backend services, and application logic all receive accurate IP information.
Have you encountered similar challenges? Share your thoughts and solutions in the comments! Let’s keep learning and growing together. 🚀