Why your logs suddenly show 192.168.255.x — and how to fix it properly
When everything sits behind Cloudflare, real-IP handling in Nginx is usually straightforward, trust Cloudflare’s IP ranges, read CF-Connecting-IP, and $remote_addr becomes the actual visitor’s address.
That simplicity disappears the moment you introduce a Linode NodeBalancer in front of your server.
Suddenly your logs show something like:
|
0 1 2 |
192.168.255.46 - - “GET / HTTP/1.1” |
No visitor IP. No geolocation, No rate-limit accuracy, No security context.
This article breaks down why this happens and how to build a clean, universal, future-proof Nginx real-IP setup that works for
- Sites behind Cloudflare only
- Sites behind Cloudflare + Linode NodeBalancer
- Mixed environments with many domains and applications
This is the configuration I now use across my infrastructure — simple, robust, and consistent.
The Real Cause, One Missing Trusted Hop
Let’s look at what changes when you add NodeBalancer.
Before
|
0 1 2 |
Visitor → Cloudflare → Nginx |
Nginx sees
- Source IP > Cloudflare edge
- Visitor IP > inside header
CF-Connecting-IP
Everything works After
|
0 1 2 |
Visitor → Cloudflare → Linode NodeBalancer → Nginx |
Nginx now sees:
- Source IP:
192.168.255.x(private LB network) - Cloudflare header exists, but is ignored
Why?
Because Nginx will only trust forwarded IP headers when the source is listed in set_real_ip_from.
Cloudflare is trusted, but the NodeBalancer private network isn’t, so Nginx throws away the real IP and uses the LB’s address instead.
The fix is shockingly simple, but must be done properly.
The Correct Strategy, Centralize the Trust Logic
Instead of putting real-IP logic inside each site config, it’s far cleaner to:
- Place all trusted proxies (Cloudflare + NodeBalancer) in a single global config
- Extract visitor IP using only one directive, globally
- Keep per-site configs minimal and focused on proxying upstream
This will ensures
- All domains behave consistently
- Adding new sites requires zero IP logic
- Changes to Cloudflare/Linode ranges only happen in one place
- Backends always receive the correct visitor IP
Step 1, Create a Global Real-IP File
/etc/nginx/conf.d/real-ip.conf
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
# Trust Linode NodeBalancer private network set_real_ip_from 192.168.255.0/24; # Trust 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 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; # Trust 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; # Extract true visitor IP real_ip_header CF-Connecting-IP; real_ip_recursive on; |
This becomes your single source of truth.
You never repeat these lines in any site config again.
Step 2, Make Sure Nginx Loads It Globally
Your /etc/nginx/nginx.conf should have
|
0 1 2 |
include /etc/nginx/conf.d/*.conf; |
Still inside http {}, add a readable access log format so you can verify the result
|
0 1 2 3 |
log_format main '$remote_addr - $http_cf_connecting_ip - $http_x_forwarded_for - $host "$request"'; access_log /var/log/nginx/access.log main; |
This makes debugging clear and transparent.
Step 3, Keep Per-Site Configs Clean and Minimal
Your site config should not contain any real-IP directives anymore.
A typical TLS vhost becomes
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
server { listen 443 ssl; server_name example.com; ssl_certificate /var/app/ssl/example.com.crt; ssl_certificate_key /var/app/ssl/example.com.key; access_log /var/log/nginx/example.com.access.log; location / { proxy_pass http://localhost:3000; proxy_set_header Host $host; 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; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } } |
Simple, Deterministic and Zero duplication.
Step 4, If You Use NodeBalancer TCP Mode + Proxy Protocol
If you enable “TCP” + “Proxy Protocol v2” on Linode LB, update your listen directive:
|
0 1 2 |
listen 443 ssl proxy_protocol; |
But do not change the global real-IP logic.
You still want the visitor IP from
|
0 1 2 |
CF-Connecting-IP |
Proxy Protocol is only useful if you want the Cloudflare edge IP, which is rarely needed for apps.
Step 5, Test the Results
Reload
|
0 1 2 3 |
sudo nginx -t sudo systemctl reload nginx |
Then watch the logs:
|
0 1 2 |
tail -f /var/log/nginx/access.log |
You should now see:
$remote_addr= real visitor IP$http_cf_connecting_ip= same IP- No more
192.168.255.x
This applies consistently across all domains, whether they use the LB or not.
Final Thoughts
Handling real IPs becomes messy only when multiple proxy layers enter the picture. The mistake most people make is scattering real-IP directives across different site configs.
That works until the first load balancer enters the system, then logs break, security breaks, and analytics break.
By centralizing trust and extracting the real client IP once, you build an Nginx setup that
- Works cleanly with Cloudflare, Linode NodeBalancer, and with both combined
- Avoids duplicated logic, keeps every domain consistent and makes future expansion simple
This unified approach keeps your infrastructure predictable and your logs honest, just the way it should be.