Applications behind an HAProxy tier log every request as coming from the proxy's own address. Which HAProxy settings restore the real client address, and what breaks when the two ends disagree about the PROXY protocol?
answer
- the proxy dials the server itself
- a header, or a preamble before the bytes
- append versus overwrite matters for trust
- both ends must be told, it is not negotiated
- a port that accepts it must not be public
basics
~20 sHAProxy opens its own connection to the server, so the source address is the proxy's. In HTTP mode option forwardfor adds the client address as a header; for TCP or TLS passthrough the PROXY protocol carries it, via send-proxy on the server line and accept-proxy on a receiving bind.
solid answer
~50 sThe proxy terminates the client connection and opens a fresh one to the server, so the server legitimately sees the proxy's address. In `mode http`, `option forwardfor` inserts the client address into `X-Forwarded-For`. Below HTTP — TCP mode, TLS passthrough, or a non-HTTP protocol — there is no header to insert into, so you use the PROXY protocol: `send-proxy` (or `send-proxy-v2`) on the `server` line makes HAProxy prepend a small header describing the original connection, and `accept-proxy` on a `bind` line makes HAProxy expect one. Both are strict. A bind with `accept-proxy` rejects any connection that arrives without the header, so health checkers and manual `curl` tests to that port fail; and a server that does not expect the header reads `PROXY TCP4 ...` as the first line of the request and answers 400. Use `tcp-request connection expect-proxy layer4 if { src ... }` when only some sources send it.
go deeper
Be able to explain why the server sees the proxy's address at all — HAProxy opens its own connection — and name option forwardfor as the HTTP-mode way to pass the client address along.
Know the split: a header in HTTP mode, the PROXY protocol below it. Say which keyword goes on which line — send-proxy on the server, accept-proxy on the bind — and that forwardfor appends rather than replaces.
Diagnose both mismatch directions from their symptoms, know the conditional expect-proxy form for mixed sources, and explain how you make X-Forwarded-For trustworthy by having exactly one hop write it from src.
Set the platform rule for client-identity propagation: which tier is authoritative, how many hops are trusted, whether v2 is required to carry TLS context, and how a PROXY-accepting listener is kept off any public address.
## Why the address is lost in the first place HAProxy is not a router. It accepts the client's TCP connection, terminates it, and opens a separate connection to the chosen server from its own address. From the server's point of view the peer *is* HAProxy — nothing is being hidden or lost, the connection genuinely originates there. Anything the application wants to know about the original client has to be carried explicitly, in band. That matters more than logging: rate limiting, geo rules, audit trails and IP allowlists in the application all read the wrong address otherwise. ## HTTP mode: option forwardfor ``` defaults mode http option forwardfor ``` `option forwardfor` makes HAProxy append the client address to `X-Forwarded-For`. Useful variants: `option forwardfor except 127.0.0.0/8` skips a trusted source, `header X-Client-IP` changes the field name, and `if-none` only adds the header when it is absent. The trap is that it **appends**. If a client sends its own `X-Forwarded-For: 1.2.3.4`, HAProxy adds its view of the peer after it, and an application that naively reads the *first* element is reading a value the client chose. Anything security-relevant — rate limits, allowlists, abuse blocking — must therefore either count from the right-hand end, trimming one entry per proxy hop it trusts, or have the edge overwrite the field outright: ``` http-request set-header X-Forwarded-For %[src] ``` Done at the outermost proxy, that discards whatever the client sent and states HAProxy's own observation. Inner tiers can then append normally. The rule of thumb: exactly one hop is allowed to write the field from `src`, and it is the one that talks to untrusted clients. ## Below HTTP: the PROXY protocol In `mode tcp`, and in TLS passthrough where the stream is encrypted end to end, there is no message for HAProxy to edit. The PROXY protocol solves this by prepending a short header to the *proxied connection* before any application bytes: source address, destination address, ports. Version 1 is a single line of text; version 2 is binary and can carry extra type-length-value fields such as the TLS server name or the client certificate details when HAProxy terminated TLS. HAProxy on the sending side: ``` backend web_tls mode tcp server web1 10.0.0.11:443 send-proxy-v2 ``` HAProxy on the receiving side: ``` frontend inner bind :8443 accept-proxy ``` With `accept-proxy`, the `src` fetch and the `%ci` log field report the original client address, so ACLs and logs at the inner tier see the real client. Other receivers have their own switch — for example nginx takes `proxy_protocol` on its `listen` directive — and any of them must be configured deliberately. ## What breaks on a mismatch The protocol is not negotiated; both ends must agree in advance, and each failure looks different: - **Sender speaks it, receiver does not.** The server reads `PROXY TCP4 203.0.113.7 10.0.0.5 51234 443` as the first line of the request and answers 400, or drops the connection. Every request fails, immediately and uniformly. - **Receiver expects it, sender does not.** A bind with `accept-proxy` requires the header on *every* connection, so anything that connects directly — a monitoring probe, a load-balancer health check from another product, an engineer running `curl` — is rejected. The symptom reads like a firewall problem until someone notices which port it is. The conditional form covers the mixed case: ``` frontend inner bind :8443 tcp-request connection expect-proxy layer4 if { src 10.0.0.0/8 } ``` Now the header is required only from the internal range that actually sends it, and everything else connects normally. There is a security dimension too: whoever can reach a port that accepts the PROXY protocol can assert any source address they like, and HAProxy will believe it. Such a bind belongs on an internal address or behind a firewall that admits only the upstream tier — never on a public listener. ## Diagnosing it Start from the log rather than the config. HAProxy's `%ci` field is the client address as HAProxy understands it; if that already shows the upstream proxy, the problem is in front of HAProxy, and if it shows the real client but the application does not, the problem is between HAProxy and the application. `tcpdump` on the server port settles the PROXY-protocol question in one packet: either the connection opens with `PROXY`/the v2 binary signature, or it does not.
- With option forwardfor in place, why is trusting the first X-Forwarded-For entry a vulnerability?Because forwardfor appends. A client that sends its own `X-Forwarded-For` keeps the leftmost position, so an application reading element zero is reading attacker-controlled data — enough to bypass an IP allowlist or evade a per-IP rate limit. Either count from the right, trimming one entry per trusted hop, or have the edge proxy overwrite the field with `http-request set-header X-Forwarded-For %[src]`.
- You enable accept-proxy on a bind and the monitoring system's checks immediately start failing. Why, and what is the fix?`accept-proxy` makes the PROXY header mandatory for every connection on that bind, and the monitoring tool sends a plain connection. Either give health checkers a separate bind without it, or replace it with `tcp-request connection expect-proxy layer4 if { src <upstream-range> }` so only the upstream tier is required to send the header.
- What can send-proxy-v2 carry that the version 1 text header cannot?Version 2 is binary and extensible through type-length-value fields, so beyond the address quad it can pass details of the TLS session HAProxy terminated — the negotiated server name, ALPN, and client-certificate information — to a backend that has no way to see them otherwise. Version 1 carries only the address and port quad.
- Why must a bind that accepts the PROXY protocol never be publicly reachable?Because the header is asserted, not verified: whoever can open a connection to that port declares the source address HAProxy will record, use in ACLs, and log. Exposed publicly it lets anyone forge a client IP and defeat every control built on it. Bind it to an internal address and restrict it to the upstream tier.
saying these in an interview costs you the question
- Says the backend can just read the socket address to get the client IP
- Trusts the leftmost X-Forwarded-For value unconditionally
- Thinks the PROXY protocol is negotiated or optional per connection
- Believes option forwardfor works for TCP mode or TLS passthrough
- Exposes an accept-proxy bind to the internet