Container escape 2026: Så bryts Docker och hur du stoppar det med rootless Podman, seccomp och AppArmor
Fyra realistiska container escape-tekniker från riktiga pentester, mappade steg för steg mot rootless Podman, seccomp, AppArmor, capabilities och Cosign-signering.
Container escape innebär att en angripare bryter sig ut ur en container och får kodexekvering på hostens kärna, oftast som root. Det händer när containern körs privilegierad, delar hostens namespace, monterar farliga sökvägar eller använder en sårbar runtime som runc eller crun. Motmedlet är enkelt att beskriva men jobbigt att göra rätt: kör rootless (helst med Podman), aktivera user namespaces, applicera en strikt seccomp-profil, låst AppArmor- eller SELinux-profil, och släpp alla capabilities du inte kan bevisa att du behöver. I den här guiden går jag igenom fyra realistiska escape-tekniker jag använder på uppdrag och mappar varje steg till en konkret hardening-kontroll.
De flesta produktionsescape jag ser under 2025–2026 utnyttjar --privileged, monteringar av /var/run/docker.sock, eller CAP_SYS_ADMIN, inte kernel-0-days.
Rootless Podman med user namespaces mappar container-UID 0 till en oprivilegierad UID på hosten, vilket eliminerar hela klasser av escape-vektorer.
En default Docker-container har fortfarande cirka 40 syscalls tillgängliga som du kan blockera med en anpassad seccomp-profil utan att bryta 99 % av arbetsbelastningar.
AppArmor-profilen docker-default är svag mot mount- och ptrace-attacker; en anpassad profil per image höjer ribban markant.
runc CVE-2024-21626 (Leaky Vessels) visar att runtime själv är en attackyta. Patcha runc och crun lika snabbt som kernel.
CIS Docker Benchmark och NIST SP 800-190 är de två auktoritativa checklistorna du kan mäta din miljö mot.
Hotmodell: vad är egentligen en container escape?
En container är inte en säkerhetsgräns — den är en isolationsprimitiv byggd av namespaces, cgroups, capabilities, seccomp och LSM-hooks (AppArmor eller SELinux). Faller en av dem, faller isolationen. När jag talar om container escape menar jag att en process som startades inuti containern får läsa eller skriva utanför den, injicera kod i en host-process, eller till och med skaffa root på noden. På ett Kubernetes-cluster är det ofta samma sak som cluster admin, eftersom kubelet-token oftast ligger monterad på noden.
Vad jag i praktiken ser under interna pentester 2025–2026 är att escape sällan behöver en kernel-0-day. Det räcker med en felkonfigurerad container: --privileged=true, en volymmontering av / eller docker-socketen, hostPID: true i en Pod, eller att någon lagt CAP_SYS_ADMIN "bara för att bygga fungerade inte annars". Det är dessa banala missar hardening ska stänga först. Kernel-nivå-buggar (Dirty Pipe, runc-symlink-attacker, cgroups v1 release_agent) är grädden på moset som fångar de sista fem procent av illa konfigurerade miljöer. På managed Kubernetes lägger dessutom molnleverantören ofta nodens IMDS på 169.254.169.254 tillgänglig från containern, vilket ger tokens till hela klustret om SSRF-skydd saknas.
Fyra escape-tekniker jag använder skarpt
Följande tekniker är alla väldokumenterade offentligt och används rutinmässigt i CTF-utmaningar och kommersiella red team-uppdrag. Jag beskriver dem så att du vet exakt vad hardening ska stoppa. Ingen av dem kräver 0-days, bara felkonfiguration, vilket är precis varför de fungerar så bra.
1. Docker-socket monterad i containern
Utvecklare monterar /var/run/docker.sock i CI-runners och observability-agenter. Får jag kodexekvering i den containern startar jag helt enkelt en ny privilegierad container med hostens rootfilsystem monterad:
Två sekunder senare är jag root på hosten. Motmedel: montera aldrig docker-socketen. Använd rootless Podman eller Sysbox om du måste bygga images i CI.
2. Utnyttja CAP_SYS_ADMIN med cgroups v1 release_agent
Om containern körs med CAP_SYS_ADMIN på ett system med cgroups v1 (fortfarande vanligt på LTS-noder) kan jag skriva en release_agent-fil som körs som root på hosten när sista processen i en cgroup dör. Motmedel: kernel 5.8+ med cgroups v2 unified hierarchy, samt att aldrig ge CAP_SYS_ADMIN till applikationskod.
Sårbara runc-versioner (före 1.1.12) läcker en filbeskrivare till hostens filsystem in i containern. Genom att följa den filbeskrivaren skriver jag över /proc/self/exe på hosten. Detta är en runtime-bugg, inget en applikation kan skydda sig mot, bara patch. Se runc-advisoryn för CVE-2024-21626.
4. Missbruk av procfs och --pid=host
När containern delar hostens PID-namespace kan jag läsa /proc/<pid>/root/ för alla host-processer, extrahera secrets ur miljövariabler och skriva till /proc/<pid>/mem om jag har rätt capabilities. Motmedel: aldrig hostPID: true, aldrig --pid=host för workloads. På Kubernetes: sätt hostPID, hostIPC och hostNetwork till false i alla PodSecurityAdmission-policyer.
Rootless Podman: eliminera root helt
Den kraftigaste enskilda kontrollen jag kan rekommendera är att sluta köra container-daemon som root. Rootless Podman gör precis det: containern körs helt i user-mode, och container-UID 0 mappas till en oprivilegierad UID på hosten via /etc/subuid och /etc/subgid. Även om en angripare bryter isolationen har processen ingen host-root att stjäla.
Så här sätter du upp en rootless-vänlig host på Debian 13 eller RHEL 10:
# 1. Installera Podman och fuse-overlayfs
sudo dnf install -y podman fuse-overlayfs slirp4netns
# 2. Verifiera att subuid/subgid är allokerat för din användare
grep "$(whoami)" /etc/subuid /etc/subgid
# Förväntat: felix:100000:65536
# 3. Sätt upp systemd user lingering så containers överlever logout
sudo loginctl enable-linger $(whoami)
# 4. Kör en container helt utan root
podman run --rm -it --read-only \
--cap-drop=ALL --security-opt=no-new-privileges \
docker.io/library/alpine id
# uid=0(root) gid=0(root) inuti, men på hosten är detta UID 100000
Kombinera detta med --userns=auto så att Podman allokerar en unik UID-range per container. Det innebär att två containers inte ens ser varandras filer på delade volymer utan explicit mappning. För fördjupning i hur namespace-isolation samspelar med nätverkskontroller, se min tidigare guide om Zero Trust-arkitektur på Linux. Att kombinera rootless containers med mikrosegmentering är ett av de bästa försvaren mot lateral movement när en initial escape ändå lyckas.
Anpassade seccomp-profiler i praktiken
Linux exponerar ungefär 350 syscalls. En vanlig webbapp behöver kanske 60. Docker- och Podmans standardprofiler blockerar cirka 44 farliga syscalls (som keyctl, mount, reboot), men lämnar mycket kvar. Med en anpassad profil kan jag krympa attackytan drastiskt, och det är i det tunna landet som de flesta kernel-escape-buggar lever.
Så här bygger jag en minimal seccomp-profil för en Nginx-container med hjälp av inspelning i utvecklingsmiljö:
# 1. Spela in vilka syscalls som faktiskt används
podman run --rm --security-opt seccomp=unconfined \
--annotation run.oci.seccomp.receiver=/tmp/seccomp.json \
docker.io/library/nginx:1.27-alpine &
CID=$(podman ps -q --latest)
# Generera realistisk trafik några minuter mot containern, sedan:
podman stop $CID
# 2. Använd den genererade profilen i produktion
podman run --rm -d --security-opt seccomp=/tmp/seccomp.json \
--security-opt no-new-privileges \
docker.io/library/nginx:1.27-alpine
Vill du hellre bygga profilen manuellt är strukturen ren JSON:
SCMP_ACT_ERRNO som default betyder att blockerade syscalls returnerar EPERM istället för att döda processen, vilket gör felsökning betydligt trevligare än SCMP_ACT_KILL. För djupare eBPF-baserad övervakning av vad containers faktiskt gör i realtid, se guiden om eBPF-baserad runtime-säkerhet med Falco och Tetragon. Kombinationen "hård seccomp + eBPF-observation" är svår att slå: du blockerar det förutsägbara och detekterar det oväntade.
AppArmor och SELinux för containers
På Debian- och Ubuntu-baserade hosts är AppArmor förvalt LSM. Containers får per default profilen docker-default eller containers-default. Den är vettig men generisk och alltför tillåtande för applikationsspecifika behov. På Red Hat-familjen används SELinux med contexten container_t, som är strikare direkt out-of-the-box men samtidigt lite mer besvärligt att felsöka när något går fel.
Ladda profilen i kärnan och kör containern med den:
sudo apparmor_parser -r /etc/apparmor.d/node-app
podman run --security-opt apparmor=node-app my-node-app:latest
SELinux-etiketter för volymer
På RHEL, Rocky eller AlmaLinux måste bind-mounts få en containerkompatibel SELinux-etikett, annars nekas åtkomst med förvirrande "Permission denied" trots att DAC-permissions är öppna:
# :Z ger volymen en privat container_file_t-etikett
podman run --rm -v /data/app:/data:Z docker.io/library/postgres:17
# :z (litet z) delar en etikett mellan flera containers på samma volym
podman run --rm -v /data/shared:/data:z docker.io/library/nginx
Har du en djupare fråga om att skriva SELinux-policymoduler eller felsöka AVC-denials, kolla min praktiska SELinux-guide för RHEL 10. Där går jag igenom hur du läser ausearch-output och skriver egna policymoduler med audit2allow.
Capabilities: släpp allt, återlämna smalt
Docker och Podman ger som standard 14 av Linux 41 capabilities till varje container. De flesta workloads behöver noll. En Node.js- eller Python-webbserver som lyssnar på port över 1024 behöver ingen enda capability. Regeln: --cap-drop=ALL, lägg sedan tillbaka det du kan bevisa behövs, inte det du tror kanske behövs.
Capability
Docker-default
Rekommendation 2026
Varför
CAP_SYS_ADMIN
Nej
Aldrig
Ekvivalent med root (cgroups-, mount-, keyring-missbruk)
CAP_NET_ADMIN
Nej
Endast VPN/nätverksverktyg
Ändra rutter, brandvägg, packet capture
CAP_NET_BIND_SERVICE
Ja
Bara om port <1024 krävs
Bind till privilegierade portar
CAP_CHOWN, CAP_FOWNER
Ja
Släpp för read-only-containers
Ändra ägare av filer
CAP_SETUID, CAP_SETGID
Ja
Släpp om PID 1 inte forkar
Ge upp privilegier (men även för lokal escalation)
CAP_DAC_OVERRIDE
Ja
Släpp
Kringgår normala filpermissions
# Bra baseline för en typisk webbapp
podman run --rm -d \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--security-opt=no-new-privileges \
--read-only --tmpfs /tmp \
my-web:latest
Runtime-CVEs och patch-hygien
Även perfekt konfigurerade containers är sårbara om runtime själv har buggar. De senaste tre åren har visat att runc, crun och till och med containerd har varit angripbara. Håll koll på i alla fall:
CVE-2024-23651/23652/23653: BuildKit-relaterade escape, samma familj men vid image-bygge.
CVE-2025-9074-klassens containerd-buggar visar att runtime-lagret är en levande attackyta.
Automatisera versionskontroll som en Falco-regel eller en Ansible-playbook som körs veckovis. Ett minimalt script som fångar dålig hygien:
#!/bin/bash
# runc-version-check.sh
MIN_VERSION="1.2.5"
INSTALLED=$(runc --version | head -1 | awk '{print $3}')
if [ "$(printf '%s\n' "$MIN_VERSION" "$INSTALLED" | sort -V | head -1)" != "$MIN_VERSION" ]; then
echo "VARNING: runc $INSTALLED är äldre än $MIN_VERSION" >&2
exit 1
fi
Kör detta på varje nod i en Kubernetes-daemonset eller i en Ansible-fact som failar patch-baseline om noden är för gammal. Att patcha kernel varje månad men glömma runc är ett vanligare hål än jag skulle vilja säga.
Signera images och verifiera SBOM i pipeline
Härdningsdiskussionen slutar inte vid runtime. Om jag som angripare kan pusha en trojaniserad image till er registry spelar seccomp ingen roll — koden är redan inne. Använd Sigstore Cosign för att signera och verifiera images, och Trivy för att skanna dem i CI.
# Bygg och signera i CI
cosign generate-key-pair
cosign sign --key cosign.key ghcr.io/acme/api:1.4.2
# Skanning: fail om kritiska CVE:er hittas
trivy image --severity CRITICAL,HIGH --exit-code 1 \
--ignore-unfixed ghcr.io/acme/api:1.4.2
# Kubernetes-admission: kräv signerade images
# via policy controller (Kyverno eller Sigstore policy-controller)
kubectl apply -f - <<EOF
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
name: require-signed-images
spec:
images:
- glob: "ghcr.io/acme/*"
authorities:
- key:
data: |
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----
EOF
På Kubernetes-noden verifierar sedan admission controllern att bara Cosign-signerade images med giltig SBOM får schemaläggas. Utan detta lager är ni sårbara mot dependency confusion och typosquat-attacker i registryn, och det är det angreppssätt som stod för de största supply chain-incidenterna under 2024–2025. Kombinera signering med SBOM-generering via syft så att ni kan svara på "vilka av våra images innehåller openssl 3.0.13?" på sekunder istället för dagar.
CIS-aligerad hardening-checklista
Följande checklista är en delmängd av CIS Docker Benchmark och NIST SP 800-190, prioriterad efter vad jag oftast får resultat på under pentest. Betrakta det som ett minimum, inte ett tak:
Docker-daemon körs inte som root, eller ersatt med rootless Podman.
Containern kör med explicit --user som inte är 0.
--cap-drop=ALL och capabilities återlagda selektivt.
Anpassad seccomp-profil, inte unconfined.
AppArmor eller SELinux aktivt för containern.
--security-opt=no-new-privileges.
--read-only rootfs med tmpfs för /tmp.
Inga bind-mounts av /, /var/run/docker.sock, /proc, /sys.
Cgroups v2 unified hierarchy aktiv (kernel 5.8+).
runc/crun/containerd patchade till senaste stable inom 7 dagar efter CVE.
Alla images signerade med Cosign och verifierade vid admission.
Trivy-skanning i CI som failar bygget vid CRITICAL eller HIGH CVE.
PodSecurityAdmission (Kubernetes) sätter restricted-baseline i alla non-system namespaces.
Vanliga frågor
Kan man verkligen fly från en Docker-container?
Ja, om containern är privilegierad, delar hostens namespaces, har CAP_SYS_ADMIN eller monterar farliga sökvägar som docker-socketen. Med en sårbar runc-version är även korrekt konfigurerade containers utsatta tills patch är installerad. En hardened, rootless container med droppade capabilities och strikt seccomp är däremot mycket svår att bryta.
Är Podman säkrare än Docker?
Ja, i praktiken. Podman har ingen central daemon som körs som root, stödjer rootless helt utan extra installationssteg och integrerar direkt med systemd-user-tjänster. Docker kan konfigureras rootless men lider av äldre arkitekturval, som att docker-socketen är en klassisk escape-vektor.
Vad gör en seccomp-profil egentligen?
Den filtrerar vilka Linux-syscalls containerns processer får göra. Om processen försöker anropa en blockerad syscall returneras EPERM (eller processen dödas, beroende på policy). Det minskar kernelns attackyta drastiskt. De flesta kernel-escape-bugar kräver ovanliga syscalls som en vanlig webbserver aldrig behöver.
Är AppArmor bättre än SELinux för containers?
Inte objektivt — de löser samma problem på olika sätt. AppArmor är enklare att skriva profiler för och används som default på Ubuntu och Debian. SELinux är kraftfullare, mer granulärt och används på RHEL-familjen med typen container_t direkt out-of-the-box. Använd den LSM som din distribution redan har aktiv.
Hur hindrar man en angripare från att pusha trojaniserade images?
Signera alla images med Cosign i CI och verifiera signaturerna vid Kubernetes admission med Kyverno eller Sigstores policy-controller. Kombinera med SBOM-generering (Syft) och Trivy-skanning som failar builds vid CRITICAL CVE:er. Utan signering kan en komprometterad registry servera vad som helst.