Disclaimer: This article is for educational purposes and authorized security testing only. Run the offensive techniques shown here only against systems and containers you own or have explicit written permission to test. Misusing them against third-party infrastructure is illegal.
Introduction / Overview
Containers feel like virtual machines, but they are not. A Docker container is just a set of processes on the host kernel, isolated by namespaces and constrained by cgroups, capabilities, and seccomp. When those guardrails are weakened — most often by --privileged, dropped seccomp profiles, or running everything as UID 0 — a single application bug can become full host compromise.
This article walks through the kernel primitives that actually enforce container isolation, demonstrates a realistic container-escape proof of concept in a lab, and then spends equal time on the blue-team side: how to harden images and runtimes so the escape never works. If you are new to Linux internals, my notes on Linux privilege escalation basics provide useful background.
How it works / Background
Three kernel mechanisms do most of the heavy lifting:
- Linux capabilities —
CAP_SYS_ADMIN,CAP_NET_RAW,CAP_SYS_PTRACE, etc. Root inside a container does not get the full capability set by default. Docker's default bounding set drops dangerous capabilities likeCAP_SYS_ADMIN,CAP_SYS_MODULE, andCAP_SYS_TIME. - seccomp — a syscall filter (BPF) that blocks roughly 40+ dangerous syscalls (
keyctl,ptracein some configs,mount,unshare,bpf, etc.). Docker ships a default profile at/usr/share/containers/seccomp.json-style allowlists. Disabling it with--security-opt seccomp=unconfinedreopens that attack surface. - Namespaces — PID, mount, network, user, IPC, and UTS namespaces give the container its own view of the system. The user namespace is what makes rootless Docker possible: in-container UID 0 maps to an unprivileged host UID.
The danger flags are --privileged (grants all capabilities, disables seccomp/AppArmor, and exposes host devices under /dev), and bind-mounting the host filesystem or /var/run/docker.sock. These map to MITRE ATT&CK T1611 — Escape to Host.
Prerequisites / Lab setup
Use a throwaway VM. Everything below assumes Docker Engine 24+ on Linux.
# Confirm version and check the active security profile of a test container
docker version --format '{{.Server.Version}}'
docker run --rm alpine grep CapEff /proc/self/status
docker run --rm alpine grep Seccomp /proc/self/status # Seccomp: 2 = filter activeBashInspect the default capability set with capsh to see what root inside a normal container can and cannot do:
docker run --rm --cap-drop=ALL ubuntu \
bash -c 'capsh --print | head -3'BashWalkthrough / PoC
1. Demonstrate why --privileged is dangerous
A privileged container can mount the host disk and read or write any file. In a lab VM, identify the host root device and mount it from inside the container:
# Inside a privileged container — DO NOT do this in production
docker run --rm -it --privileged ubuntu bash
# (in container) host root filesystem is typically the largest non-loop disk
lsblk
mkdir /host && mount /dev/sda1 /host
ls /host/etc/shadow # full read access to the hostBashThe mount syscall succeeds because --privileged grants CAP_SYS_ADMIN and removes the seccomp filter that would normally block it. This is the canonical escape primitive.
2. The Docker socket mount
Mounting the daemon socket is functionally equivalent to giving root on the host, because you can ask the daemon to start a new privileged container for you:
docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock docker:cli sh
# inside: spawn a container that mounts the host root and chroots into it
docker run --rm -it -v /:/host alpine chroot /host shBash3. Verifying a hardened container blocks the same actions
Now run the correct way and watch the same primitives fail:
docker run --rm -it \
--cap-drop=ALL \
--security-opt no-new-privileges \
--read-only \
ubuntu bash
# (in container) the mount syscall is denied
mkdir /host && mount /dev/sda1 /host
# mount: permission denied -> CAP_SYS_ADMIN missing + seccomp activeBash4. Run rootless to remove the last bit of host trust
Rootless mode runs the entire daemon as an unprivileged user via user namespaces, so even a full breakout lands as a non-root host UID:
# Install and start the rootless daemon (Docker provides a helper script)
dockerd-rootless-setup-tool.sh install
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
docker run --rm alpine id # uid 0 in-container...
# ...but on the host this maps to your subuid range, e.g. 100000+Bash5. Scan the image before you ship it
Vulnerable base images are the most common real-world entry point. Use Trivy or Grype:
trivy image --severity HIGH,CRITICAL --ignore-unfixed myapp:latest
grype myapp:latest -o tableBashFor context, runc's CVE-2019-5736 allowed overwriting the host runc binary from inside a container, and CVE-2024-21626 (the "Leaky Vessels" WORKDIR file-descriptor leak) enabled escape during build/run — both are caught by keeping runc and Docker patched, which scanners flag.
Mermaid diagram

