De eerste keer dat een service niet meer resolvede in een van mijn clusters, zat ik een avond iptables chains te lezen. Honderden regels, gegenereerd door kube-proxy, van boven naar beneden geëvalueerd. Ik heb het echte probleem nooit gevonden. Ik herstartte een node en het was weg. Dat zat me meer dwars dan de outage zelf. Ik draaide iets dat ik niet kon lezen.

Dat gevoel is waarom ik naar Cilium ben overgestapt. Het gebruikt eBPF om networking logica naar beneden te duwen, de Linux kernel in, en slaat iptables volledig over. Je krijgt betere performance, je kunt daadwerkelijk zien wat je verkeer doet, en network policies worden geen giswerk meer.

Dit draai ik nu in mijn clusters. Ik bouw het op vanaf het simpelste stuk, zodat je kunt stoppen met lezen op de diepte die je nodig hebt en alsnog iets werkends meeneemt.

Wat is eBPF?

Begin hier, want de rest valt pas op zijn plek zodra dit klikt. eBPF (extended Berkeley Packet Filter) laat je sandboxed programma’s draaien in de Linux kernel zonder kernel source code te wijzigen of een kernel module te laden.

flowchart TD
    subgraph userspace["User Space"]
        APP["Applicatie"]
        CILIUM["Cilium Agent"]
    end

    subgraph kernel["Kernel Space"]
        EBPF["eBPF Programma's"]
        NET["Network Stack"]
        HOOKS["Kernel Hooks<br/>(XDP, TC, Socket)"]
    end

    APP --> NET
    CILIUM -->|"laadt"| EBPF
    EBPF --> HOOKS
    HOOKS --> NET

Traditionele networking leunt op iptables, een keten van regels die één voor één wordt afgelopen tot er iets matcht. eBPF programma’s worden gecompileerd en draaien direct in de kernel, en ze zoeken dingen op in maps in plaats van een lijst te scannen. Dat klinkt als een klein verschil tot je het opschaalt.

Hier knelt het. Iptables met 10.000 services betekent een lineaire scan die trager wordt met elke service die je toevoegt. Cilium met 10.000 services doet een constant-time map lookup. Zelfde workload, heel ander gedrag onder belasting. En als er iets mis is, kan ik de maps direct inspecteren in plaats van te gokken welke van een paar honderd regels misging.

Cilium installeren

De minimale werkende versie: ruil je bestaande CNI in voor Cilium en check of die omhoog kwam.

# 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

De CLI is prima voor een eerste blik, maar ik draai niets twee keer met de hand. Hier is dezelfde installatie als een ArgoCD Application, zodat de netwerklaag van het cluster net als al het andere in Git leeft:

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 vervanging

Dit is de eerste laag die het waard is om aan te zetten. Cilium kan kube-proxy volledig vervangen en Kubernetes Services in eBPF implementeren, en zo ben ik van die iptables chains afgekomen waar dit hele verhaal 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 voor betere performance
  • Socket-level load balancing, waarbij de beslissing valt bij connect() in plaats van bij elk packet
  • Maglev hashing voor consistente backend selectie

Verifieer dat het werkt:

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

Network policies

Nu het deel dat echt verandert hoe je over security denkt. Cilium spreekt standaard Kubernetes NetworkPolicy, en zet daar zijn eigen CiliumNetworkPolicy bovenop met veel meer bereik. Begin met de standaardvariant als je nog nooit een policy hebt geschreven.

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 (uitgebreid)

Zodra de basis logisch is, wordt de native policy 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 hier L7 policies af, dus je kunt specifieke HTTP methods en paths toestaan en de rest weigeren, geen service mesh nodig. Een gecompromitteerde frontend kan GET /api/v1/... raken, maar kan niet ineens een admin endpoint aanroepen dat je nooit had willen blootstellen. Dat is blast radius reductie die je daadwerkelijk kunt lezen.

DNS-based policies

Sta verkeer naar externe services toe op DNS 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 werkt de policy bij wanneer de IPs achter die namen veranderen, zodat je de regel schrijft tegen een naam die een mens begrijpt in plaats van een CIDR die onder je vandaan rouleert.

Cluster-wide policies

Pas policies toe 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 expliciet iets openzet. Default-deny egress is zo’n ding dat paranoïde voelt tot de dag dat een dependency die je vergeten was naar huis probeert te bellen en je het in de logs betrapt.

Hubble: network observability

Weet je nog die iptables-avond waar ik mee opende? Hubble is het antwoord daarop. Het is Cilium’s observability laag, en het toont je wat je netwerk daadwerkelijk doet in plaats van je het te laten afleiden uit regels.

# 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

Toegang tot de visuele network flow map:

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

