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 flowshubble_drop_total, gedropte packets per redenhubble_http_requests_total, HTTP requests per method en statushubble_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.
