AI-Jail: Security Update, Docker Goes Opt-In

July 25, 2026 · 💬 Join the Discussion
If you're lazy, click here for the TL;DR

Someone opened an important issue on the ai-jail repo this morning: issue #88, reported by @mdindoffer, titled “Sandbox escape via a docker socket passthrough (effective host root)”.

The report checked out, and the fix is already available in v1.16.0.

Update to v1.16.0

If you use ai-jail, the update is the usual drill:

# Arch Linux (AUR)
yay -Syu ai-jail-bin

# Homebrew (macOS / Linux)
brew update && brew upgrade ai-jail

# crates.io
cargo install ai-jail --force

# mise
mise cache clear && mise upgrade github:akitaonrails/ai-jail

What changed in v1.16.0

Up through v1.15.x, ai-jail mounted the Docker socket inside the jail automatically whenever /var/run/docker.sock existed on the host. Read-write. No warning, no flag, no asking for your opinion. I documented this in the README as “favors usability,” and it was true: a coding agent often needs to run docker compose to bring up a test database, and the automatic passthrough saved some configuration.

Starting with v1.16.0, the behavior flipped:

  • Socket passthrough is now off by default. It only goes in if you explicitly ask for it with the --docker flag or no_docker = false in .ai-jail.
  • When you enable it and a host socket exists, ai-jail prints a launch warning spelling out that this amounts to giving host root to the process inside the jail.
  • ai-jail status now shows Docker as disabled (default), so nobody thinks it got turned on by accident.
  • In --lockdown and browser profile modes the socket never goes in, same as before.

A behavior change, yes, and a deliberate one. If your workflow depends on Docker inside the jail (mine does, in a few projects), opting in is one line in the project’s .ai-jail:

no_docker = false

The field name is ugly (no_docker = false to turn it on, I know), but old configs keep parsing the same way, and --no-docker / no_docker = true work as before. Only the default changed. The details are in the v1.16.0 release notes.

What issue #88 showed

mdindoffer’s report is the kind every maintainer wants to get: precise summary, a repro in a few lines, a fix proposal. The gist:

ai-jail was mounting the raw Docker socket inside the sandbox, read-write. The Docker daemon runs as root on the host. So an agent inside the jail could run this:

docker run --rm -v /:/host alpine sh -c 'cat /host/etc/shadow'

And that’s the ballgame. Read any file on the host, as root. It defeats the tmpfs $HOME, --mask, --deny-path, and Landlock in one move, because the action no longer happens inside the sandbox: it happens in the daemon, which lives outside and above any namespace bwrap created. The agent doesn’t even need to escape the jail when the jail has a door straight into the engine room.

In retrospect, shipping this on by default was a design mistake. I knew the passthrough was “dangerous” in the abstract, and I wrote as much in the README. What I hadn’t internalized: dangerous like this, with this default, is a vulnerability.

mdindoffer also proposed the definitive hardening path: instead of mounting the raw socket, put a filtered proxy in front of it (in the style of wollomatic/socket-proxy) that only accepts bind mounts from paths the agent can already write to inside the jail. It’s on the radar for a future release. To close the hole now, opt-in with an explicit warning fixes the default, which is where the problem lived.

The vulnerability class: docker.sock is root

This is not the first time I’ve run into this story, and I’d bet it isn’t yours either. “Whoever has access to the Docker socket has root on the host” is one of the classics of container security, documented by Docker itself on the daemon attack surface page: only trusted users should control the daemon, because Docker lets you share any host directory with a container, with no access restriction whatsoever.

The technical reason is simple. dockerd is a daemon that runs as root and obeys commands arriving through the API on the /var/run/docker.sock Unix socket. The docker CLI is just a client of that API. When you ask for docker run -v /:/host, the daemon is the one creating the container and mounting the host’s entire filesystem inside it, with full privilege. And a process inside a container runs as uid 0 by default, which the kernel sees as real uid 0 (barring user namespace remapping, which almost nobody turns on). The math checks out: write access to the socket equals root on the host. The docker group is passwordless sudo by another name.

The demo, on your machine

If you have Docker installed and your user in the docker group, reproduce it right now. No sudo, no exploiting any bug:

# confirm you're a regular user
$ id
uid=1000(akitaonrails) gid=1000(akitaonrails) groups=...,docker

# try reading /etc/shadow directly: denied, as expected
$ cat /etc/shadow
cat: /etc/shadow: Permission denied

# now ask the daemon to do it for you
$ docker run --rm -v /:/host alpine sh -c 'head -3 /host/etc/shadow'
root:$6$...:...
bin:!:...
daemon:!:...

And if you want the whole nine yards, a root shell on your own host:

$ docker run --rm -it -v /:/host alpine chroot /host /bin/bash
# id
uid=0(root) gid=0(root) groups=0(root)

No exploit, no 0-day. You used the official API, the documented way, and went from regular user to root in one command. That’s exactly what an agent inside ai-jail could do until today, even with every layer (bwrap, Landlock, seccomp, rlimits) turned on.

Why does this still exist?

If everyone has known about this for over a decade, why hasn’t anyone “fixed” it? Short answer: this is architecture, and architecture doesn’t get patched.

  • The root daemon with an all-powerful API was the design that made Docker simple to operate. Granular per-request authorization exists in the form of authorization plugins, but it’s opt-in, annoying to configure, and I can count on one hand the setups I’ve seen using it.
  • userns-remap has been around since Docker 1.10 and maps container root to an unprivileged user on the host. It ships turned off, because it breaks compatibility with images and volumes that assume uid 0.
  • Rootless mode runs the whole daemon as your user, with the socket at $XDG_RUNTIME_DIR/docker.sock. It works, but it has network and storage limitations, and the entire internet of tutorials assumes the root daemon at the classic path.
  • Podman was born rootless and daemonless precisely because of this criticism. There’s a whole section about it below.

