· · 10 min leestijd Advanced

Cilium: eBPF networking voor Kubernetes

Beheers Cilium CNI voor Kubernetes: eBPF networking, network policies, service mesh en observability. Een praktische gids voor moderne container networking.

Bijen met badges checken pakketten bij een honingraat in een kast terwijl een trol ze buiten één voor één stempelt

TL;DR Cilium is de CNI die ik in elk cluster draai. Het vervangt kube-proxy en z’n iptables-chains door eBPF-programma’s in de kernel, dwingt network policies af op identiteit en op HTTP-paden, en Hubble laat me elke flow en elke drop zien. WireGuard tussen de pods, BGP naar de switch, geen sidecars. M’n productie-values staan onderaan.

De eerste keer dat een service niet meer resolvede in een van m’n clusters, zat ik een avond iptables-chains te lezen. Honderden regels, gegenereerd door kube-proxy, van boven naar beneden afgelopen. Het probleem heb ik nooit gevonden. Node herstart, weg.

Dat zat me meer dwars dan de storing zelf. Ik draaide iets dat ik niet kon lezen.

Daarom ben ik naar Cilium gegaan. Het gebruikt eBPF om de networking-logica de Linux-kernel in te duwen en slaat iptables helemaal over. Het is sneller, je ziet wat je verkeer doet, en een network policy is geen gokwerk meer.

Dit draai ik nu in m’n clusters. Ik begin bij het kleinste stuk dat werkt en bouw daarop verder, dus stop gerust waar je genoeg hebt.

Wat eBPF is

Begin hier, want de rest valt pas op z’n plek als dit klikt. eBPF, extended Berkeley Packet Filter, laat je gesandboxte programma’s in de Linux-kernel draaien zonder de kernel-source te patchen of een module te laden.

Kernel Space

User Space

laadt

Applicatie

Cilium Agent

eBPF Programma's

Network Stack

Kernel Hooks
(XDP, TC, Socket)

Traditionele networking leunt op iptables, een keten van regels die één voor één wordt afgelopen tot er iets matcht. Een eBPF-programma wordt gecompileerd en draait in de kernel, en het zoekt dingen op in maps in plaats van een lijst af te lopen. Het verschil zie je pas als je opschaalt.

Iptables met 10.000 services is een lineaire scan die trager wordt met elke service die erbij komt. Cilium met 10.000 services doet één map-lookup in constante tijd. Zelfde workload, heel ander gedrag onder belasting. En gaat er iets mis, dan dump ik de maps in plaats van te gokken welke van een paar honderd regels het was.

Cilium installeren

De kleinste versie die werkt. Ruil je bestaande CNI in voor Cilium en kijk of hij omhoog komt.

# Installeer Cilium CLI
CILIUM_CLI_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/cilium-cli/main/stable.txt)
curl -L --fail --remote-name-all https://github.com/cilium/cilium-cli/releases/download/${CILIUM_CLI_VERSION}/cilium-linux-amd64.tar.gz
sudo tar xzvfC cilium-linux-amd64.tar.gz /usr/local/bin
rm cilium-linux-amd64.tar.gz

# Installeer Cilium in cluster
cilium install --version 1.15.0

# Verifieer installatie
cilium status --wait

Voor een eerste blik is de CLI prima, maar iets twee keer met de hand doen, daar heb ik geen zin in. Dezelfde install als ArgoCD Application, zodat de netwerklaag in Git staat zoals de rest van het cluster:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: cilium
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://helm.cilium.io/
    chart: cilium
    targetRevision: 1.15.0
    helm:
      values: |
        kubeProxyReplacement: true
        k8sServiceHost: "10.0.0.1"
        k8sServicePort: "6443"

        hubble:
          enabled: true
          relay:
            enabled: true
          ui:
            enabled: true

        operator:
          replicas: 2

        ipam:
          mode: kubernetes

  destination:
    server: https://kubernetes.default.svc
    namespace: kube-system

