Client IPs Behind a Reverse Proxy - Trust the Connection Before the Header
Client IPs Behind a Reverse Proxy - Trust the Connection Before the Header
An application behind a reverse proxy faces an awkward choice. If it records only the network peer, every request may appear to come from the proxy. If it accepts a header such as X-Forwarded-For without a trust policy, a client may be able to choose the address that appears in logs, rate limits, or access rules.
The useful question is therefore not simply, “Which header contains the real IP?” It is: which machine delivered this request, which machines are allowed to add forwarding information, and where does that chain stop being trustworthy?
This article develops that model for a small Nginx deployment. The configuration is deliberately generic. Proxy addresses, network paths, and provider-specific headers must be matched to the actual architecture rather than copied blindly.
A Connection Address and a Header Are Different Evidence
On a direct connection, Nginx can observe the address of the peer that opened the socket. Put a reverse proxy in front, and that immediate peer becomes the proxy. The original client address is no longer available at the same network layer, so proxies commonly carry it in HTTP metadata.
X-Forwarded-For is a widely used de-facto request header. The IETF standardized a related Forwarded field in RFC 7239, but the older X-prefixed field remains common. Neither name authenticates its value. A browser, script, misconfigured intermediary, or malicious client can send forwarding fields too.
That distinction creates a practical rule: trust begins with the direct connection, not with the leftmost text in a header. Forwarded metadata becomes useful only when the immediate peer is an expected proxy and that proxy has a defined policy for creating, replacing, or extending it.
Read a Proxy Chain from the Trusted Side
Consider a request that reaches the origin with this field:
X-Forwarded-For: 198.51.100.23, 10.20.0.8
The conventional order runs from the earliest address on the left toward later proxies on the right. However, “leftmost” does not mean “verified.” A client could have supplied an existing value before the first controlled proxy appended anything.
For a security-sensitive decision, evaluation works in the other direction. Start with the socket peer, confirm that it belongs to a narrowly defined trusted proxy, then walk the list from right to left while addresses remain trusted. The first address outside that trusted set is the closest defensible client address. As the MDN X-Forwarded-For reference cautions, it may belong to an untrusted intermediary rather than the person's device. It is still the boundary beyond which the origin has no basis for believing additional claims.
There are two common ways to describe that boundary:
- A fixed number of proxy hops, which is simple but fragile when requests can take paths of different lengths.
- An explicit list of trusted proxy addresses or ranges, which usually models a changing path more safely if that list is kept accurate.
A private address is not automatically trusted, and a public address is not automatically hostile. Trust comes from control of the route and the proxy, not from the address looking familiar.
Express the Boundary with Nginx Real IP
Nginx's Real IP module can replace the effective client address with a value supplied by a known proxy. The module is not enabled by default in every upstream build, so check the installed binary before using its directives:
nginx -V 2>&1
Look for --with-http_realip_module, or confirm how the operating-system package provides the module. The Debian host used to validate the syntax for this article reports Nginx 1.26.3 with that build option. This local result confirms only that the example parses on that host; it says nothing about another server's package or proxy topology.
Suppose one internal load balancer at 10.20.0.10 is the only intended peer for the origin and it maintains X-Forwarded-For. A starting policy is:
set_real_ip_from 10.20.0.10;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
Each line has a separate job:
set_real_ip_fromnames an address or CIDR whose replacement information Nginx will accept. It should identify actual controlled proxies, not every address with a convenient rule such as0.0.0.0/0.real_ip_headerchooses the field carrying the replacement address. Its Nginx default isX-Real-IP; this example selectsX-Forwarded-Forexplicitly.real_ip_recursive ontells Nginx to move backward through the supplied chain and select the last address that is not trusted, rather than simply taking the last value.
After successful processing, $remote_addr contains the effective address. Nginx keeps the original connection peer in $realip_remote_addr. Logging both during a rollout makes the decision visible:
log_format proxy_path
'$remote_addr via $realip_remote_addr "$request"';
access_log /var/log/nginx/access.log proxy_path;
This example uses a hypothetical private address. A real configuration needs every legitimate immediate proxy path, including IPv6 where applicable, and no unrelated range.
The Edge Proxy and the Origin Have Different Jobs
If Nginx is directly exposed to clients, its socket peer is already the address it can observe. It does not need to trust a client-supplied forwarding header merely to obtain that address. If this edge Nginx then proxies to another service, the proxy module can pass the chain onward:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
The downstream service should trust this field only because its direct peer is that controlled Nginx instance. It should not independently apply a conflicting rule such as “always take the first comma-separated value.” One layer should establish the effective address, and later layers should consume that decision under the same documented boundary.
A CDN creates a different arrangement. For example, Cloudflare documents that an origin normally sees a Cloudflare address and receives the visitor address in CF-Connecting-IP. Its Nginx guidance combines that field with Cloudflare's published proxy ranges. Those ranges can change, so a static list copied from an old article is not a durable configuration.
The header and the source-range check belong together. Trusting CF-Connecting-IP from any peer would let a direct client claim an arbitrary value. Trusting current provider ranges while also restricting origin access to the provider creates a much clearer boundary.
Direct-Origin Access Is a Separate Problem
Correct header parsing does not force traffic through a CDN, load balancer, or WAF. If the origin's address remains publicly reachable, a client may connect around the intended edge. Nginx can ignore the forwarded field from that untrusted peer, yet edge caching, filtering, and abuse controls have still been bypassed.
Where the design is genuinely proxy-only, enforce that property at the network boundary: allow the origin port only from the controlled proxy network or current provider ranges, while preserving a deliberate administrative route. The exact firewall mechanism is outside this article's scope. The important point is that network reachability and HTTP-header interpretation are two controls, not substitutes for each other.
Verify the Path, Including a Spoof Attempt
Before reloading a production service, inspect the complete configuration and validate its syntax:
sudo nginx -T
sudo nginx -t
A syntax check cannot prove the trust model. Test the behavior through every expected route and compare the dual-address log:
- Send a normal request through the proxy and confirm the expected effective address and proxy peer.
- Send a request containing a deliberately false
X-Forwarded-Forvalue through the public path. Confirm that untrusted left-side input does not become the security address. - If a direct test route is intentionally available, send the same spoofed header directly. Confirm that an untrusted peer cannot make Nginx replace its socket address.
- Repeat for each valid path, including IPv4 and IPv6 if both are supported.
- Check what PHP or another upstream actually receives; do not assume its framework uses the same proxy policy as Nginx.
Tests should use an address and endpoint owned for that purpose. Avoid locking out the only administration path, and retain a known-good configuration for rollback. Once behavior is understood, reload with the site's established safe procedure rather than treating a successful syntax check as proof that routing is correct.
Do Not Turn an IP Address into More Than It Is
A defensible client address can improve logs, incident investigation, coarse abuse controls, and rate-limit inputs. It is not authentication. Carrier-grade NAT, shared Wi-Fi, VPNs, privacy relays, mobile networks, and ordinary address reassignment all weaken any claim that one address identifies one person or account.
IP-based access rules also need a failure policy. If trusted-proxy metadata is missing or malformed, silently choosing another header may fail open. A sensitive route may need to reject the request or fall back to the verified socket peer, depending on the architecture. That choice should be explicit and tested.
There is a privacy cost as well. RFC 7239 treats forwarded addresses as potentially sensitive and discusses both information leakage and user privacy. Keep them only for a stated operational purpose, restrict access to logs, choose a retention period, and avoid copying an entire internal proxy chain into responses or unnecessary analytics.
A Useful Address Starts with a Small Trust Boundary
The safest answer to “What is the real client IP?” is often that the system cannot know with absolute certainty. It can know something narrower: a particular trusted proxy observed an address and conveyed it over a controlled path.
Build from that narrow statement. Identify the immediate proxy, trust only its actual addresses, evaluate multi-hop information from the nearest trusted side, keep direct-origin exposure consistent with the design, and log both the effective address and original peer while verifying the rollout. Then use the result as one operational signal, not as proof of identity.