Bottom line: this is here to stay. Every tool that mounts /var/run/docker.sock into an environment “for convenience” opens the same hole, knowingly or not. CI mounting the socket for image builds, remote IDEs, code-server, AI agent sandboxes (hi, me), web dashboard plugins. The fix always lives on the side of whoever builds the environment.

Fun fact: this is anything but new

If you’ve been following me for a while, this whole story should give you déjà vu. Back in 2023 I recorded [Akitando #139] - Understanding How Containers Work, where I explain what a container actually is: an ordinary Linux process, throttled by cgroups, fooled by namespaces, with its capabilities trimmed. No magic and no virtual machine. And look at what was already sitting in that episode’s link list: a walkthrough of privilege escalation via Docker, demonstrating the exact docker run -v /:/host trick. The hole issue #88 exploited inside ai-jail is the same one I was already pointing at in a 2023 video, and it was old news long before that.

Nobody knows this better than Red Hat. Podman was born there in 2018 as a direct answer to that architecture: no central daemon, rootless by default. Every container becomes a direct child of your user, via fork-exec, with no all-powerful process running as root brokering anything. The whole “docker.sock is root” problem doesn’t exist in that model, because there’s no docker.sock, no daemon, and no root.

And before you ask: yes, I run Podman for some things and I recommend it. But it’s no perfect solution either, and it’s worth understanding why.

What Podman does well:

  • The right architecture. Daemonless and rootless from day zero. The entire vulnerability class from this article stops making sense.
  • CLI compatibility. alias docker=podman covers the overwhelming majority of day-to-day commands. Build, run, push, pull: same commands, same flags.
  • systemd integration. Quadlets are, in my opinion, the cleanest way to run a container as a service on Linux. Docker never came close.

Where it stumbles:

  • The compatibility socket isn’t 100%. Podman offers a socket compatible with the Docker API (podman.socket), and plenty of tools work on top of it. But “plenty” isn’t “all”: Testcontainers, some IDE plugins, more exotic CI tooling trip over behavioral differences. It works until the day it doesn’t, and then you lose an afternoon debugging.
  • Compose is a second-class citizen. The official docker compose does talk to the Podman socket, and podman-compose exists, but neither has the polish of the original pairing. Projects with a complicated compose file are where migrations usually get stuck.
  • Rootless has a price. Rootless networking (slirp4netns, and pasta these days) has limits: no ping by default, weird source IPs, lower throughput. Images that assume uid 0 and volumes with wrong permissions need fiddling. Nothing fatal, but it’s friction.
  • Docker Desktop is a product. On macOS and Windows, Docker Desktop delivers a polished experience that Podman Desktop is still catching up to. For a lot of people, that’s the only contact with containers they’ll ever have.

Add it all up and you get the answer to why the world stays on Docker: ecosystem inertia. Every tutorial, every CI pipeline, every example image, every docker run pasted from Stack Overflow assumes the root daemon at the classic path. Docker became the name of the category, the Kleenex of containers. Moving to Podman is technically easy and politically expensive: it’s you against the accumulated knowledge of the entire internet.

One detail that matters for sandbox users: mounting the rootless Podman socket inside a jail is far less catastrophic than mounting the Docker one. The equivalent “daemon” runs as your user, so a malicious agent would gain your privileges, not root. Still bad (it could overwrite your ~/.ssh, for one), but a whole different ballgame. Even so, ai-jail’s default stands: don’t mount any socket, from any runtime. Opt-in is opt-in.

Best practices so this doesn’t bite you

The list I apply and recommend:

  1. Treat the docker group as passwordless sudo. Before adding any user or service to it, ask whether you’d give that thing unrestricted sudo. Same thing.
  2. Never mount /var/run/docker.sock into untrusted environments. AI agents, CI jobs running code from a stranger’s pull request, third-party containers. Run grep -r docker.sock over your docker-compose files, CI manifests, and tool configs. It shows up in more places than you remember.
  3. Need to expose it to something semi-trusted? Use a socket proxy. wollomatic/socket-proxy and Tecnativa’s docker-socket-proxy sit between the client and the daemon with an endpoint allowlist and blocking of arbitrary bind mounts. Read-only by default; you enable only what you need.
  4. Prefer rootless whenever possible. Rootless Podman on Linux is the cleanest path; Docker’s own rootless mode is the second option. The daemon stops being root and this entire class of problem loses its bite.
  5. In CI, build images without a privileged daemon. Kaniko and Buildah build images rootless, with no socket mounted in the job at all.
  6. Can’t go rootless? Turn on userns-remap. It costs an afternoon of testing with your volumes and buys real isolation between container root and host root.
  7. In ai-jail, let the default work for you. Docker off, and --docker only in projects where you trust the workload the way you’d trust a sudo. Rule of thumb: if you wouldn’t blindly hand sudo to the agent in that directory, don’t hand it --docker either.

Conclusion

A well-built sandbox loses much of its value with a back door left open for convenience. bwrap, Landlock, seccomp, and rlimits in ai-jail keep doing their jobs, but none of them can see what happens when the process inside politely asks the host’s root daemon to mount the whole filesystem into a container. A security layer you don’t audit becomes decoration.

My public thanks to @mdindoffer: clean report, minimal repro, correct severity, and a solution proposal on top. That’s how you report a vulnerability to an open source project.

If you want the full context of how I use sandboxes day to day, I wrote about it in How Do I Protect Myself From My Agents Deleting My Stuff?. The ai-jail story is in AI Agents: Locking Down Your System and in the Rust rewrite.

Now go run the upgrade.