PROXY protocol makes the Hetzner load balancer pass the real client IP to your pods. Without it, every request appears to originate from the load balancer, which breaks rate limiting, WAF rules, audit logs, and any `NetworkPolicy` that matches on source IP. Set it on **both** sides: the load balancer adds the header, and the backend reads it. If only one side is enabled, every connection breaks. ## Turn it on Enable it at the load balancer Add the annotation to the `type: LoadBalancer` Service: ```yaml metadata: annotations: load-balancer.hetzner.cloud/uses-proxyprotocol: "true" ``` Tell the backend to read it The backend side depends on what receives the connection. Set `use-proxy-protocol` in the controller ConfigMap: ```yaml title="ingress-values.yaml" controller: config: use-proxy-protocol: "true" ``` Enable `proxyProtocol` on the entryPoint the load balancer targets (`web` or `websecure`). Traefik only trusts PROXY headers from IPs you list, so set `trustedIPs` to cover the sources that reach the entryPoint. An application that reads the connection directly must parse the PROXY protocol header itself, before it reads the request bytes. Use a PROXY protocol library for your language rather than parsing the header manually. ```yaml title="ingress-values.yaml" controller: replicaCount: 2 config: use-proxy-protocol: "true" service: annotations: load-balancer.hetzner.cloud/location: fsn1 load-balancer.hetzner.cloud/uses-proxyprotocol: "true" ``` Both the Service annotation and the ConfigMap value are present, so the two sides agree. ## What else it turns on Enabling PROXY protocol also causes the CCM to set the Service's `ipMode` to `Proxy`, which keeps a pod calling its own load balancer IP working. You do not set that yourself. See [Handle hairpinning](/docs/hetzner/apalla/network/load-balancing/hairpinning) for why it matters. By default the load balancer hides the caller. When a client connects, the load balancer opens a fresh connection to your node and puts its own address on it (SNAT, source network address translation). The application at the other end sees the load balancer as the source and cannot determine who actually called. PROXY protocol addresses this. As the load balancer forwards the connection, it prepends a short header carrying the client's real IP and port. The backend reads that header first, then treats the connection as if it came directly from the client. Kubernetes can lose the address a second time, inside the cluster. It depends on the Service's `externalTrafficPolicy`: - **`Cluster` (the default):** the node that receives the packet may forward it to a pod on another node, and it source-NATs the packet on the way, so the pod sees the node's address, not the client's. - **`Local`:** the node does not source-NAT and does not forward across nodes. Only nodes that run a ready pod for the Service receive traffic, and the pod keeps the source it was handed. On a Hetzner load balancer the outer address is already replaced before the packet reaches any node, so PROXY protocol is what recovers the real client IP. `externalTrafficPolicy` decides what happens after that, inside the cluster. For a deeper walk through both hops, see the Kubernetes [Using Source IP](https://kubernetes.io/docs/tutorials/services/source-ip/) tutorial and [Preserve the client source IP](/docs/hetzner/apalla/network/load-balancing/preserve-client-source-ip). > [!IMPORTANT] > Enable PROXY protocol before you rely on anything that reads the client address: rate limits, a WAF, or source-IP network policies. Until both sides are set, those controls see the load balancer's address and behave as if every request came from one client. For the wider picture, including the `externalTrafficPolicy: Local` alternative and how to verify what the pod sees, read [Preserve the client source IP](/docs/hetzner/apalla/network/load-balancing/preserve-client-source-ip).