Traefik2
Kubernetes
ClusterIP
X-Forwarded-For
kube-keepalive-vip

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:

 
1Client (1.2.3.4)
2VIP (10.0.0.100) via keepalived
3  → kube-proxy (iptables DNAT + SNAT)
4Traefik Pod (ClusterIP)
5Backend Pod

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:

yaml
1apiVersion: v1
2kind: Service
3metadata:
4  name: traefik
5spec:
6  type: NodePort  # or LoadBalancer — NOT ClusterIP
7  externalTrafficPolicy: Local
8  ports:
9    - name: web
10      port: 80
11      targetPort: 8000
12    - name: websecure
13      port: 443
14      targetPort: 8443
15  selector:
16    app: traefik

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

yaml
1# Traefik static configuration
2entryPoints:
3  web:
4    address: ":80"
5    proxyProtocol:
6      trustedIPs:
7        - "10.0.0.0/8"      # Trust your cluster CIDR
8        - "192.168.0.0/16"
9  websecure:
10    address: ":443"
11    proxyProtocol:
12      trustedIPs:
13        - "10.0.0.0/8"
14        - "192.168.0.0/16"

As Traefik deployment args:

yaml
1containers:
2  - name: traefik
3    args:
4      - "--entrypoints.web.address=:80"
5      - "--entrypoints.web.proxyProtocol.trustedIPs=10.0.0.0/8,192.168.0.0/16"
6      - "--entrypoints.websecure.address=:443"
7      - "--entrypoints.websecure.proxyProtocol.trustedIPs=10.0.0.0/8,192.168.0.0/16"
8      - "--providers.kubernetescrd"

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:

 
1# HAProxy example (if used in front of Traefik)
2frontend http_front
3    bind *:80
4    default_backend traefik_backend
5
6backend traefik_backend
7    server traefik1 10.0.0.11:80 send-proxy-v2
8    server traefik2 10.0.0.12:80 send-proxy-v2

Solution 3: Use hostNetwork on Traefik

Run Traefik directly on the host network, bypassing kube-proxy entirely:

yaml
1apiVersion: apps/v1
2kind: DaemonSet
3metadata:
4  name: traefik
5spec:
6  selector:
7    matchLabels:
8      app: traefik
9  template:
10    metadata:
11      labels:
12        app: traefik
13    spec:
14      hostNetwork: true
15      dnsPolicy: ClusterFirstWithHostNet
16      containers:
17        - name: traefik
18          image: traefik:v2.10
19          ports:
20            - containerPort: 80
21              hostPort: 80
22            - containerPort: 443
23              hostPort: 443
24          args:
25            - "--entrypoints.web.address=:80"
26            - "--entrypoints.websecure.address=:443"
27            - "--providers.kubernetescrd"

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:

yaml
1entryPoints:
2  web:
3    address: ":80"
4    forwardedHeaders:
5      trustedIPs:
6        - "10.0.0.0/8"
7        - "172.16.0.0/12"
8        - "192.168.0.0/16"
9      insecure: false

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

SolutionPreserves Client IPComplexityLimitations
externalTrafficPolicy: LocalYesLowRequires NodePort/LB, pods on all nodes
Proxy ProtocolYesMediumUpstream must support Proxy Protocol
hostNetwork: trueYesLowPort conflicts, reduced isolation
forwardedHeaders trusted IPsDependsLowRequires upstream to set XFF correctly

Verifying XFF

bash
1# Deploy a test pod that echoes headers
2kubectl run echoserver --image=ealen/echo-server --port=80
3
4# Create an Ingress route through Traefik
5# Then test from outside the cluster:
6curl -s http://your-vip/echo | jq '.request.headers'
7
8# Look for:
9# "x-forwarded-for": "1.2.3.4"    ← Your real client IP
10# "x-real-ip": "1.2.3.4"

Common Pitfalls

  • ClusterIP does not support externalTrafficPolicy: externalTrafficPolicy only works with NodePort and LoadBalancer service types. If you must use ClusterIP, use hostNetwork or 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 trustedIPs to 0.0.0.0/0 allows 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: Local with NodePort/LoadBalancer to preserve client IPs (simplest fix)
  • Use hostNetwork: true on 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

Course illustration
Course illustration

All Rights Reserved.