A request arriving at your application carries two kinds of address evidence: the peer that opened the network connection, and an HTTP field claiming an earlier client's address. They answer different questions. The socket peer is observed, while the header is asserted by whoever wrote it, and neither should silently replace the other in a log or an access rule. The comparison below keeps them apart, then shows the one arrangement in which the header becomes usable.

Describe the path before comparing anything:

Client -> approved reverse proxy -> application origin

In that arrangement the origin's socket peer is normally the proxy, and a forwarding header can carry the proxy's assertion about the client. Its value comes from the controlled relationship with that proxy, not from the header's name.

Two kinds of address evidence

The socket peer is a fact about one TCP connection: something completed a handshake from that address. It is not necessarily the human, the device or the account you care about, because a VPN or a Tor exit can legitimately be the source a service observes. It is still the only address the host itself verified, and that is the whole of its value.

A forwarding field is metadata about a chain. X-Forwarded-For, X-Real-IP and the standardized Forwarded field can be written by any client that reaches an endpoint which echoes them, so their trustworthiness depends entirely on who was allowed to set them.

Keep both in the request context under distinct names. An application that overwrites the peer with the header loses the ability to reconstruct a trust error later, which is exactly when you need it.

A header never reroutes the connection

Writing an address into a forwarding field does not change the source of the TCP connection arriving at the server, and it does not alter routing anywhere along the path. Ordinary VPN routing or source NAT delivers traffic without any HTTP header at all. Likewise, a transit gateway does not rewrite a properly protected HTTPS exchange without changing the TLS endpoint arrangement.

The reverse mistake is just as common: HTTPS does not make a client-provided forwarding header truthful. It protects the transport of a false assertion exactly as well as a true one.

If a colleague proposes a header change to fix a routing problem, the diagnosis has already gone wrong. Fix the path, or fix the proxy configuration that writes the header.

Name the proxy allowed to assert

Two rules make a forwarding field usable, and both are configuration rather than code. The edge normalises or drops whatever arrives from outside, following its own documented behaviour for those fields. The origin then takes a replacement address from the proxy path you named and from nothing else. That second rule only holds while clients cannot reach the origin another way, since an origin answering both direct and proxied requests has no idea which one it is serving.

The NGINX real-IP module shows the split cleanly. set_real_ip_from names the senders whose assertion counts, and real_ip_header says which field carries it; the first does the trusting, the second only says where to look.

set_real_ip_from 10.20.0.0/24;
real_ip_header X-Forwarded-For;
# real_ip_recursive off - take the rightmost value in the field
# real_ip_recursive on  - walk the list back past every trusted address

A header-selection directive on its own still establishes nothing about who sent the value.

Transport security has to run the whole way between the components you trust. An edge that normalises correctly loses that value the moment an unprotected hop downstream lets someone else edit the assertion while it travels.

Handle multi-hop chains deliberately

Multiple proxies need an explicit chain model. Do not take the leftmost or rightmost comma-separated value because a blog post did; find out which of your proxies appends, which replaces and which preserves the incoming field. NGINX, for one, can walk the list backwards against its trusted set and stop at the first address it does not own, which is that product's rule rather than a law every framework follows. Read your own proxy's documentation with the same two questions: which value does it select, and which senders does it believe?

Write the chain down: hop order, which hop sets the field, and which hop strips inbound copies. Then keep that note next to the proxy configuration, not only in the ticket. Then test it by sending a request with a forged header through the approved validation process and confirming the origin ignores it.

When you add a hop, revisit that document. A new load balancer inserted in front of the trusted proxy can change which address wins without anyone editing the application.

Keep both observations in the log

Where diagnostics are authorized, log the connection peer and the normalized client address as two separate fields, and add only the forwarding metadata the purpose actually needs. Then prove the deployment with two requests: a proxied one that should record both values sensibly, and a direct-to-origin attempt that has to be refused.

Remember what the pair means. The header adds context supplied by a component you chose to trust; the peer remains the only address your host actually saw. Neither one is proof of individual identity, and a design that treats either as proof will be wrong about someone.

Nuŋlɔɖi siwo ku ɖe eŋu