kube-proxy Modes
kube-proxy runs as a DaemonSet on every node. Its job: watch Services and EndpointSlices from the API server and program the local kernel to implement the ClusterIP virtual IP abstraction.
1. iptables Mode (default)
kube-proxy writes iptables NAT rules in the KUBE-SERVICES chain. When a packet is sent to a ClusterIP, netfilter intercepts it in PREROUTING and randomly rewrites the destination to one of the pod IPs.
flowchart LR
POD["Pod<br/>10.0.1.5"] -->|"dst: 10.96.45.20:80"| NF["netfilter<br/>PREROUTING chain"]
NF -->|"KUBE-SERVICES rule matches"| KCHAIN["KUBE-SVC-XXXXX chain<br/>(one per Service)"]
KCHAIN -->|"random 33%"| SEP1["KUBE-SEP-AAA<br/>DNAT --> 10.0.1.10:8080"]
KCHAIN -->|"random 33%"| SEP2["KUBE-SEP-BBB<br/>DNAT --> 10.0.2.15:8080"]
KCHAIN -->|"random 33%"| SEP3["KUBE-SEP-CCC<br/>DNAT --> 10.0.3.9:8080"]
The O(n) problem:
Every packet traverses the chain linearly until a matching rule is found. With 10,000 services and ~3 endpoints each → 30,000 iptables rules evaluated per packet. kube-proxy CPU spikes every time rules are rewritten (any endpoint change = full rewrite of affected chains).
# Count iptables rules
iptables-save | grep -c KUBE
# Inspect a service's rules
iptables-save | grep KUBE-SVC-$(kubectl get svc my-svc -o jsonpath='{..uid}' | head -c8 | tr '[:lower:]' '[:upper:]')
# Watch conntrack table size (iptables mode creates a conntrack entry per connection)
sysctl net.netfilter.nf_conntrack_count
sysctl net.netfilter.nf_conntrack_max
# If count ≈ max → new connections dropped silently
conntrack issue: Every new connection through a ClusterIP creates a conntrack entry to remember the DNAT mapping for the return path. At high connection rates, the conntrack table fills → nf_conntrack: table full, dropping packet.
2. IPVS Mode
IPVS (IP Virtual Server) is a Linux kernel module originally built for load balancers. It uses hash tables instead of linear rule chains → O(1) lookup regardless of service count.
flowchart LR
PKT["Packet<br/>dst: 10.96.45.20:80"] --> IPVS["IPVS<br/>kernel hash table"]
IPVS --> POD1["10.0.1.10:8080"]
IPVS --> POD2["10.0.2.15:8080"]
IPVS --> POD3["10.0.3.9:8080"]
Load balancing algorithms (vs iptables which is only random):
| Algorithm | Flag | When to use |
|---|---|---|
| Round Robin | rr |
Default, equal weight |
| Least Connection | lc |
Route to pod with fewest active connections |
| Destination Hash | dh |
Sticky by destination IP |
| Source Hash | sh |
Sticky by source IP (session persistence) |
| Shortest Expected Delay | sed |
Fewest connections + lowest weight |
| Never Queue | nq |
Send to idle server, else SED |
# Enable IPVS mode (edit kube-proxy ConfigMap)
kubectl edit configmap kube-proxy -n kube-system
# Set: mode: "ipvs"
# Set: ipvs.scheduler: "lc"
# Required kernel modules (load before switching)
modprobe ip_vs ip_vs_rr ip_vs_wrr ip_vs_sh nf_conntrack
# Inspect IPVS rules after enabling
ipvsadm -Ln
# TCP 10.96.45.20:80 rr
# -> 10.0.1.10:8080 Masq 1 0 0
# -> 10.0.2.15:8080 Masq 1 0 0
# Stats per service
ipvsadm -Ln --stats
# Connection count per backend
ipvsadm -Lnc
When to switch to IPVS:
-
1000 Services in the cluster
- kube-proxy CPU usage is high (> 20% sustained)
- Need connection-aware load balancing (least-connection)
- Seeing iptables rule rewrite latency during deployments
3. Cilium / eBPF — No kube-proxy
Cilium replaces kube-proxy entirely. Instead of iptables or IPVS, it uses eBPF programs loaded at socket level — load balancing happens before packets even enter the network stack.
flowchart LR
subgraph "With iptables/IPVS"
APP1["App<br/>socket"] -->|"sends to ClusterIP"| NS1["network stack<br/>iptables DNAT<br/>conntrack"] --> POD_A["Pod"]
end
subgraph "With Cilium eBPF"
APP2["App<br/>socket"] -->|"eBPF program intercepts<br/>socket.connect() call"| DIRECT["Direct to Pod IP<br/>no DNAT, no conntrack"]
DIRECT --> POD_B["Pod"]
end
How it works:
- Cilium loads eBPF programs at the tc (traffic control) ingress/egress hooks and at the socket layer (BPF_PROG_TYPE_SOCK_OPS)
- When an app calls
connect()to a ClusterIP, the socket-level eBPF program rewrites the destination to a real pod IP before the packet is constructed - No DNAT, no conntrack entry needed for the NAT mapping
Install without kube-proxy:
# Helm install with kube-proxy replacement
helm install cilium cilium/cilium \
--namespace kube-system \
--set kubeProxyReplacement=true \
--set k8sServiceHost=<API_SERVER_IP> \
--set k8sServicePort=6443
Hubble — eBPF-based observability:
# Install Hubble UI
cilium hubble enable --ui
# Observe flows in real time
hubble observe --namespace default
# See what's hitting a service
hubble observe --to-service my-svc
# Show dropped packets (NetworkPolicy violations)
hubble observe --verdict DROPPED
L7 NetworkPolicy (Cilium-specific):
# iptables can only filter L3/L4 — Cilium can filter by HTTP path, gRPC method
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
spec:
endpointSelector:
matchLabels:
app: api
ingress:
- fromEndpoints:
- matchLabels:
app: frontend
toPorts:
- ports:
- port: "8080"
rules:
http:
- method: "GET"
path: "/api/v1/.*" # only allow GET /api/v1/*
4. Comparison
| iptables | IPVS | Cilium/eBPF | |
|---|---|---|---|
| Mechanism | netfilter NAT rules | kernel LB hash table | eBPF socket-level |
| Lookup complexity | O(n) per packet | O(1) | O(1), before packet created |
| conntrack | Yes — per connection | Yes — per connection | No (socket-level rewrite) |
| LB algorithms | Random only | rr, lc, dh, sh, sed, nq | Random, Maglev consistent hash |
| NetworkPolicy L7 | No | No | Yes (HTTP, gRPC, Kafka) |
| Observability | iptables counters | ipvsadm stats | Hubble: per-flow visibility |
| Max services | ~5,000 practical | 100,000+ | 100,000+ |
| Production maturity | Very high (default) | High | High (CNCF graduated) |
| kube-proxy needed | Yes | Yes | No |
| When to use | Small clusters, simplicity | Medium-large clusters | Large scale, need L7 policy |
5. Debugging
iptables mode
# Count rules
iptables-save | wc -l
# Find service rules
iptables-save | grep KUBE-SERVICES
# Check conntrack table pressure
conntrack -S # shows inserts, found, invalid, ignore, delete stats
conntrack -L | wc -l # current entries
# Flush conntrack for a specific IP (emergency)
conntrack -D -d 10.96.45.20
IPVS mode
# Check kernel modules loaded
lsmod | grep ip_vs
# List all virtual services + backends
ipvsadm -Ln
# Connection stats
ipvsadm -Ln --stats
# Verify kube-proxy is in IPVS mode
kubectl get configmap kube-proxy -n kube-system -o yaml | grep mode
Cilium
# Overall health
cilium status
# Check kube-proxy replacement is active
cilium status | grep KubeProxyReplacement
# Endpoint status
cilium endpoint list
# Observe live traffic
hubble observe -n my-namespace --last 100
# Debug a specific pod's connectivity
cilium policy trace --src-k8s-pod my-namespace/pod-a --dst-k8s-pod my-namespace/pod-b --dport 8080