Skip to main content
  1. Posts/

Docker Group = Root: How I Upgraded Packages Without Sudo

Ethan Troy
Author
Ethan Troy
hacker & writer
I deployed vulnerable dev infrastructure with a 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.

Terminal output from a coding agent reporting a successful apt upgrade via a privileged Docker container with the host root filesystem mounted

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:

WhatValue
Daily SSH userdev (non-root)
Passwordless sudoNo
Docker group membershipYes
Docker daemonStandard install, runs as root
Docker socket/var/run/docker.sock

So dev cannot run this without a password:

sudo apt upgrade

But 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.sock

2. 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 bash

What each flag does:

  • --privileged: grants the container nearly all Linux capabilities, including CAP_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 /host inside 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 /host

Now 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 -y

Those 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 check

No 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 artifacts

Why each piece is dangerous
#

FactorWhat it actually meansRisk
Docker group membershipTalk to root daemon, spawn arbitrary containersCritical
--privilegedNear-full capabilities inside the containerHigh
Mounting /Read/write every host file (configs, keys, shadow)Critical
chroot /hostHost root filesystem becomes your working /Critical
No passwordless sudoSudo restrictions never entered the pictureHigh

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 run but cannot sudo

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:

  1. A non-root daily user
  2. Docker for builds and test runs
  3. 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 docker

Log the user out and back in (or restart the SSH session). Group membership is cached per session.

Verify:

getent group docker
id dev

2. 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 upgrade

That 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 socket
  • maint: 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 --privileged unless you can defend why it is required
  • Block or alert on bind mounts of /, /etc, /root, and docker.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}}' | jq

Look 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/bash

From inside the container, root can write to the host’s real /etc/sudoers through the mount:

echo 'mark ALL=(ALL) NOPASSWD: ALL' >> /privesc/etc/sudoers

Then the low-privileged host user exits the container and runs:

sudo bash

No 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#