Mounting the Docker Socket Is Root on Your Host. Here Is Why.
A CI job mounts /var/run/docker.sock so it can build images. An attacker who compromises that job uses the socket to start a privileged container that mounts the host root filesystem. No exploit required. This is one of the most common real-world container escapes, and the fix is to stop needing the socket at all.
Here is a setup that exists in thousands of CI pipelines right now. A build job needs to produce a container image, so someone mounts the Docker socket into the build container:
volumes:
- /var/run/docker.sock:/var/run/docker.sock # so the job can run `docker build`
It works. The job builds and pushes images. Everyone moves on. What almost nobody realizes is that this line is not "let the job build images." It is "give this container root on the host." And it requires no vulnerability, no exploit, and no clever trick to abuse. The daemon simply does what it is asked.
This is one of the most common container escapes found in real environments, precisely because it does not look like a security decision. Understanding exactly why it is equivalent to host root, and what to do instead, is one of those pieces of knowledge that quietly prevents a breach.
Why the socket is root#
The Docker socket (/var/run/docker.sock) is not a build tool. It is the control API of a daemon that runs as root. Every docker command you type is really a request to that daemon over this socket, and the daemon carries them out with its own root privilege. So the real question is not "can this container build images?" It is "what can anything that talks to the socket ask the root daemon to do?" The answer is: anything Docker can do, including start a new container that is --privileged and bind-mounts the host filesystem.
That is the whole escape. A process with access to the socket asks the daemon to create a container with the host root mounted inside it, and now it is operating on the host as root:
# From inside a container that has docker.sock mounted. No exploit; the root
# daemon is simply asked to hand over the host filesystem.
docker -H unix:///var/run/docker.sock run -v /:/host --privileged alpine \
chroot /host sh
# ...now a root shell on the HOST filesystem.
From there, reading every secret on the node, writing an SSH key, or adding a cron job is trivial. The container boundary was never crossed by force. It was walked around, because the container was handed the keys to the thing that creates containers.
The takeaway to internalize: socket access is a privilege grant equal to a root shell, and it should be treated with exactly that seriousness. "Mount the Docker socket" belongs in the same mental category as "give this job root on the box."
It is not just Docker#
The same reasoning applies to any container-runtime control socket, because they all front a root daemon. The containerd socket (/run/containerd/containerd.sock) is just as dangerous to expose as docker.sock; it is the same problem one layer down. Never mount a runtime socket into a workload container, and lock down its permissions on the node so only root and the intended service can reach it.
The neighboring foot-guns#
The socket is the most common, but the same "a convenience quietly grants host power" pattern shows up in a handful of places worth recognizing:
- Host path mounts. Bind-mounting host paths gives a container direct file access. Mounting
/is total, but even narrow mounts (a kubelet directory,/etc, a user's.ssh) are enough to steal credentials or persist. hostPIDandhostNetwork.hostPIDlets a container see and signal host processes and read their environment (including secrets).hostNetworkshares the node network, exposing internal services and, on cloud nodes, the metadata endpoint at169.254.169.254, which can hand back the node's IAM credentials.- Writable or unmasked
/procand/sys. Runtimes mask sensitive/procpaths by default for a reason. Undo that, or addCAP_SYS_ADMINwith a writable/sys, and techniques abusing the cgrouprelease_agentorcore_patterncan make the host execute an attacker-controlled program.
Each of these is a request that sounds reasonable ("mount a directory so I can read a file", "share host PID for a monitoring agent") and grants far more than it appears to.
The fix: stop needing the socket#
The CI scenario is the classic one, so it deserves the classic answer. The reason the socket gets mounted is that building an image with Docker requires talking to the root daemon. The fix is to build without a daemon and without the socket. A whole class of daemonless builders produces OCI images as an ordinary, unprivileged process and pushes them to a registry:
- Kaniko builds from a Dockerfile inside an unprivileged container and pushes to a registry. No daemon, no socket.
- BuildKit in rootless mode builds without root or a shared daemon, and keeps the modern BuildKit features (cache mounts, build secrets).
- Buildah builds OCI images daemonless, commonly fully rootless.
# Build in CI without the Docker socket: an ordinary, unprivileged pod.
# Note what is absent: no docker.sock mount, no privileged: true.
containers:
- name: build
image: gcr.io/kaniko-project/executor:latest
args:
- "--dockerfile=Dockerfile"
- "--destination=registry.example.com/app:sha-abc123"
This removes the entire escape class. There is no root daemon to command and no socket to mount, so the "start a privileged container mounting the host" path simply does not exist. You still get a standard OCI image that runs anywhere.
The mental model#
When a request crosses your desk to mount docker.sock (or the containerd socket, or a host path, or share host PID), do not evaluate it as a convenience. Translate it into what it actually grants. "Mount the Docker socket" translates to "give this container root on the host." Once that translation is automatic, the answer to "can we just mount the socket in CI?" becomes an easy no, with a ready alternative: a daemonless, rootless builder.
There is no vulnerability to patch here, and that is the point. The daemon behaves exactly as designed. The security lives entirely in not handing an untrusted workload the control interface to a root process.
The Docker socket, host mounts, host namespaces, and the daemonless build pattern that replaces them are covered in depth in Container Internals and Runtime Engineering, a course on everything beneath the container abstraction: namespaces and cgroups, the OCI image spec, runtimes, isolation and escape, secure builds, and container security at the runtime and kernel level. For the container-is-just-a-process foundation, see There Is No Container: What Actually Runs When You docker run. The cluster-level version of this blast-radius reasoning is in Tracing the Blast Radius of a Compromised Pod, and the egress control that contains the metadata-endpoint reach is in Your NetworkPolicy Controls the Front Door and Leaves the Building Through the Back.