Cilium: Kubernetes Networking and Security Built on eBPF

Every Kubernetes cluster needs a CNI (Container Network Interface) plugin to wire pods together, and for years the default answer involved iptables. kube-proxy programmed Service load-balancing as iptables (later IPVS) rules. Calico and Flannel programmed pod-to-pod connectivity the same way — long chains of rules evaluated sequentially per packet. It worked, but it scaled badly: a cluster with thousands of Services and tens of thousands of pods could end up with iptables rule sets so large that a single packet might traverse thousands of rules before a decision was made, and every Service or pod change meant regenerating large chunks of that ruleset.

Cilium takes a fundamentally different approach. Instead of generating rules for a sequential rule-matching engine, it compiles network policy and service load-balancing into eBPF programs attached directly to kernel hooks — the same eBPF mechanism covered in the eBPF and bpftrace post, but used here to enforce and forward rather than just observe. The result is O(1) policy lookups instead of O(n) rule traversal, and a datapath that scales with cluster size instead of degrading as it grows.

This post connects Cilium back to three things already covered in this series — Linux network namespaces, nftables, and eBPF/XDP — and then walks through what using it day to day actually looks like: network policy, kube-proxy replacement, and the Hubble observability layer that comes with it.


The Foundation: Namespaces and veth Pairs, Still

Cilium doesn’t replace the fundamental building blocks covered in the network namespaces post — it builds directly on top of them. Every pod in a Cilium-managed cluster still gets:

  • Its own network namespace, isolating its routing table, socket table, and interface list from the host and from other pods
  • A veth pair connecting that namespace to the host — one end inside the pod’s namespace (typically eth0), the other end on the host side
  • An IP address assigned from the cluster’s pod CIDR

What changes is what happens to traffic once it crosses that veth pair. In a traditional iptables-based CNI, the host-side veth end connects to a Linux bridge, and packets are then matched against iptables FORWARD and NAT chains as they cross. In Cilium, an eBPF program is attached directly to the veth’s tc ingress and egress hooks — the same tc eBPF hook type mentioned in the eBPF post’s probe table — and that program makes the forwarding, load-balancing, and policy decision in-line, without the packet ever being evaluated against a sequential rule list.

[Pod A namespace]              [Host namespace]              [Pod B namespace]
   eth0  <---veth pair--->  eBPF @ tc hook  <---veth pair--->   eth0
   (own routing table,      (forwarding +                      (own routing table,
    own socket table)        policy decision                    own socket table)
                              happens here)

If you’ve worked through the namespaces post’s veth and bridge walkthrough, this is the same topology with the bridge’s forwarding logic replaced by an eBPF program.

Why Not Just Use nftables?

The nftables post covered nftables as the modern replacement for iptables — better rule organisation, atomic updates, and named sets instead of long chains. It’s a real improvement, and for a single host or a small fleet, it’s still the right tool. The motivation for Cilium isn’t that nftables is bad; it’s that any sequential rule-matching engine — iptables or nftables — has a structural problem at Kubernetes scale.

A Service with 500 backend pods needs 500 load-balancing rules (or a chain of equivalent complexity) evaluated for every packet to that Service’s virtual IP, on every node in the cluster, updated on every pod scale event. nftables’ named sets make this more efficient than raw iptables, but it’s still a matching structure evaluated per-packet in sequence.

Cilium’s eBPF datapath replaces sequential matching with hash-table lookups. A Service-to-backend mapping becomes a key in an eBPF map; looking up the destination for a packet to that Service’s VIP is a single hash lookup, not a chain walk. Network policy is similarly compiled to identity-based lookups (more on that below) rather than IP-based rule chains. The complexity of the ruleset stops being something the packet has to walk through.

nftablesCilium (eBPF)
Rule evaluationSequential, per-chainHash-table / map lookup
Update cost on scale-outRegenerate affected rules/setsUpdate a map entry
Policy modelIP/port basedPod identity based (labels)
Where it runsHost network namespace, every hostAttached per-veth via tc/XDP
Best fitSingle host, small fleet, host firewallingKubernetes-scale, high pod churn

They’re not mutually exclusive in practice — Cilium typically still relies on nftables/iptables for some host-level concerns depending on configuration, and understanding nftables first is exactly what makes Cilium’s design decisions make sense: it solves the specific scaling problem that a sequential rule engine runs into.

Identity-Based Network Policy

The most significant conceptual shift Cilium introduces is that network policy is enforced based on pod identity, derived from Kubernetes labels, rather than IP address. IP addresses in Kubernetes are ephemeral — a pod’s IP changes every time it’s rescheduled. An IP-based firewall rule has to be regenerated on every reschedule. Cilium instead assigns each unique set of pod labels a numeric security identity, and policy is written against labels:

apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: allow-frontend-to-api
spec:
  endpointSelector:
    matchLabels:
      app: api-server
  ingress:
    - fromEndpoints:
        - matchLabels:
            app: frontend
      toPorts:
        - ports:
            - port: "8080"
              protocol: TCP

This policy means “pods labelled app: api-server accept TCP/8080 only from pods labelled app: frontend” — regardless of which nodes either set of pods lands on, and regardless of how many times they’re rescheduled with new IPs. The eBPF program enforcing this on each node checks the security identity attached to the packet (carried in an encapsulation header or, in direct-routing mode, derived from the source), not the IP.

Layer 7 policy

Cilium can go further than L3/L4 and enforce policy on application-layer semantics, because the eBPF datapath can be paired with an in-kernel or proxy-based parser for common protocols:

apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: api-server-http-policy
spec:
  endpointSelector:
    matchLabels:
      app: api-server
  ingress:
    - fromEndpoints:
        - matchLabels:
            app: frontend
      toPorts:
        - ports:
            - port: "8080"
              protocol: TCP
          rules:
            http:
              - method: "GET"
                path: "/api/v1/.*"
              - method: "POST"
                path: "/api/v1/orders"

This permits GET requests to any /api/v1/ path and POST only to /api/v1/orders — from frontend pods specifically — and rejects everything else at L7, including requests that are valid TCP but the wrong HTTP method or path. This is policy that nftables and iptables simply cannot express; they don’t parse HTTP.

kube-proxy Replacement

By default, most Kubernetes distributions still run kube-proxy to implement Services via iptables or IPVS. Cilium can replace it entirely, handling Service load-balancing directly in eBPF:

# Cilium Helm values
kubeProxyReplacement: true
k8sServiceHost: <api-server-address>
k8sServicePort: 6443

With kube-proxy replacement enabled, a packet destined for a Service’s ClusterIP is load-balanced to a backend pod entirely within the eBPF program attached at the socket layer or the tc hook — no iptables KUBE-SERVICES chain involved at all. For a cluster with a large number of Services, this is the change that has the most visible effect on per-packet latency, because it removes the chain-walk entirely from the path.

XDP for the Highest-Performance Cases

The eBPF post introduced XDP as the hook that runs before the kernel allocates a socket buffer for a packet — the closest point to the wire available in software. Cilium uses XDP in two specific scenarios where the tc-level eBPF programs aren’t fast enough on their own:

DSR (Direct Server Return) load balancing. For Services where the response traffic is much larger than the request (a common pattern for content-heavy backends), Cilium can use XDP to handle load-balancing decisions for inbound traffic at the earliest possible point, while response traffic returns directly from the backend pod to the client rather than routing back through the original node.

DDoS / high-volume drop filtering. Cilium’s XDP-based filtering can drop unwanted traffic before it consumes any further kernel resources — the same principle as the XDP observability example in the eBPF post, except here the program drops the packet instead of just counting it. This matters specifically at the edge of a cluster, where a node-level NIC might be absorbing significantly more traffic than any single Service needs to see.

Enabling XDP acceleration requires NIC driver support for native XDP mode (not all NICs and not all cloud provider virtual NICs support it) — when it’s not available, Cilium falls back to the tc-level eBPF hooks, which cover the large majority of use cases without needing XDP at all.

Hubble: Observability That Comes Free With the Datapath

Because Cilium’s eBPF programs already see every packet’s identity, policy verdict, and L7 metadata as part of forwarding it, exposing that data for observability is mostly a matter of emitting it rather than capturing it separately. Hubble is the observability component that does this:

# Install the Hubble CLI and enable the Relay/UI
cilium hubble enable --ui

# Watch policy verdicts live, cluster-wide
hubble observe --verdict DROPPED

# Filter to a specific namespace and protocol
hubble observe --namespace production --protocol http

# Show only traffic between two specific labels
hubble observe --from-label app=frontend --to-label app=api-server

A dropped connection between two pods shows up in hubble observe with the exact policy that caused the drop, the source and destination identity, and (for L7-aware traffic) the HTTP method and path that was rejected — the equivalent of the rst-forensics classification work, but for policy-driven drops rather than RST analysis, and without needing a packet capture at all because the eBPF program already had the full context at forwarding time.

A Minimal Worked Example

Standing up a local cluster to see this in practice, using kind:

# Create a cluster without a default CNI
cat <<EOF | kind create cluster --config -
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
  disableDefaultCNI: true
EOF

# Install Cilium via Helm
helm repo add cilium https://helm.cilium.io/
helm install cilium cilium/cilium --version 1.16.0 \
  --namespace kube-system \
  --set kubeProxyReplacement=true

# Confirm the agent is healthy on every node
cilium status --wait

Apply the identity-based policy from above, then verify enforcement:

kubectl apply -f allow-frontend-to-api.yaml

# From a pod NOT labelled app=frontend, this should now fail
kubectl run test-pod --image=curlimages/curl --rm -it -- \
  curl -m 3 http://api-server.production.svc.cluster.local:8080/

# From a pod labelled app=frontend, this should succeed
kubectl run -l app=frontend test-pod --image=curlimages/curl --rm -it -- \
  curl -m 3 http://api-server.production.svc.cluster.local:8080/

hubble observe --verdict DROPPED run alongside the first command shows exactly which policy rejected it, in real time.

Connecting It Together

Cilium isn’t a separate technology bolted onto Linux networking — it’s the same primitives covered across this series, composed differently:

  • Namespaces and veth pairs still provide pod isolation; Cilium attaches eBPF programs to the veth, it doesn’t replace the veth
  • eBPF and bpftrace’s probe model is the same mechanism Cilium uses for enforcement, just with action programs that forward/drop instead of printf/count
  • XDP handles the highest-performance forwarding and filtering cases, the same hook covered for line-rate packet observability
  • nftables’ scaling limitation — sequential rule evaluation — is the specific problem Cilium’s hash-table-based eBPF maps were designed to solve

If the namespaces, nftables, and eBPF posts made sense individually, Cilium is best understood as “what happens when you point all three at the specific problem of Kubernetes-scale pod networking and policy.”

  • Linux network namespaces — the namespace and veth-pair foundation every Cilium-managed pod still sits on top of
  • eBPF and bpftrace — the underlying hook mechanism and verifier model that Cilium’s datapath programs use for enforcement rather than observation
  • nftables — the sequential-matching model Cilium’s hash-table-based eBPF maps were built to outscale