kube-proxy vervangen

Dit zet ik als eerste aan na de install. Cilium kan kube-proxy volledig vervangen en Kubernetes Services in eBPF implementeren. Zo ben ik van de iptables-chains afgekomen waar deze post mee begon:

# Cilium values
kubeProxyReplacement: true
k8sServiceHost: "10.0.0.1"  # API server IP
k8sServicePort: "6443"

Wat je krijgt:

  • Geen iptables-regels voor services
  • Direct server return, wat sneller is
  • Socket-level load balancing, waarbij de keuze één keer valt bij connect() in plaats van bij elk packet
  • Maglev hashing, zodat de backend-keuze consistent blijft

Kijk of het aanstaat:

kubectl -n kube-system exec ds/cilium -- cilium status | grep KubeProxyReplacement
# KubeProxyReplacement: True [eth0 10.0.0.10 (Direct Routing)]

Network policies

Nu het stuk dat verandert hoe je over security denkt. Cilium spreekt de standaard Kubernetes NetworkPolicy, en zet daar z’n eigen CiliumNetworkPolicy bovenop met veel meer bereik. Nog nooit een policy geschreven? Begin dan met de standaardvariant.

Kubernetes NetworkPolicy

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-ingress
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 8080

CiliumNetworkPolicy, de uitgebreide

Zit dat, dan wordt de native policy pas interessant:

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

Kijk naar het rules.http-blok. Cilium dwingt daar L7-policy af, dus je staat specifieke HTTP-methods en paden toe en weigert de rest, zonder service mesh. Een gecompromitteerde frontend kan nog steeds GET /api/v1/... doen. Een admin-endpoint aanroepen dat je nooit had willen openzetten kan hij niet. Die policy kan ik lezen en je vertellen wat een gekraakte pod bereikt, en dat lukte me met iptables nooit.

DNS-based policies

Verkeer naar een externe service toestaan op naam:

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: allow-external-api
spec:
  endpointSelector:
    matchLabels:
      app: backend
  egress:
    - toFQDNs:
        - matchName: "api.stripe.com"
        - matchPattern: "*.amazonaws.com"
      toPorts:
        - ports:
            - port: "443"

Cilium onderschept de DNS-queries en past de policy aan als de IP’s achter die namen veranderen. Je schrijft de regel tegen een naam die een mens begrijpt, in plaats van een CIDR die onder je vandaan roteert.

Cluster-wide policies

Hetzelfde, maar over alle namespaces:

apiVersion: cilium.io/v2
kind: CiliumClusterwideNetworkPolicy
metadata:
  name: deny-external-by-default
spec:
  endpointSelector: {}
  egress:
    - toEntities:
        - cluster
        - host
    - toFQDNs:
        - matchPattern: "*.internal.company.com"
  egressDeny:
    - toEntities:
        - world

Dit weigert internettoegang standaard en laat alleen intern verkeer door, tenzij je bewust iets openzet. Default-deny egress voelt paranoïde, tot de dag dat een dependency die je vergeten was naar huis probeert te bellen en je hem in de logs betrapt.

Hubble, de observability-laag

Weet je nog, die iptables-avond bovenaan? Hubble is wat ik die avond had willen hebben. Het is de observability-laag van Cilium, en het laat zien wat het netwerk doet in plaats van je dat uit regelvolgorde te laten afleiden.

# Enable Hubble
hubble:
  enabled: true
  relay:
    enabled: true
  ui:
    enabled: true
  metrics:
    enabled:
      - dns
      - drop
      - tcp
      - flow
      - icmp
      - http

Hubble CLI

# Installeer Hubble CLI
export HUBBLE_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/hubble/master/stable.txt)
curl -L --fail --remote-name-all https://github.com/cilium/hubble/releases/download/$HUBBLE_VERSION/hubble-linux-amd64.tar.gz
sudo tar xzvfC hubble-linux-amd64.tar.gz /usr/local/bin

