KBeezie

There's no place like ::1

Menu
  • Home
  • Start Here
  • Security Series
  • About

Restoring Real Visitor IPs with Cloudflare and Nginx

2026/05/11 in Security

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.conf is included at the http { } level, not inside a server block
  • Cloudflare's SSL/TLS mode is set to "Full" or "Full (Strict)" — Flexible mode strips the CF-Connecting-IP header
  • You're not using another proxy layer in front of Cloudflare that overwrites the header

Full reference: ngx_http_realip_module

Technical Audit Summary

This guide is maintained as part of a modular, SSL-first framework. Each configuration is audited for production stability and modern security standards.

Last Audit: May 2026
Environment: Debian Trixie (13)
Nginx: 1.30.0
PHP-FPM: 8.5.5

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.

Tags: cloudflare, real-ip, nginx, logging
©2026 KBeezie | Disclaimer | Privacy Notice