Kubernetes
externalTrafficPolicy
source IP
networking
cloud computing

Kubernetes gives an internal source IP although externalTrafficPolicy is set to Local

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

externalTrafficPolicy: Local preserves the original client source IP only in a specific traffic path: the packet must arrive at a node that has a local endpoint for the Service, and the infrastructure in front of that node must not terminate and re-originate the connection. If you still see an internal IP, something else in the path is rewriting or proxying the request.

This is why the setting feels confusing: it affects kube-proxy behavior for a Service, but it does not magically disable every other hop in front of your pod.

What externalTrafficPolicy: Local Actually Does

For a NodePort or LoadBalancer Service, the default Cluster policy allows traffic to land on one node and then be forwarded to endpoints on another node. That forwarding can require source NAT, so the pod may see the node IP instead of the client IP.

With Local, kube-proxy only sends external traffic to endpoints on the same node. Kubernetes documents this as the way to preserve the original source IP for Service traffic.

A minimal Service looks like this:

yaml
1apiVersion: v1
2kind: Service
3metadata:
4  name: echo
5spec:
6  type: LoadBalancer
7  externalTrafficPolicy: Local
8  selector:
9    app: echo
10  ports:
11    - port: 80
12      targetPort: 8080

That is necessary, but not always sufficient.

Why You May Still See an Internal IP

There are several common reasons.

The Request Is Going Through Another Proxy

If traffic first passes through an ingress controller, CDN, reverse proxy, WAF, or L7 load balancer, your pod may see the proxy's address rather than the original client address. In that case externalTrafficPolicy is not broken; you are simply no longer looking at raw client-to-Service traffic.

When a proxy terminates HTTP and opens a new connection to the backend, the original client IP often moves into a forwarded header such as X-Forwarded-For. Your application then has to trust and parse that header correctly.

The Node Receiving Traffic Has No Local Endpoint

With Local, a node without a local backend should not forward traffic to another node. Kubernetes docs show that such traffic is dropped for NodePort, precisely to preserve client IP instead of proxying across nodes.

That means you should verify endpoint placement:

bash
kubectl get pods -o wide -l app=echo
kubectl get endpointslice -l kubernetes.io/service-name=echo -o wide

If the external load balancer still sends traffic to nodes without endpoints, the platform health-check integration may be misconfigured or unsupported.

Your Cloud Provider Does Not Preserve Source IP in That Setup

Kubernetes supports the field, but cloud-provider implementations differ. The official source-IP tutorial explicitly notes that only some providers preserve source IP for LoadBalancer Services. On platforms such as GKE, switching to Local causes nodes without endpoints to fail a health check so they are removed from the load balancer backend set.

You can inspect that behavior here:

bash
kubectl get svc echo -o yaml | grep -i healthCheckNodePort

If your provider or load balancer integration does not honor this model, the packet may still be proxied or NATed before it reaches the pod.

You Are Looking at the Wrong Address in the Application

Some applications log the peer address from a sidecar proxy, not from the original request. Others run behind service meshes or HTTP proxies that normalize headers. In those cases the "source IP" in your logs may be the sidecar, the node, or the internal load balancer hop.

So separate these questions:

  • what address does the TCP connection peer show
  • what address does the forwarded header show
  • which one does the application actually trust

Without that distinction, debugging becomes guesswork.

A Practical Debugging Workflow

Use a simple echo service first, before debugging your real app:

yaml
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4  name: echo
5spec:
6  replicas: 2
7  selector:
8    matchLabels:
9      app: echo
10  template:
11    metadata:
12      labels:
13        app: echo
14    spec:
15      containers:
16        - name: echo
17          image: registry.k8s.io/echoserver:1.10
18          ports:
19            - containerPort: 8080

Then verify:

bash
kubectl get svc echo -o yaml
kubectl get pods -o wide -l app=echo
kubectl logs deploy/echo

If the echo service shows the client IP correctly but your real app does not, the issue is in your ingress, sidecar, or application layer. If the echo service still shows an internal address, focus on the Service path and provider integration.

What Local Does Not Guarantee

It does not guarantee even traffic distribution. In fact, it can reduce distribution because only nodes with local endpoints should receive traffic.

It also does not preserve source IP through arbitrary reverse proxies, nor does it apply to traffic that never reaches the Service directly.

Finally, it does not fix application code that ignores forwarded headers when a proxy is intentionally in front of the pod.

Common Pitfalls

The biggest pitfall is assuming externalTrafficPolicy: Local affects ingress controllers and HTTP proxies. It only changes Service routing behavior.

Another is forgetting that nodes without local endpoints should be excluded. If traffic still reaches them, investigate health checks and provider support.

Teams also debug using a full application stack too early. Start with an echo server so you can isolate network behavior from application behavior.

Finally, do not assume the logged address is the true client address unless you know whether a proxy or service mesh sits in front of the app.

Summary

  • 'externalTrafficPolicy: Local preserves client IP only for supported Service traffic paths.'
  • If you still see an internal IP, a proxy, load balancer, or unsupported provider path is usually involved.
  • Verify local endpoints, load balancer health checks, and provider support first.
  • Use an echo service to separate Kubernetes networking from application-layer logging.
  • When an L7 proxy is in the path, the real client address may live in forwarded headers rather than the socket peer.

Course illustration
Course illustration

All Rights Reserved.