# Port-forward naar Hubble Relay
cilium hubble port-forward &

# Observeer flows
hubble observe --namespace production

# Filter op pod
hubble observe --pod production/api-xyz123

# Filter op verdict
hubble observe --verdict DROPPED

# Filter op HTTP
hubble observe --protocol http --http-status 500

Hubble UI

De visuele flow-map:

cilium hubble ui
# Opent browser naar http://localhost:12000

De UI toont:

  • de service-dependency-graph
  • verkeersflows, live
  • policy-verdicts, allowed of dropped
  • HTTP-request- en response-details

Hubble metrics

Exporteren naar Prometheus:

hubble:
  metrics:
    serviceMonitor:
      enabled: true
    enabled:
      - dns:query;ignoreAAAA
      - drop
      - tcp
      - flow
      - icmp
      - http

Bruikbare metrics:

  • hubble_flows_processed_total, totaal aantal waargenomen flows
  • hubble_drop_total, gedropte packets per reden
  • hubble_http_requests_total, HTTP-requests per method en status
  • hubble_dns_queries_total, DNS-queries per type en response

Service mesh zonder sidecars

Hier vervangt Cilium een hele categorie tooling. Je krijgt service-mesh-features zonder een sidecar-proxy naast elke pod:

# Enable L7 proxy
l7Proxy: true

# Enable mutual TLS
authentication:
  mutual:
    spiffe:
      enabled: true
      trustDomain: cluster.local

Wat dat oplevert:

  • mTLS tussen services, op SPIFFE -identities
  • L7 load balancing met retries
  • Traffic management, canary en header-based routing
  • Observability, L7-metrics en distributed tracing

Waar het mij om gaat is die ontbrekende sidecar. De eBPF-dataplane doet het allemaal, en dat scheelt het geheugen en de latency die je anders betaalt voor een Envoy naast elke pod. En het is één container minder om naar te kijken als een request zoekraakt.

# Enable BGP
bgpControlPlane:
  enabled: true

BGP voor bare metal

Self-host je op echte hardware, zoals ik, dan krijg je geen cloud-loadbalancer in je schoot geworpen. Cilium spreekt BGP, dus je eigen switch kan rechtstreeks naar pods routeren:

apiVersion: cilium.io/v2alpha1
kind: CiliumBGPPeeringPolicy
metadata:
  name: rack-bgp
spec:
  nodeSelector:
    matchLabels:
      rack: rack-1
  virtualRouters:
    - localASN: 65001
      exportPodCIDR: true
      neighbors:
        - peerAddress: "10.0.0.1/32"
          peerASN: 65000
apiVersion: v1
kind: Pod
metadata:
  annotations:
    kubernetes.io/ingress-bandwidth: "10M"
    kubernetes.io/egress-bandwidth: "10M"
spec:
  containers:
    - name: app
      image: my-app:latest

Je pods krijgen routeerbare IP’s, via BGP geadverteerd naar je netwerkapparatuur.

Bandbreedte begrenzen

Cilium kan de bandbreedte per pod begrenzen:

# WireGuard encryptie (aanbevolen)
encryption:
  enabled: true
  type: wireguard

# Of IPsec
encryption:
  enabled: true
  type: ipsec

Dit draait in eBPF in plaats van als tc-rules, dus het is goedkoper en de limiet houdt onder belasting.

Encryptie

Versleutel elke pod-to-pod-flow met één blok config:

# WireGuard encryption (recommended)
encryption:
  enabled: true
  type: wireguard

# Or IPsec
encryption:
  enabled: true
  type: ipsec

Ik draai WireGuard. Sneller, simpeler, en de kernel-module zit er al in. IPsec alleen als een compliance-certificering het bij naam eist.

Controleren:

kubectl -n kube-system exec ds/cilium -- cilium status | grep Encryption
# Encryption: Wireguard [NodeEncryption: Disabled, cilium_wg0 (Pubkey: xxx, Port: 51871, Peers: 2)]

