When you put a site behind Cloudflare, Nginx sees Cloudflare's proxy IPs as the source of every request — not your visitor's actual address. Without fixing this, your access logs show Cloudflare instead of real users, your rate limits throttle Cloudflare's IPs instead of attackers, and any IP-based access control stops working. The ngx_http_realip_module restores the real IP from Cloudflare's headers.
Check if the module is available
Most Nginx packages include ngx_http_realip_module by default:
nginx -V 2>&1 | tr ' ' '\n' | grep realip
If you see --with-http_realip_module, you're set. If not, install the full Nginx package (some minimal builds omit it):
# Debian/Ubuntu
apt install nginx-full
# RHEL/Fedora — the default nginx package usually includes it
dnf install nginx
Step 1: Configure the real-IP module
Create /etc/nginx/cloudflare.conf and include it in your nginx.conf at the http { } level:
# cloudflare.conf — include in nginx.conf http { } block
# Cloudflare IPv4 ranges
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 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;
# Cloudflare IPv6 ranges
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;
Include it in nginx.conf:
http {
include cloudflare.conf;
# ... rest of your config
}
After reloading Nginx, your $remote_addr and $binary_remote_addr variables now contain the real visitor IP — everything downstream (access logs, rate limiting, geo-IP lookups) uses the correct address.
Important: Cloudflare publishes their current IP ranges at cloudflare.com/ips/. Ranges change as they expand. Check the list periodically and update your config — or set up the auto-update script in Step 3.
Step 2: Conditionally skip logging your own IPs
With real IPs restored, you can filter your own traffic from access logs — keeps them lean and focused on actual visitors:
map $remote_addr $log_ip {
"127.0.0.1" 0;
"::1" 0;
"203.0.113.50" 0; # Your home/office IPv4
"2001:db8:abc::1" 0; # Your home IPv6
# Nginx maps don't support CIDR for ranges natively,
# but you can use a regex for an IPv6 prefix:
"~*^2001:db8:abc:def:" 0;
default 1;
}
Use $log_ip in your server blocks:
access_log /var/log/nginx/mysite.access.log combined if=$log_ip;
The regex approach for IPv6 prefixes works because Nginx's map directive supports case-insensitive regex matching with ~*. For IPv4 subnets, list the specific IPs or the first few octets as a regex — but in practice, residential IPv4 changes often enough that you're better off listing your specific current address and updating it when it rotates.
Step 3: Automate IP range updates
Cloudflare occasionally adds new IP ranges. A cron job that fetches the current list and reloads Nginx keeps your config current without manual intervention:
#!/bin/bash
# /etc/cron.weekly/cloudflare_realip_update
# Fetch current IP ranges
CF_IPS=$(curl -s https://www.cloudflare.com/ips-v4)
CF_IPS6=$(curl -s https://www.cloudflare.com/ips-v6)
if [ -z "$CF_IPS" ] || [ -z "$CF_IPS6" ]; then
echo "Failed to fetch Cloudflare IPs — skipping update" >&2
exit 1
fi
# Write new config
cat > /etc/nginx/cloudflare.conf.tmp << 'HEADER'
# Cloudflare IPv4
HEADER
for ip in $CF_IPS; do
echo "set_real_ip_from $ip;" >> /etc/nginx/cloudflare.conf.tmp
done
cat >> /etc/nginx/cloudflare.conf.tmp << 'HEADER'
# Cloudflare IPv6
HEADER
for ip in $CF_IPS6; do
echo "set_real_ip_from $ip;" >> /etc/nginx/cloudflare.conf.tmp
done
cat >> /etc/nginx/cloudflare.conf.tmp << 'HEADER'
real_ip_header CF-Connecting-IP;
HEADER
# Only reload if the file actually changed
if ! diff -q /etc/nginx/cloudflare.conf /etc/nginx/cloudflare.conf.tmp > /dev/null 2>&1; then
mv /etc/nginx/cloudflare.conf.tmp /etc/nginx/cloudflare.conf
nginx -t && systemctl reload nginx
echo "Cloudflare IP ranges updated and Nginx reloaded"
else
rm /etc/nginx/cloudflare.conf.tmp
echo "Cloudflare IP ranges unchanged"
fi
chmod +x /etc/cron.weekly/cloudflare_realip_update
The script fetches the live lists from Cloudflare, rebuilds the config file, and reloads Nginx only if something changed. Weekly is sufficient — Cloudflare doesn't update ranges often.
Step 4: Use real IPs in rate limiting
Once the real-IP module is active, your rate-limit and connection-limit zones should track $realip_remote_addr instead of $binary_remote_addr. Without it, your limits apply to Cloudflare's proxy IPs — a handful of addresses shared by millions of visitors:
# In nginx.conf http { } block — after including cloudflare.conf
limit_req_zone $realip_remote_addr zone=botstop:10m rate=1r/m;
limit_conn_zone $realip_remote_addr zone=phplimit:10m;
$realip_remote_addr is only available after the real-IP module processes the request. Make sure include cloudflare.conf; appears before any limit_req_zone or limit_conn_zone definitions that reference it.
Step 5: Verify it's working
Check your access log after a few page loads — you should see real visitor IPs, not Cloudflare addresses:
tail -f /var/log/nginx/mysite.access.log
If you're still seeing Cloudflare IPs, check that:
cloudflare.confis included at thehttp { }level, not inside a server block- Cloudflare's SSL/TLS mode is set to "Full" or "Full (Strict)" — Flexible mode strips the
CF-Connecting-IPheader - You're not using another proxy layer in front of Cloudflare that overwrites the header
Full reference: ngx_http_realip_module
This guide is maintained as part of a modular, SSL-first framework. Each configuration is audited for production stability and modern security standards.
Compatibility: Tested against current stable releases. While optimized for the stack above, core logic remains relevant for Nginx 1.26+ and PHP 8.2+ environments.