no X-Forwarded-For with Traefik 2 on bare metal Kubernetes with ClusterIP Service and kube-keepalive-vip
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
When running Traefik 2 as an ingress controller on bare-metal Kubernetes with a ClusterIP Service and kube-keepalived-vip for high availability, the X-Forwarded-For (XFF) header may be missing or contain the wrong IP. This happens because the client's real IP is lost during the network path from the VIP to the Traefik pod. The fix depends on which component is performing SNAT (Source Network Address Translation).
Why XFF Is Missing
The request path on bare metal with kube-keepalived-vip:
The problem: kube-proxy performs SNAT when routing to a ClusterIP Service, replacing the client's source IP with a node IP. By the time the request reaches Traefik, the source IP is a cluster-internal address, not the client's IP. Traefik sets XFF from the source IP it sees, which is now wrong.
Solution 1: Use externalTrafficPolicy: Local
The most common fix — prevents kube-proxy from performing SNAT:
With externalTrafficPolicy: Local, kube-proxy routes traffic only to Traefik pods on the same node as the incoming request, without SNAT. The client's real IP is preserved.
Tradeoff: If Traefik is not running on the node that receives the request, the request is dropped. Use a DaemonSet to run Traefik on every node, or ensure keepalived only assigns the VIP to nodes running Traefik.
Solution 2: Enable Proxy Protocol
If an L4 load balancer or keepalived supports Proxy Protocol, it can encode the client IP in a header:
Configure Traefik to Accept Proxy Protocol
As Traefik deployment args:
Configure the Upstream Component to Send Proxy Protocol
The component in front of Traefik (HAProxy, nginx, or a custom keepalived check) must be configured to send Proxy Protocol:
Solution 3: Use hostNetwork on Traefik
Run Traefik directly on the host network, bypassing kube-proxy entirely:
With hostNetwork: true, Traefik binds directly to the node's network interface. Client IPs are preserved because there is no kube-proxy SNAT. Keepalived routes traffic to the node, and Traefik sees the real client IP.
Solution 4: Configure Traefik forwardedHeaders
Tell Traefik which IPs are trusted to send XFF headers:
This tells Traefik to trust XFF headers from these IP ranges. If an upstream proxy (like kube-proxy or keepalived) sets XFF, Traefik will use it instead of the source IP.
Comparison of Solutions
| Solution | Preserves Client IP | Complexity | Limitations |
externalTrafficPolicy: Local | Yes | Low | Requires NodePort/LB, pods on all nodes |
| Proxy Protocol | Yes | Medium | Upstream must support Proxy Protocol |
hostNetwork: true | Yes | Low | Port conflicts, reduced isolation |
forwardedHeaders trusted IPs | Depends | Low | Requires upstream to set XFF correctly |
Verifying XFF
Common Pitfalls
- ClusterIP does not support externalTrafficPolicy:
externalTrafficPolicyonly works withNodePortandLoadBalancerservice types. If you must use ClusterIP, usehostNetworkor Proxy Protocol instead. - Proxy Protocol mismatch: If Traefik expects Proxy Protocol but the upstream does not send it (or vice versa), connections fail completely. Both ends must agree on whether Proxy Protocol is used.
- trustedIPs too broad: Setting
trustedIPsto0.0.0.0/0allows anyone to spoof XFF headers. Only trust your internal network CIDRs. - DaemonSet with hostNetwork port conflicts: If another process (like another ingress controller or web server) uses port 80/443 on the node, Traefik cannot bind. Ensure exclusive port access.
- kube-keepalived-vip and externalTrafficPolicy: Local: If the VIP is on a node where Traefik is not running, traffic is dropped. Configure keepalived health checks to only assign the VIP to nodes running Traefik pods.
Summary
- XFF is missing because kube-proxy performs SNAT when routing to ClusterIP services, replacing the client IP
- Use
externalTrafficPolicy: Localwith NodePort/LoadBalancer to preserve client IPs (simplest fix) - Use
hostNetwork: trueon the Traefik DaemonSet to bypass kube-proxy entirely - Enable Proxy Protocol on both the upstream load balancer and Traefik for explicit client IP encoding
- Always verify with a header-echo endpoint after making changes

