Cilium
eBPF-powered networking for Kubernetes
What Is Cilium?
Cilium is an eBPF-based CNI (Container Network Interface) plugin for Kubernetes. It replaces traditional iptables-based networking with eBPF programs that run directly in the kernel, providing networking, observability, and security at a fundamentally different performance tier. Cilium is a CNCF graduated project and the default CNI in Google GKE, AWS EKS (optional), and Azure AKS (optional).
Cilium's Core Components
cilium-agent
DaemonSet on every node. Compiles and loads eBPF programs, manages maps, syncs K8s state (services, endpoints, policies).
cilium-operator
Cluster-wide singleton. Handles IPAM allocation, CRD management, garbage collection of stale state.
Hubble
eBPF-powered observability. Flow logs, service maps, DNS visibility, L7 metrics — all without sidecar proxies.
How Cilium Replaces kube-proxy
In a standard Kubernetes cluster, kube-proxy watches the API server for Service/Endpoint changes and writes iptables DNAT rules to implement service load balancing. Cilium replaces this entirely with eBPF hash map lookups.
Packet Path
- Pod sends packet to
ClusterIP:port - Packet enters netfilter PREROUTING chain
- iptables walks KUBE-SERVICES chain (O(n) — one rule per service)
- Matches service, jumps to KUBE-SVC-xxxx chain
- Probabilistic DNAT to one of the KUBE-SEP-xxxx chains (random endpoint)
- DNAT rewrites destination IP to backend pod IP
- Conntrack entry created for return path
Warning
At scale: 10,000 services = ~50,000 iptables rules. Every packet traverses these linearly. Rule updates require atomic replacement of the entire chain — can take seconds with large rulesets.
Packet Path
- Pod sends packet to
ClusterIP:port - tc-bpf program on pod's veth intercepts
- eBPF hash map lookup:
{ClusterIP, port}-> backend list (O(1)) - Select backend (Maglev consistent hashing or random)
- DNAT in eBPF — rewrite headers directly
- eBPF conntrack (CT) map entry for return path
Tip
Performance is independent of cluster size. Whether you have 100 or 100,000 services, the lookup is a single hash map access. Map updates are atomic per-entry — no full-chain replacement.
$ # Verify Cilium has replaced kube-proxy
$ cilium status
KubeProxyReplacement: True
- ClusterIP: Enabled
- NodePort: Enabled
- ExternalIPs: Enabled
- LoadBalancer: Enabled
- HostPort: Enabled
$ # View eBPF service map
$ cilium bpf lb list
SERVICE ADDRESS BACKEND ADDRESS
10.96.0.1:443 10.244.0.5:6443
10.96.0.10:53 10.244.1.12:53, 10.244.2.8:53
10.96.45.200:80 10.244.1.20:8080, 10.244.3.15:8080, 10.244.2.31:8080
eBPF-Based Network Policy
Cilium enforces Kubernetes NetworkPolicy using eBPF programs instead of iptables. But Cilium goes far beyond standard K8s NetworkPolicy — it provides L7-aware policy that can filter by HTTP path, gRPC method, Kafka topic, and DNS domain.
Policy Enforcement Layers
L7 Policy: Cilium's Unique Strength
HTTP Policy
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "api-access"
spec:
endpointSelector:
matchLabels:
app: api-server
ingress:
- fromEndpoints:
- matchLabels:
app: frontend
toPorts:
- ports:
- port: "80"
protocol: TCP
rules:
http:
- method: "GET"
path: "/api/v1/products"
- method: "GET"
path: "/api/v1/health"Only GET /api/v1/products and GET /api/v1/health are allowed. POST, PUT, DELETE, or any other path is denied.
Kafka Policy
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "kafka-access"
spec:
endpointSelector:
matchLabels:
app: kafka-broker
ingress:
- fromEndpoints:
- matchLabels:
app: order-service
toPorts:
- ports:
- port: "9092"
protocol: TCP
rules:
kafka:
- apiKey: "produce"
topic: "orders"
- apiKey: "fetch"
topic: "orders"The order-service can only produce/consume from the orders topic. Access to other topics is denied.
gRPC Policy
rules:
http:
- method: "POST"
path: "/myservice.UserService/GetUser"
- method: "POST"
path: "/myservice.UserService/ListUsers"gRPC uses HTTP/2 — Cilium filters by the gRPC method path. Only GetUser and ListUsers are permitted.
DNS Policy
spec:
endpointSelector:
matchLabels:
app: backend
egress:
- toFQDNs:
- matchPattern: "*.amazonaws.com"
- matchName: "api.stripe.com"
- toEndpoints:
- matchLabels:
io.kubernetes.pod.namespace: kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"Backend can only reach AWS APIs and Stripe. DNS queries to kube-dns are explicitly allowed (required for FQDN resolution).
Note
How L7 policy works under the hood: For L3/L4 policy, Cilium uses pure eBPF. For L7 policy, Cilium transparently redirects matching traffic through an Envoy proxy (running as a DaemonSet or per-pod) via eBPF socket redirection. The proxy inspects L7 content and enforces the policy. The key insight: eBPF decides which traffic needs L7 inspection, so only matching flows take the proxy path.
Identity-Based Security
Traditional network policy works with IP addresses. In Kubernetes, pod IPs are ephemeral — they change on every restart, reschedule, or scale event. Cilium solves this by assigning numeric identities to pods based on their labels, and enforcing policy on identity rather than IP.
- Pod
app=frontendgets IP10.244.1.5 - iptables rule:
-s 10.244.1.5 -j ACCEPT - Pod restarts, gets IP
10.244.2.12 - Old rule is stale. New rule must be synced.
Warning
Race condition window: Between pod IP change and iptables rule update, policy is either too permissive (old IP not blocked) or too restrictive (new IP not allowed). At scale with frequent deployments, this window is non-trivial.
- Pod
app=frontendassigned identity12345 - eBPF policy map:
identity 12345 -> ALLOW - Pod restarts, gets different IP — same identity
- Policy still matches. No rule update needed.
Tip
Identity is derived from labels, not IPs. All pods with the same label set share the same identity. This eliminates the pod-IP-to-policy synchronization problem entirely.
How Cilium Identity Works
Pod Created
K8s assigns labels: app=frontend, env=prod
Identity Allocated
cilium-agent hashes labels -> numeric ID (e.g. 12345)
Identity Stamped
Outgoing packets carry identity in VXLAN/Geneve header or WireGuard metadata
Policy Enforced
Receiving node's eBPF matches on identity, not IP
$ # View identity assignments
$ cilium identity list
ID LABELS
12345 k8s:app=frontend k8s:env=prod
12346 k8s:app=api-server k8s:env=prod
12347 k8s:app=redis k8s:env=prod
$ # View per-endpoint eBPF policy
$ cilium bpf policy get 1234
IDENTITY DIRECTION PORT/PROTO ACTION
12345 ingress 80/TCP allow
12346 ingress 80/TCP allow
0 ingress 0/ANY deny
Hubble: eBPF-Powered Observability
Hubble is Cilium's built-in network observability layer. Because Cilium's eBPF programs already see every packet at tc-bpf and XDP hook points, Hubble can extract flow metadata without injecting sidecar proxies or tapping interfaces. Zero additional overhead for flow visibility.
What Hubble Sees
- Flow logs — source/destination pod, namespace, identity, IP, port, protocol, verdict (forwarded/dropped/redirected)
- L7 visibility — HTTP request/response (method, path, status code, latency), DNS queries/responses, Kafka operations
- Drop reasons — policy denied, conntrack invalid, TTL exceeded, etc.
- Service dependency map — which services talk to which, auto-discovered from flows
- DNS resolution — which pods resolve which domains, response IPs, TTLs
How It's Different from Sidecar Observability
- No sidecar injection — runs at the node level in eBPF, not per pod
- No extra containers — no Envoy/Linkerd proxy per pod consuming CPU/RAM
- Sees all traffic — including traffic between pods on the same node (sidecar misses this if using
localhost) - No application changes — no library instrumentation needed
- Low overhead — eBPF event ring buffer, not packet copying
$ # Watch live flows
$ hubble observe --namespace production
TIMESTAMP SOURCE DESTINATION TYPE VERDICT
Mar 31 10:23:45.123 production/frontend-7b4c production/api-server-3d2 L4/TCP FORWARDED
Mar 31 10:23:45.125 production/api-server-3d2 production/redis-5a1f L4/TCP FORWARDED
Mar 31 10:23:45.200 production/api-server-3d2 kube-system/coredns-6d4 L4/UDP FORWARDED
Mar 31 10:23:46.001 production/unknown-pod-x production/api-server-3d2 L4/TCP DROPPED (policy denied)
$ # Filter by drop verdict
$ hubble observe --verdict DROPPED --namespace production
Mar 31 10:23:46.001 production/unknown-pod-x production/api-server-3d2 DROPPED (Policy denied)
$ # HTTP-level flows
$ hubble observe --protocol http --namespace production
Mar 31 10:23:45.124 production/frontend-7b4c -> production/api-server-3d2
HTTP/1.1 GET /api/v1/products -> 200 OK (12ms)
$ # DNS flows
$ hubble observe --protocol dns
Mar 31 10:23:45.200 production/api-server-3d2 -> kube-system/coredns-6d4
DNS Query A api.stripe.com -> 104.16.0.1, 104.16.0.2 (TTL: 300)
Tip
Hubble UI provides a graphical service dependency map — a live topology view of which services communicate, with flow rates, error rates, and latency percentiles. Deploy it with cilium hubble enable --ui.
Cilium Service Mesh (Sidecar-Free)
Traditional service meshes (Istio, Linkerd) inject a sidecar proxy (usually Envoy) into every pod. This adds 2 extra containers per pod (init + sidecar), consumes significant CPU/memory, and adds latency to every request. Cilium offers a sidecar-free service mesh using eBPF.
- 2 extra containers per pod (init-container + sidecar)
- ~50-100MB RAM per sidecar Envoy instance
- ~0.5-2ms latency added per hop (iptables redirect + proxy)
- Requires sidecar injection (mutating webhook)
- Difficult to debug (traffic flows through proxy transparently)
- No sidecar containers — eBPF programs run per-node, not per-pod
- Near-zero memory overhead per pod (shared node-level eBPF programs)
- Lower latency — kernel-level processing, no userspace proxy hop for L3/L4
- No injection webhooks — works with any pod, including system pods
- Transparent — Hubble gives full visibility into eBPF-handled traffic
What Cilium's Service Mesh Handles
| Feature | Implementation | Where It Runs |
|---|---|---|
| mTLS (mutual TLS) | WireGuard-based or SPIFFE/SPIRE | Kernel (WireGuard) or per-node Envoy |
| L4 load balancing | eBPF hash maps (Maglev) | Kernel (tc-bpf / XDP) |
| L7 traffic management | Per-node Envoy proxy (not per-pod sidecar) | Userspace Envoy, invoked via eBPF redirect |
| Retries, timeouts, circuit breaking | Per-node Envoy proxy | Userspace Envoy (L7 features only) |
| Observability (metrics, traces) | Hubble (eBPF ring buffer) | Kernel |
| Canary/traffic splitting | CiliumEnvoyConfig CRD | Per-node Envoy |
Note
Cilium still uses Envoy for L7. The key difference is where it runs: one shared Envoy per node (DaemonSet) instead of one Envoy per pod (sidecar). eBPF handles all L3/L4 work natively in the kernel. Only traffic that needs L7 inspection (HTTP routing, retries, etc.) is redirected to the per-node Envoy via eBPF socket redirection. Most production traffic is L4 and never touches Envoy at all.
Why Cilium Is Winning Over Calico/iptables-Based CNIs
| Dimension | Calico (iptables mode) | Cilium (eBPF) |
|---|---|---|
| Data plane | iptables rules (O(n) per packet) | eBPF programs + hash maps (O(1)) |
| Service load balancing | Relies on kube-proxy/iptables | Native eBPF replacement for kube-proxy |
| Network policy | L3/L4 only (standard K8s NetworkPolicy) | L3/L4/L7 (HTTP, gRPC, Kafka, DNS) |
| Observability | External tools needed (Prometheus + custom exporters) | Built-in Hubble (flow logs, service maps, L7 visibility) |
| Service mesh | Requires separate mesh (Istio/Linkerd) | Built-in sidecar-free service mesh |
| Scale | Degrades with rule count (10K+ services problematic) | Constant performance regardless of scale |
| Encryption | WireGuard (separate config) | Native WireGuard integration, transparent |
| Multi-cluster | Third-party federation | ClusterMesh — native multi-cluster connectivity |
| CNCF status | CNCF project (Calico Open Source) | CNCF Graduated (strong governance and community) |
Tip
Calico also supports eBPF now. Calico added an eBPF data plane mode that can replace its iptables mode. However, Cilium was purpose-built for eBPF from the ground up, with deeper integration (L7 policy, Hubble, service mesh). Calico's eBPF mode is catching up but lacks the L7 capabilities and integrated observability.
Debugging Tools
bpftool — eBPF Inspection
$ # List all loaded eBPF programs
$ bpftool prog list
6: sched_cls name cil_from_cont tag a1b2c3 gpl
loaded_at 2024-01-15 uid 0
xlated 4096B jited 2480B memlock 8192B
map_ids 4,5,6,7
$ # Dump an eBPF map's contents
$ bpftool map dump id 4
key: 0a f4 01 05 value: 00 00 30 39
# (10.244.1.5 -> identity 12345)
$ # Show program stats
$ bpftool prog show id 6 --json | jq
bpftrace — Dynamic Tracing
$ # Trace XDP actions on eth0
$ bpftrace -e '
tracepoint:xdp:xdp_redirect {
@actions[args->act] = count();
}'
@actions[2]: 15234 # XDP_PASS
@actions[1]: 8921 # XDP_DROP
@actions[4]: 3417 # XDP_REDIRECT
$ # Histogram of packet sizes
$ bpftrace -e '
kprobe:netif_receive_skb {
@bytes = hist(((struct sk_buff *)arg0)->len);
}'
Cilium CLI — Cluster Diagnostics
$ # Overall Cilium health status
$ cilium status
/¯¯\
/¯¯\__/¯¯\ Cilium: OK
\__/¯¯\__/ Operator: OK
/¯¯\__/¯¯\ Hubble: OK
\__/¯¯\__/ ClusterMesh: disabled
\__/
KubeProxyReplacement: True
Encryption: Wireguard
$ # List endpoints and their identity/policy status
$ cilium endpoint list
ENDPOINT POLICY (ingress) POLICY (egress) IDENTITY LABELS
1234 Enabled Enabled 12345 k8s:app=frontend
1235 Enabled Enabled 12346 k8s:app=api-server
$ # Inspect eBPF CT (conntrack) table
$ cilium bpf ct list global
TCP IN 10.244.1.5:45321 -> 10.96.0.10:53 expires=300 rx_packets=2 tx_packets=2
TCP IN 10.244.1.5:52100 -> 10.244.2.8:80 expires=120 rx_packets=45 tx_packets=38
$ # Run connectivity test
$ cilium connectivity test
✅ All 46 tests (172 actions) successful, 0 tests skipped, 0 scenarios skipped.
Essential Debugging Commands Quick Reference
| Task | Command |
|---|---|
| List loaded eBPF programs | bpftool prog list |
| Dump eBPF map contents | bpftool map dump id <ID> |
| Dynamic kernel tracing | bpftrace -e 'tracepoint:... { ... }' |
| Cilium cluster health | cilium status |
| List Cilium endpoints | cilium endpoint list |
| View eBPF service table | cilium bpf lb list |
| View eBPF conntrack table | cilium bpf ct list global |
| View eBPF policy maps | cilium bpf policy get <endpoint-id> |
| Observe live flows | hubble observe --namespace <ns> |
| View dropped flows | hubble observe --verdict DROPPED |
| End-to-end connectivity test | cilium connectivity test |
| Monitor eBPF events (debug) | cilium monitor --type drop |
Cilium Architecture Summary
Node-Level Architecture
K8s Event
Service created, NetworkPolicy applied, pod scheduled
cilium-agent
Translates K8s objects into eBPF map entries
eBPF Maps Updated
Atomic per-entry updates, no traffic interruption
eBPF Programs
Read maps at packet time — new policy effective immediately
Warning
Kernel version matters. Cilium requires Linux 4.19.57+ (minimum), but many features need 5.4+ or 5.10+ (kube-proxy replacement, WireGuard, BPF LSM). Always check the Cilium system requirements for your target kernel.