De UI toont:

  • Service dependency graph
  • Real-time traffic flows
  • Policy verdicts (allowed/dropped)
  • HTTP request/response details

Hubble metrics

Exporteer naar Prometheus:

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

Nuttige metrics:

  • hubble_flows_processed_total, totaal geobserveerde 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)

Dit is de geavanceerde laag, en degene waar Cilium stilletjes een hele categorie tooling vervangt. Je krijgt service mesh features zonder een sidecar proxy naast elke pod te draaien:

# Enable L7 proxy
l7Proxy: true

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

Beschikbare features:

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

Waar het me om gaat: geen sidecar containers. De eBPF dataplane regelt alles, wat de resource overhead en de latency wegneemt die je anders betaalt voor een Envoy naast elke pod. Minder bewegende delen om over na te denken als er om 2 uur ’s nachts iets stukgaat.

BGP voor bare metal

Als je op echte hardware self-host zoals ik, krijg je geen cloud load balancer in je schoot geworpen. Cilium spreekt BGP, dus je eigen netwerkapparatuur kan direct naar pods routeren:

# Enable BGP
bgpControlPlane:
  enabled: true
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

Je pods krijgen routeerbare IPs, geadverteerd via BGP naar je netwerkinfrastructuur.

Bandbreedte management

Cilium kan bandbreedte limieten afdwingen:

apiVersion: v1
kind: Pod
metadata:
  annotations:
    kubernetes.io/ingress-bandwidth: "10M"
    kubernetes.io/egress-bandwidth: "10M"
spec:
  containers:
    - name: app
      image: my-app:latest

Dit draait in eBPF in plaats van als tc rules, dus het is efficiënter en de limieten gedragen zich voorspelbaar onder belasting.

Encryptie

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

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

# Of IPsec
encryption:
  enabled: true
  type: ipsec

Ik draai WireGuard. Het is sneller en simpeler, en de kernel module zit er al in. Grijp alleen naar IPsec als je een compliance certificering hebt die het specifiek eist.

Verifieer encryptie:

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

Verbind meerdere Cilium clusters:

# Enable Cluster Mesh op elk cluster
cilium clustermesh enable

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

Features:

  • Global services, dezelfde service bereikbaar vanaf elk cluster
  • Cross-cluster network policies, zodat je verkeer kunt toestaan van cluster1 naar cluster2 pods
  • Gedeelde identities, voor consistente security policies over clusters
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

Check endpoint status

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

Check policy verdicts

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

Debug gedropt verkeer

hubble observe --verdict DROPPED

Check BPF maps

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

Mijn productie configuratie

Hier is de hele plaat, de config die ik echt draai. Vergelijk hem met de kale cilium install waar we mee begonnen en je ziet hoe ver we de stack op zijn geklommen:

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 er zit:

  • kube-proxy vervanging, omdat het simpeler en sneller is, en de laag wegneemt die ik niet kon lezen
  • WireGuard encryptie, omdat pod-to-pod verkeer versleuteld hoort te zijn en dit me bijna niets kost om aan te zetten
  • Hubble metrics, geëxporteerd naar Prometheus zodat ik alerts krijg in plaats van verrassingen
  • L7 proxy, voor HTTP-aware policies zonder er een aparte mesh bij te slepen

Waarom Cilium?

Eerlijk is eerlijk, de traditionele CNI plugins werken. Calico, Flannel, Weave, ze routen allemaal packets en genoeg clusters draaien er prima op. Mijn probleem ermee is waar ze op gebouwd zijn: iptables, een tool uit 1998 die ik met de hand zit te lezen als dingen misgaan.

Cilium past bij hoe ik infrastructuur wil draaien. De dataplane is programmeerbare eBPF in de kernel. Policies zijn identity-based, dus ze beschrijven wat een workload is in plaats van welk IP hij vandaag toevallig heeft. Het verkeer is observable via Hubble, dus ik zie flows in plaats van ze af te leiden uit regelvolgorde. En service mesh, BGP en encryptie komen allemaal uit één tool in plaats van vier.

Dat laatste weegt zwaarder dan het lijkt. Zoals ik betoogde in Sovereign Infrastructure wil ik begrijpen wat ik draai, en een black box in het netwerkpad is de vervelendste soort splinter. Cilium geeft me de zichtbaarheid die iptables nooit kon, vangt L7-aanvallen die L3/L4-regels missen, en draait er sneller bij. Dat ik kan lezen wat het doet, is de hele reden dat ik ben overgestapt.


iptables heeft decennia zijn werk gedaan. Maar ik wil een netwerklaag die ik kan inspecteren, niet een die ik herstart en waar ik dan voor bid. eBPF geeft me dat, en Cilium is hoe ik het inzet.