The diagram shows the decision chain every container syscall passes through: --privileged bypasses all of it, while capabilities, seccomp, and rootless mapping each independently shrink the blast radius.
Detection & Defense (Blue Team)
Hardening is layered. Apply as many of these as your workload allows.
Drop privileges by default. Never use --privileged unless absolutely required (e.g., some CI/build agents). Drop all capabilities and add back only what you need:
docker run \
--cap-drop=ALL --cap-add=NET_BIND_SERVICE \
--security-opt no-new-privileges \
--security-opt seccomp=default.json \
--read-only --tmpfs /tmp \
--user 1000:1000 \
myapp:latestBashRun as non-root in the image. Add a USER directive so even the default process is unprivileged:
RUN addgroup -S app && adduser -S -G app app
USER appDockerfileEnforce policy at the orchestrator. In Kubernetes, use a securityContext and Pod Security Admission restricted profile:
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefaultYAMLDetection. Watch for the tell-tale escape signals:
- Audit the daemon: alert on
docker runevents carryingPrivileged: true,/var/run/docker.sockbind mounts, orseccomp=unconfined. Falco rulesLaunch Privileged ContainerandDocker Socket Mountedcover these directly. - Use Falco or auditd to catch suspicious in-container syscalls —
mount,unshare,setns, and writes to/proc/sys/kernel/core_pattern(a known escape via cgroupsrelease_agent). - Scan continuously in CI with Trivy/Grype and gate on
CRITICALfindings; pin base images by digest (FROM alpine@sha256:...) so the scan result is reproducible.
Reduce trust. Prefer rootless Docker or Podman, enable user-namespace remapping (/etc/docker/daemon.json with "userns-remap": "default"), keep the default AppArmor/seccomp profiles enabled, and patch runc promptly. For deeper container-escape tradecraft, HackTricks' container section is the standard reference; see also my walkthrough on Kubernetes RBAC misconfigurations.
Conclusion
Docker isolation is real but conditional. The default profile is reasonably safe; the danger comes from operators who reach for --privileged, mount the socket, or disable seccomp to "make it work." Drop capabilities, keep seccomp on, run rootless and non-root, scan every image, and monitor the daemon for privileged launches. Each layer is cheap; together they turn a container escape from a single misstep into a chain an attacker is unlikely to complete.
References
- MITRE ATT&CK — T1611 Escape to Host: https://attack.mitre.org/techniques/T1611/
- Docker Engine Security docs: https://docs.docker.com/engine/security/
- Docker Rootless mode: https://docs.docker.com/engine/security/rootless/
- seccomp profiles in Docker: https://docs.docker.com/engine/security/seccomp/
- CVE-2024-21626 (Leaky Vessels / runc): https://nvd.nist.gov/vuln/detail/CVE-2024-21626
- CVE-2019-5736 (runc): https://nvd.nist.gov/vuln/detail/CVE-2019-5736
- Falco rules: https://falco.org/docs/reference/rules/default-rules/
- Trivy: https://aquasecurity.github.io/trivy/
- HackTricks — Docker security & breakout: https://book.hacktricks.xyz/linux-hardening/privilege-escalation/docker-security
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments