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