Skip to main content

Handle hairpinning to a Service's own LB IP

Inspect 1.36

A pod that calls its own Service through the external load balancer IP works on Syself Autopilot. That same call, hairpinning, times out on many other setups. It occurs when an ingress controller health-checks itself through the load balancer, or when a pod reaches a shared Service by its public address instead of the in-cluster Service name. It works here because the CCM sets one field correctly. This page explains the mechanism so you can recognize the rare case where it breaks.

Hairpinning is a pod connecting to the public load balancer IP, with the packet turning around to return to a pod on the same side. It resembles a client on the internet, but it never leaves:

flowchart LR
  P["Pod"] -->|"1. connect to the Service's<br/>external load balancer IP"| LB["Load balancer IP"]
  LB -->|"2. packet turns around,<br/>back into the cluster"| B["Backend pod"]

The ipMode field decides the path#

The CCM writes status.loadBalancer.ingress[*].ipMode on the Service, and its value chooses how hairpin traffic is routed.

Without PROXY protocol, ipMode is VIP. Cilium replaces kube-proxy here. It runs in the Linux kernel, intercepts traffic to the load balancer IP on the node itself, and routes it directly to a pod. The Hetzner load balancer is not involved. Hairpinning works.

With PROXY protocol, ipMode is Proxy. Cilium does not intercept the load balancer IP. Traffic from a pod travels to the real Hetzner load balancer, which routes it back into the cluster and adds the PROXY protocol header. Hairpinning works, and the header the backend expects is present.

The CCM sets ipMode: Proxy automatically when you set uses-proxyprotocol: "true". You never set it manually.

What breaks it#

The failure is one specific mismatch: the load balancer sends PROXY protocol headers, but ipMode is not Proxy. In that case, Cilium bypasses the load balancer and routes directly to the pod, so the ingress controller receives no PROXY header at the start of the connection, misparses it, and drops it. The connecting pod experiences a timeout.

Note

The platform CCM sets ipMode correctly, so you will not encounter this on a stock cluster. It can only happen if you replace or downgrade the CCM to a version that does not implement ipMode: Proxy.

Verify it#

Once the CCM has processed the Service, read the field:

		$ kubectl get svc <service-name> -o jsonpath='{.status.loadBalancer.ingress[0].ipMode}'
Proxy
	

With PROXY protocol enabled, expect Proxy. Without it, expect VIP. If you set load-balancer.hetzner.cloud/hostname on the Service, the ingress entry carries only the hostname and this command returns nothing. If a hairpin call still times out, .