Skip to content
Menu

Networking11 min read

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.

kube-proxy + iptables (Traditional)

Packet Path

  1. Pod sends packet to ClusterIP:port
  2. Packet enters netfilter PREROUTING chain
  3. iptables walks KUBE-SERVICES chain (O(n) — one rule per service)
  4. Matches service, jumps to KUBE-SVC-xxxx chain
  5. Probabilistic DNAT to one of the KUBE-SEP-xxxx chains (random endpoint)
  6. DNAT rewrites destination IP to backend pod IP
  7. 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.

Cilium + eBPF (Modern)

Packet Path

  1. Pod sends packet to ClusterIP:port
  2. tc-bpf program on pod's veth intercepts
  3. eBPF hash map lookup: {ClusterIP, port} -> backend list (O(1))
  4. Select backend (Maglev consistent hashing or random)
  5. DNAT in eBPF — rewrite headers directly
  6. 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.

console
console

    $ # 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

L3 Policy
Allow/deny by IP address, CIDR, pod label, namespace, service account
L4 Policy
Allow/deny by port and protocol (TCP/UDP/SCTP/ICMP)
L7 Policy (Cilium-exclusive)
Allow/deny by HTTP path/method, gRPC service/method, Kafka topic, DNS domain

L7 Policy: Cilium's Unique Strength

HTTP Policy

yaml
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

yaml
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

yaml
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

yaml
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.

IP-Based Policy (Calico/iptables)
  1. Pod app=frontend gets IP 10.244.1.5
  2. iptables rule: -s 10.244.1.5 -j ACCEPT
  3. Pod restarts, gets IP 10.244.2.12
  4. 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.

Identity-Based Policy (Cilium)
  1. Pod app=frontend assigned identity 12345
  2. eBPF policy map: identity 12345 -> ALLOW
  3. Pod restarts, gets different IP — same identity
  4. 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

  1. Pod Created

    K8s assigns labels: app=frontend, env=prod

  2. Identity Allocated

    cilium-agent hashes labels -> numeric ID (e.g. 12345)

  3. Identity Stamped

    Outgoing packets carry identity in VXLAN/Geneve header or WireGuard metadata

  4. Policy Enforced

    Receiving node's eBPF matches on identity, not IP

console
console

    $ # 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
console
console

    $ # 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.

Traditional Service Mesh (Istio/Linkerd)
App Container
Envoy Sidecar
mTLS, routing, metrics
Envoy Sidecar
(remote pod)
App Container
  • 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)
Cilium Service Mesh (eBPF)
App Container
eBPF (kernel)
mTLS, routing, metrics
App Container
  • 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

console
console

        $ # 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

console
console

        $ # 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

console
console

      $ # 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 API Server
Services, Endpoints, Policies
cilium-agent
DaemonSet, per node
eBPF Programs
XDP, tc-bpf, cgroup-bpf
eBPF Maps
Services, Policy, CT, NAT
  1. K8s Event

    Service created, NetworkPolicy applied, pod scheduled

  2. cilium-agent

    Translates K8s objects into eBPF map entries

  3. eBPF Maps Updated

    Atomic per-entry updates, no traffic interruption

  4. 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.

Solidnines — solidnines.com