Skip to main content

Preserve the client source IP

Inspect 1.36

Preserving the original client IP is essential for security and observability. Features like rate limiting, Web Application Firewalls (WAF), audit logging, and NetworkPolicy IP rules all rely on knowing the true caller address. By default, external traffic is source-NATed by the Hetzner Load Balancer, making all incoming requests appear to originate from the load balancer itself. This guide explains why client IPs are masked and how to recover them.

Why the address is lost

A request takes several hops to reach your pod:

flowchart LR
    C(["Client<br><code>Src: 203.0.113.5</code>"]):::you --> LB["Hetzner LB<br><code>Src: 162.55.158.16</code>"]:::external
    LB -- "1. External SNAT" --> N1["Node A (NodePort)<br><code>No local pod</code>"]:::platform
    N1 -- "2. Inter-node SNAT" --> P["Pod on Node B<br><code>Sees Node A IP</code>"]:::app

Client IP addresses can be masked at two distinct network boundaries:

  1. At the external load balancer: When a client connects, the Hetzner Load Balancer terminates the connection and opens a new TCP connection to one of your cluster nodes. On this outbound connection, the load balancer applies Source Network Address Translation (SNAT), replacing the client's real IP with its own address.
  2. Inside the cluster network: With the default externalTrafficPolicy: Cluster, if a node receives traffic but does not host a local pod replica, it forwards the packet across the cluster to another node. This inter-node hop applies a second SNAT, overwriting the source address with the forwarding node's internal IP.

Recovering the client IP

PROXY protocol

PROXY protocol is the only way to recover the client IP on Hetzner Cloud, because the Hetzner Load Balancer always applies Source NAT (SNAT) when forwarding connections to your nodes.

In PROXY protocol as the load balancer forwards the connection, it prepends a small header of plain bytes at the very start of the stream, before any TLS or HTTP data. That header carries the original client IP and port. The load balancer writes it as it reads and forwards the packets. Because the header sits at the TCP level ahead of application data, it successfully delivers the client IP even when encrypted TLS connections pass straight through to backend pods. The backend must be configured to expect the header, or it reads those bytes as invalid input and the connection breaks.

Supported for both IPv4 and IPv6 across TCP and HTTP connections, the PROXY protocol requires configuration on both the load balancer and your backend application. See for complete setup instructions.

Eliminate the in-cluster hop with externalTrafficPolicy: Local

Configuring externalTrafficPolicy: Local on your Service ensures incoming traffic is only routed to nodes that host a ready, local pod replica. This stops nodes from proxying traffic across the cluster, eliminating the secondary in-cluster SNAT hop:

yaml
		spec:
  externalTrafficPolicy: Local
	

Setting externalTrafficPolicy to Local by itself does not restore the real client IP, because the load balancer has already applied Source NAT before the packet reaches any node. Use Local to eliminate cross-node forwarding (secondary SNAT) and ensure the Hetzner Load Balancer routes traffic exclusively to nodes currently hosting an endpoint for the service backed by the loadbalancer. Use the PROXY protocol when your workloads need the caller's actual IP address.

This behavior introduces operational trade-offs. Because the load balancer only targets nodes with local, healthy endpoints, any node without an active replica drops out of the target pool. To prevent dropped connections during rolling node replacements, distribute replicas across nodes using topologySpreadConstraints and enforce a . See .

Note

Direct server return and BGP-based source preservation are not available on Hetzner load balancers. PROXY protocol is the mechanism the platform supports.

Verify what the pod sees

For an HTTP application behind ingress-nginx with PROXY protocol enabled, the controller writes the real client address into the X-Forwarded-For header. Echo it back from a test application, or read the ingress controller's access logs, and confirm the address is the client's rather than the load balancer's.

Where to go next

Recovering the client address is the foundation for the rest of the controls that decide by address.