Real Client IPs Behind a Reverse Proxy: Getting Them Right

In this article
- Why does your server log the proxy address instead of the visitor?
- X-Forwarded-For, X-Real-IP and the Forwarded header: which one carries the client IP?
- How to restore the real client IP with the nginx real_ip module
- Trusted proxy addresses: why the list decides everything
- PROXY protocol: an option when headers are not enough
- How to check that the real client IP is logged correctly
- Summary
- FAQ
To get the real client IP behind a reverse proxy, you need two things. The proxy has to pass the visitor’s address in a request header. And your origin has to accept that header only from trusted proxy addresses. Do your access logs, analytics and crawler checks all show one proxy IP instead of real visitors and bots? Then one of those two pieces is missing. Below: how the address gets lost, which headers carry it, how to restore it with the nginx real_ip module and how to confirm the fix actually works. The same trust model covers the X-Forwarded-Proto header, which is what stops WordPress redirect loops behind proxies when HTTPS ends at the proxy.
Why does your server log the proxy address instead of the visitor?
Because the proxy opens the TCP connection to your server. So by definition, the proxy is the remote address. The visitor talks to the proxy, then the proxy starts a brand new connection to your origin. Nothing in that second connection says who sent the original request, unless the proxy adds it.
And the side effects pile up fast:
- every access log entry carries the same IP,
- bot verification by reverse DNS fails, because the proxy is not Googlebot,
- geo reports show the proxy’s location rather than your audience,
- per-IP allow, deny and blocking rules hit the proxy and, with it, everyone.
That last one hurts. Block one abusive IP and you’ve blocked your whole audience.
For SEO work it’s worse than it looks. Reading server logs for crawl issues only works when you can tell which requests came from real crawlers. If every line says the same address, what exactly are you analysing? The fix lives in two places: the proxy sends the address, and the origin decides whether to trust it.
X-Forwarded-For, X-Real-IP and the Forwarded header: which one carries the client IP?
Any of the three can. The right one is simply the one your proxy actually sends. They differ in format and in how widely they’re used:
- X-Forwarded-For header: a comma-separated chain where each proxy appends the address it received the request from. The leftmost entry is the claimed original client (note “claimed”).
- X-Real-IP: a single address, no chain, commonly set by an NGINX proxy.
- Forwarded header (RFC 7239): the standardised form with for=, proto= and by= parameters. Less common in practice.
Here’s the catch. Any client can send these headers itself. So the value is only as trustworthy as the proxy that wrote the last entry. Don’t guess which header your proxy adds, either. Ask your provider, then look at a raw request and confirm it yourself.
How to restore the real client IP with the nginx real_ip module
The nginx real_ip module swaps the client address for the value from a header you pick, and it swaps the port too if the header includes one. That’s all in the official ngx_http_realip_module documentation. Setup is five steps:
- Confirm the module is available in your build (
nginx -Vlists compiled modules). - List your proxy ranges with
set_real_ip_from, using single IPv4 or IPv6 addresses or CIDR blocks. - Pick the header with
real_ip_header. - Turn on
real_ip_recursivewhen requests pass through more than one proxy hop. - Reload nginx and test.
A minimal block in the http or server context looks like this. Swap the example ranges for your proxy’s actual addresses:
set_real_ip_from 192.168.1.0/24;
set_real_ip_from 2001:0db8::/32;
real_ip_header X-Forwarded-For;
real_ip_recursive on;Once that’s live, $remote_addr and the access log show the visitor. Logging and allow/deny rules just work, no other changes needed.
Trusted proxy addresses: why the list decides everything
Nginx rewrites the address only when the connection comes from one of the trusted proxy addresses in set_real_ip_from. That list is your entire security boundary. Seriously, all of it. With recursive mode off, nginx uses the last value in the header. With it on, nginx takes the last address that isn’t on the trusted list, which skips over your own proxy chain.
Too broad a list (say, 0.0.0.0/0) and anyone can spoof their IP with a forged header. Too narrow, and the proxy address stays in your logs. So keep it updated when your provider adds or rotates egress addresses. One more thing people forget: block direct access to the origin from outside the proxy ranges. Otherwise an attacker just skips the proxy and sends a forged header straight to your server. Game over.
PROXY protocol: an option when headers are not enough
PROXY protocol passes the client address at the connection level, not in HTTP. Handy for TCP or TLS passthrough, where the proxy never sees HTTP and can’t add headers at all. In nginx you enable proxy_protocol on the listen directive, then set real_ip_header proxy_protocol.
Both sides have to agree, though. A listener that expects PROXY protocol rejects plain connections. A plain listener chokes on the extra header. If you use the Jalvo NGINX reverse proxy service with EU and USA addresses, all of this configuration happens on your own origin.
How to check that the real client IP is logged correctly
Simple test: your own visit shows up in the log with your real address, and a forged header sent straight to the origin does nothing. Run through this:
- visit the site from a known IP and find it in
access.log, - send a forged
X-Forwarded-Fordirectly to the origin and confirm it is ignored, - check that crawler IPs in the log resolve by reverse DNS,
- compare analytics geo data before and after the change.
While testing, I’d log $http_x_forwarded_for next to $remote_addr so you can see the raw chain. The usual mistakes? Forgetting IPv6 ranges. A typo or wrong header name. And running two layers, like a CDN in front of a proxy, without recursive mode. If you’re still picking a setup, compare a reverse proxy versus a VPN first.
Summary
Restoring the real client IP behind a reverse proxy boils down to three things: the proxy sends the address, the origin trusts only known proxy ranges, and you test the result. For most setups, the nginx real_ip module with a tight set_real_ip_from list and recursive mode does the job. Want more along these lines? Browse technical SEO guides on hosting.
FAQ
Can visitors fake their IP through the X-Forwarded-For header?
Yes, if the origin trusts that header from anyone. Limit set_real_ip_from to your proxy ranges and block direct access to the origin, and the problem goes away.
Should I use X-Forwarded-For or the Forwarded header?
Whichever your proxy actually sends. Forwarded, defined in RFC 7239, is the standard. But X-Forwarded-For is far more widely supported by tools and log parsers.
Why do my logs still show the proxy IP after enabling real_ip?
Usually one of three things: the connecting address is missing from set_real_ip_from, the header name doesn’t match what the proxy sends, or nginx wasn’t reloaded. To track it down, log the raw header next to $remote_addr.


