Once your site sits behind Cloudflare, a CDN, a load balancer or any reverse proxy, your server sees the proxy’s IP address for every request, not the visitor’s. Logs fill with Cloudflare IPs, rate limits throttle Cloudflare itself, Fail2Ban bans the CDN, and your app records the wrong IPs. The fix: tell the web server which proxies to trust, and which header contains the real client IP.
How proxies pass the client IP
| Header | Set by |
|---|---|
CF-Connecting-IP |
Cloudflare (a single client IP) |
X-Forwarded-For |
Most proxies and load balancers: a list client, proxy1, proxy2 |
X-Real-IP |
Often set by Nginx when proxying to an app |
True-Client-IP |
Some CDNs (Cloudflare Enterprise, Akamai) |
| PROXY protocol | TCP-level proxies (HAProxy, AWS NLB). See HAProxy TCP mode |
Security rule: these headers can be forged by anyone who connects to your server directly. Only trust them when the request comes from a known proxy IP.
Nginx: the real_ip module
Nginx’s ngx_http_realip_module (included in standard packages) replaces $remote_addr with the client IP from a header, but only for requests from trusted addresses.
Cloudflare
Create /etc/nginx/conf.d/cloudflare-realip.conf:
# Cloudflare IP ranges: refresh from https://www.cloudflare.com/ips-v4 and /ips-v6
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2a06:98c0::/29;
set_real_ip_from 2c0f:f248::/32;
real_ip_header CF-Connecting-IP;
Generate it automatically, so it stays current:
{
for ip in $(curl -s https://www.cloudflare.com/ips-v4) $(curl -s https://www.cloudflare.com/ips-v6); do
echo "set_real_ip_from $ip;"
done
echo "real_ip_header CF-Connecting-IP;"
} | sudo tee /etc/nginx/conf.d/cloudflare-realip.conf > /dev/null
sudo nginx -t && sudo systemctl reload nginx
Run it weekly from cron or a timer. See systemd timers.
Your own load balancer or proxy
set_real_ip_from 10.0.0.5; # the load balancer's IP
real_ip_header X-Forwarded-For;
real_ip_recursive on;
real_ip_recursive on walks the X-Forwarded-For list from the right, skipping trusted proxies, and picks the first untrusted address. That’s the actual client, even through several proxy layers.
Apache: mod_remoteip
sudo a2enmod remoteip
# /etc/apache2/conf-available/remoteip.conf
RemoteIPHeader CF-Connecting-IP
RemoteIPTrustedProxy 173.245.48.0/20
RemoteIPTrustedProxy 103.21.244.0/22
# … all Cloudflare ranges (IPv4 and IPv6)
sudo a2enconf remoteip && sudo systemctl reload apache2
Update LogFormat to use %a (the client IP after remoteip) instead of %h.
Pass the IP on to your application
When Nginx proxies to an app (Node, Python, PHP-FPM), forward the corrected address:
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
Configure the framework to trust only the local proxy:
- Express:
app.set('trust proxy', 'loopback'); - Django: use
X-Real-IP, or a middleware that only trusts known proxies; - Laravel:
TrustProxiesmiddleware with your proxy IPs; - PHP-FPM:
$_SERVER['REMOTE_ADDR']is already corrected by Nginx’s realip module.
Verify
tail -f /var/log/nginx/access.log # should now show visitor IPs, not Cloudflare ranges
Visit the site from your phone on mobile data, and compare the logged IP with what ifconfig.me shows on the phone.
Things that depend on correct IPs
- Rate limiting (
limit_reqwith$binary_remote_addr). See Nginx rate limiting. - Fail2Ban: with real IPs in the logs it can identify attackers, but banning at your server’s firewall doesn’t stop traffic arriving via Cloudflare. Use Cloudflare’s firewall (Fail2Ban has a Cloudflare action) instead. See Fail2Ban for Nginx.
- Geo-IP, analytics and access lists (
allow/deny).
Lock the origin to the proxy
If clients can bypass Cloudflare and connect to your server directly, they can also forge CF-Connecting-IP (only if their own IP is trusted, which it isn’t with the config above), and they bypass Cloudflare’s WAF. Allow HTTP/HTTPS only from Cloudflare’s ranges, or use Cloudflare Tunnel. See Cloudflare WAF rules and Cloudflare Tunnel.
Quick reference
| Server | Settings |
|---|---|
| Nginx + Cloudflare | set_real_ip_from <CF ranges> + real_ip_header CF-Connecting-IP |
| Nginx + own proxy | set_real_ip_from <proxy> + real_ip_header X-Forwarded-For + real_ip_recursive on |
| Apache | mod_remoteip: RemoteIPHeader + RemoteIPTrustedProxy |
| Logs (Apache) | %a instead of %h |
Related: Cloudflare with a VPS and Nginx 403 errors.