Multi-cluster met Cluster Mesh

Meerdere Cilium-clusters aan elkaar knopen:

# Enable Cluster Mesh op elk cluster
cilium clustermesh enable

# Verbind clusters
cilium clustermesh connect --destination-context cluster2

Wat dat oplevert:

  • Global services, dezelfde service bereikbaar vanuit elk cluster
  • Cross-cluster network policies, zodat je verkeer van cluster1 naar pods in cluster2 kunt toestaan
  • Gedeelde identities, zodat één policy in elk cluster hetzelfde betekent
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: allow-from-cluster1
spec:
  endpointSelector:
    matchLabels:
      app: api
  ingress:
    - fromEndpoints:
        - matchLabels:
            io.cilium.k8s.policy.cluster: cluster1
            app: frontend

Troubleshooting

Endpoint-status

kubectl -n kube-system exec ds/cilium -- cilium endpoint list

Policy-verdicts

kubectl -n kube-system exec ds/cilium -- cilium policy get

Gedropt verkeer

hubble observe --verdict DROPPED

BPF-maps

kubectl -n kube-system exec ds/cilium -- cilium bpf lb list
kubectl -n kube-system exec ds/cilium -- cilium bpf endpoint list

Wat ik in productie draai

Hier is de hele config, de values die ik draai. Naast de kale cilium install van bovenaan is dit alles wat deze post heeft aangezet:

cilium:
  kubeProxyReplacement: true
  k8sServiceHost: "10.0.0.1"
  k8sServicePort: "6443"

  ipam:
    mode: kubernetes

  # Enable Hubble observability
  hubble:
    enabled: true
    relay:
      enabled: true
    ui:
      enabled: true
    metrics:
      enabled:
        - dns:query
        - drop
        - tcp
        - flow
        - http

  # WireGuard encryptie
  encryption:
    enabled: true
    type: wireguard

  # L7 policies
  l7Proxy: true

  # Operator HA
  operator:
    replicas: 2

  # Resource limits
  resources:
    limits:
      cpu: 1000m
      memory: 1Gi
    requests:
      cpu: 100m
      memory: 256Mi

Waarom elk stuk erin zit:

  • kube-proxy replacement, omdat het simpeler en sneller is, en het de laag weghaalt die ik niet kon lezen
  • WireGuard-encryptie, omdat pod-to-pod-verkeer versleuteld hoort te zijn en dit me vrijwel niks kost
  • Hubble-metrics, geëxporteerd naar Prometheus, zodat ik een alert krijg in plaats van een verrassing
  • L7-proxy, voor HTTP-bewuste policies zonder aparte mesh

Waarom Cilium

Eerlijk is eerlijk, de traditionele CNI-plugins werken. Calico , Flannel , Weave, ze routeren allemaal packets en zat clusters draaien er zonder morren op. M’n probleem zit in waar ze op gebouwd zijn. Iptables is een tool uit 1998, en ik zit hem met de hand te lezen zodra het misgaat.

Cilium past bij hoe ik infrastructuur wil draaien. De dataplane is eBPF in de kernel, en de maps kan ik dumpen. Policies zijn identity-based, dus ze beschrijven wát een workload is in plaats van welk IP hij vandaag toevallig heeft. Hubble laat me flows zien in plaats van dat ik ze uit regelvolgorde moet afleiden. En service mesh, BGP en encryptie komen uit één tool in plaats van vier.

Zoals ik in Sovereign Infrastructure schreef wil ik begrijpen wat ik draai, en het netwerkpad is de slechtste plek voor een black box. Cilium vangt L7-aanvallen die L3/L4-regels missen en is daarbij ook nog sneller. Maar de reden dat ik overstapte is de storing waar ik mee opende. De volgende keer dat een service niet meer resolvet, draai ik hubble observe --verdict DROPPED en lees ik het antwoord, in plaats van een node te herstarten en te hopen.