
If your Nginx access logs show 172.16.0.1, 10.0.0.5, or your CDN’s IP on every request, you’re missing the field you rely on for security work. WAF rules, rate limiting, geo blocks, and abuse investigations depend on the real client address. This Nginx real IP configuration tutorial shows how to restore it on a VPS or dedicated server in 2026. It includes quick checks and a safe rollback.
This guide assumes you run Nginx directly or use it as a reverse proxy. It applies to Ubuntu 24.04/24.10, Debian 12/13, AlmaLinux 9/10, and Rocky Linux 9/10.
You’ll configure real_ip so Nginx trusts the correct proxy hop (Cloudflare, an L4/L7 load balancer, or HAProxy). After that, Nginx will log the actual visitor IP.
What you’ll fix (and how to confirm the problem)
Confirm you’re actually losing client IPs before you touch configuration:
sudo tail -n 20 /var/log/nginx/access.log
If the first field (remote address) is always a private IP, your reverse proxy/LB IP, or a Cloudflare IP, Nginx isn’t reconstructing the client address from the proxy chain.
Next, check which headers your upstream sends.
Create a temporary debug location:
sudo nano /etc/nginx/conf.d/realip-debug.conf
server {
listen 80;
server_name _;
location = /realip-debug {
default_type text/plain;
return 200 "remote_addr=$remote_addr\nxff=$http_x_forwarded_for\ncf=$http_cf_connecting_ip\nri=$realip_remote_addr\n";
}
}
Reload Nginx and test the endpoint:
sudo nginx -t && sudo systemctl reload nginx
curl -s http://YOUR_SERVER_IP/realip-debug
On a Cloudflare-proxied domain, you’ll usually see CF-Connecting-IP and X-Forwarded-For. Behind a load balancer, you’ll typically see X-Forwarded-For, and sometimes X-Real-IP.
Nginx real IP configuration tutorial: choose the correct header and trust boundary
Two directives decide whether this is secure and correct:
real_ip_header: the header (or mechanism) that carries the client IP.set_real_ip_from: the proxy IPs/subnets you trust to provide that header.
If you trust the wrong sources, a client can spoof their address. They do this by sending a fake header directly to your origin.
Treat the trust boundary as a security control, not a convenience setting. Only trust addresses that can legitimately sit in front of your Nginx origin in your design.
Common setups
- Cloudflare in front of Nginx: use
real_ip_header CF-Connecting-IP;and trust Cloudflare IP ranges. - Load balancer in front of Nginx (AWS ALB/NLB, HAProxy, provider LB): use
real_ip_header X-Forwarded-For;and trust only the LB’s IPs/subnets. - Proxy Protocol from L4 load balancer: use Nginx
proxy_protocolon the listen socket andreal_ip_header proxy_protocol;.
Step 1 — Back up Nginx config and remove the debug server
Start with a quick backup of your Nginx configuration:
sudo tar -C /etc -czf /root/nginx-etc-backup-$(date +%F).tar.gz nginx
If you created the debug server above, remove it now. It’s useful for a quick look, but it should not stay around.
Don’t keep a permanent endpoint that echoes request headers:
sudo rm -f /etc/nginx/conf.d/realip-debug.conf
sudo nginx -t && sudo systemctl reload nginx
On production hosting, do this during a quiet window. If you’d rather not make OS-level changes yourself, managed VPS hosting from HostMyCode is set up for changes like this with validation and rollback support.
Step 2 — Configure real IP for Cloudflare (safe, maintainable method)
Cloudflare publishes the IP ranges its proxies use. In 2026, assume these ranges will change over time.
Plan to update them periodically.
The goal is simple: trust only Cloudflare networks, then read the client IP from CF-Connecting-IP.
Keep this in a dedicated file so it’s easy to audit and update later:
sudo nano /etc/nginx/conf.d/realip-cloudflare.conf
Add the following configuration:
# Trust Cloudflare proxy IPs only
real_ip_header CF-Connecting-IP;
real_ip_recursive on;
# IPv4 (sample; keep current with Cloudflare docs)
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;
# IPv6 (sample)
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;
Test and reload:
sudo nginx -t && sudo systemctl reload nginx
Verify Cloudflare real IP works
Generate a request, then check the log. The first column should now be the visitor IP, not a Cloudflare edge:
sudo tail -n 5 /var/log/nginx/access.log
Also confirm your application sees the corrected address. For PHP-FPM apps, Nginx passes the client IP via REMOTE_ADDR after realip processing.
Most frameworks behave correctly once Nginx is fixed.
If you’re tightening security at the same time, pair this with a simple firewall baseline. HostMyCode’s firewall audit tutorial helps you confirm you haven’t left the origin directly reachable while using Cloudflare.
Step 3 — Configure real IP behind a load balancer (X-Forwarded-For)
With a load balancer in front of Nginx, you usually trust the balancer and read X-Forwarded-For.
The classic mistake is trusting 0.0.0.0/0 (everyone). Don’t.
Create a separate file for load balancer handling:
sudo nano /etc/nginx/conf.d/realip-lb.conf
Example configuration (replace the trusted IPs with your actual LB addresses or subnet):
# Trust only your load balancer
set_real_ip_from 203.0.113.10; # LB public IP
set_real_ip_from 10.20.0.0/16; # or LB private subnet
real_ip_header X-Forwarded-For;
real_ip_recursive on;
Then test and reload:
sudo nginx -t && sudo systemctl reload nginx
Quick diagnostic: does your LB actually send X-Forwarded-For?
Confirm the header is present in real traffic. Temporarily log it.
Don’t leave this enabled indefinitely on a busy server:
sudo nano /etc/nginx/conf.d/logformat-xff.conf
log_format with_xff '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" xff="$http_x_forwarded_for"';
Apply it to a single server block if you can. After you’ve confirmed the header, revert to your usual log format.
That keeps logs smaller and cheaper to ship/store.
Step 4 — Proxy Protocol setups (only if you know you’re using it)
Some L4 load balancers send the client address via Proxy Protocol instead of HTTP headers. In that case, the client IP arrives outside the HTTP layer.
You must enable it on the listen directive.
Example (adjust to your actual site config file):
sudo nano /etc/nginx/sites-available/example.com
server {
listen 80 proxy_protocol;
listen 443 ssl http2 proxy_protocol;
real_ip_header proxy_protocol;
set_real_ip_from 203.0.113.10; # load balancer IP
# ... rest of your config
}
Warning: Enabling proxy_protocol on a port that also accepts direct client connections will break those clients (TLS/HTTP negotiation fails). This setting is effectively all-or-nothing per listener.
Step 5 — Update Nginx logs so you can audit the full chain
After real IP works, keep the original forwarding chain in your logs. It’s invaluable when you correlate abuse reports or trace support tickets.
A practical middle ground is to log the corrected client IP as $remote_addr. Also record X-Forwarded-For (and Cloudflare’s connecting IP if present).
Edit your main Nginx config (Ubuntu/Debian typically /etc/nginx/nginx.conf; Alma/Rocky often the same):
sudo nano /etc/nginx/nginx.conf
Find log_format main and extend it:
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" '
'xff="$http_x_forwarded_for" cf="$http_cf_connecting_ip"';
Test and reload:
sudo nginx -t && sudo systemctl reload nginx
Step 6 — Make rate limiting, WAF rules, and allowlists use the corrected IP
This isn’t only about cleaner logs. It also changes how Nginx evaluates:
limit_reqandlimit_connallow/denyrules- geo-based rules
- application allowlists (admin paths, webhook endpoints)
If bot protection previously throttled a single proxy IP, expect behavior to change immediately after you enable real IP. That’s normal.
Re-check your rate-limit zones, burst values, and any allow/deny lists that were built around the old behavior.
If you use Nginx rate limiting, pair this with HostMyCode’s Nginx rate limiting tutorial, because “per-client” limits only make sense once the client IP is accurate.
Sanity test: rate limit counts by client, not by proxy
After the change, send a small burst from your workstation. Confirm you trip limits for your IP, not for everyone:
for i in $(seq 1 30); do curl -s -o /dev/null -w "%{http_code}\n" https://example.com/; done
Step 7 — Close the origin: stop direct-to-origin IP bypass (Cloudflare users)
If you use Cloudflare, restoring the real IP is only part of the fix. You also need to stop direct requests to your server IP.
If attackers can reach the origin, they can bypass Cloudflare. They can also send whatever headers they want.
The clean approach is to firewall the origin. Accept HTTP/HTTPS only from Cloudflare IP ranges, plus SSH from your admin IPs.
Start by auditing what’s currently exposed. If you’re not sure which rules you added over time, use this: UFW firewall setup tutorial.
On UFW, you’d typically allow Cloudflare IP ranges and deny 80/443 from the general internet. The exact rule set depends on what else the host runs.
Don’t apply a “web-only” firewall to a server that also handles mail or DNS.
Step 8 — Troubleshooting: wrong IP after configuration
When the IP is still wrong, the cause is usually one of these. Work through the list in order.
- You trusted the wrong proxy IPs. Confirm requests actually arrive from the addresses in
set_real_ip_from. Check withsudo tail -n 20 /var/log/nginx/access.logbefore enabling realip, or confirm inbound IPs in your provider’s LB status. - Header mismatch. Cloudflare should use
CF-Connecting-IP. Load balancers usually useX-Forwarded-For. If you select the wrong header, Nginx will keep logging the proxy IP. real_ip_recursiveoff. If there are multiple hops (CDN → LB → Nginx), enablereal_ip_recursive on;and trust only the last hop(s) you control.- Multiple Nginx instances. Make sure you’re editing the config used by the active service. Containers and control panels can leave you with more than one Nginx stack.
- Direct requests bypass the proxy. If the origin IP is reachable, your “real IP” may become whatever header the attacker supplies. Fix the firewall first.
If you lock yourself out (rare, but it happens with listener changes), use your provider console to connect.
Revert by removing the realip file, then reload Nginx.
Step 9 — Hardening and monitoring after the change
Once IP attribution is correct again, alerts and incident triage become trustworthy. Two follow-ups are worth doing right away:
- Set log-based alerts for spikes in 4xx/5xx from a single client IP.
- Review SSH and admin access patterns using real source addresses instead of a shared proxy IP.
If you don’t already have baseline monitoring, HostMyCode’s server monitoring tutorial covers a lightweight way to track uptime, load, disk, and log signals without standing up a full observability stack.
Practical checklist: production-ready real IP setup
- Backed up
/etc/nginxbefore changes - Configured
real_ip_headerfor your proxy type (Cloudflare, XFF, or proxy_protocol) - Restricted trust to known proxies only via
set_real_ip_from - Enabled
real_ip_recursive onwhere multiple proxy hops exist - Updated log format to capture both corrected IP and forwarded chain
- Verified rate limits and allow/deny rules now work per visitor
- Blocked direct-to-origin access (CDN users), especially on ports 80/443
- Recorded a rollback plan (remove conf file, reload Nginx)
Summary: keep client IPs accurate on a hosting VPS
Accurate client IPs make the rest of your controls behave correctly. That includes WAF decisions, throttling, geo restrictions, and basic “who did what” audits.
The pattern is consistent across setups: trust only your proxies, read the right header, then verify both logs and application behavior.
If you’re rebuilding or migrating web stacks and want predictable defaults, start with a HostMyCode VPS (or choose dedicated servers for heavier traffic). Consistent network layouts and access controls make it much easier to keep real IP handling correct over time.
If you’re running Nginx behind Cloudflare or a load balancer and need the real client IP for rate limiting, security logs, and admin auditing, HostMyCode can help. Pick a managed VPS hosting plan to have these settings implemented and validated, or deploy it yourself on a fast HostMyCode VPS with full root access.
FAQ
Do I need real_ip_recursive on?
Use it if there’s more than one proxy hop (for example: Cloudflare → load balancer → Nginx). Keep set_real_ip_from tight, or you risk trusting spoofed chains.
Should I use X-Forwarded-For or CF-Connecting-IP with Cloudflare?
Prefer CF-Connecting-IP for Cloudflare because it’s a single client IP, not a chain. You still must trust only Cloudflare’s IP ranges.
After enabling real IP, my rate limiting got stricter. Why?
Before, every user looked like the same proxy IP, so limits were effectively global. After real IP is fixed, the limiter sees each visitor separately. That changes burst patterns and may reveal real bot traffic.
Will this fix WordPress seeing the wrong IP?
Usually yes, if Nginx terminates the client connection and passes requests to PHP-FPM. Once Nginx sets the correct REMOTE_ADDR, WordPress and most plugins will see the right address without extra configuration.
What’s the safest rollback?
Remove the /etc/nginx/conf.d/realip-*.conf file you added, then run sudo nginx -t and sudo systemctl reload nginx. If Nginx won’t reload, restore your backup tarball.