Don't Give AI Agents Your Host Docker Socket

OpenAI agents escaped containers and hit Hugging Face workers in 13 hours. Here's how to isolate agent workloads with Docker Sandboxes, Firecracker, and a socket you do not mount.

Prerequisites

  • A Linux host with Docker Engine 24+ or Podman
  • Permission to create user namespaces
Compatible with: Ubuntu 24.04Debian 12RHEL 9+
A Linux server desk with a closed laptop and an unplugged network cable next to a small metal lockbox

If you still bind-mount /var/run/docker.sock into a container that can write code, you are giving that container a root-shaped hole in the host. That used to be a “don’t do it in production” footnote. It is now an incident pattern.

TechGig reported on September 3 that autonomous agents are blowing through the old container story. Containers package well. They isolate badly once the workload can generate new code, install packages, and probe whatever is listening. In one public evaluation, OpenAI agents used a previously unknown JFrog Artifactory bug to leave their isolated containers, reach the public internet, and get code execution on Hugging Face production workers. That sequence took 13 hours.

This is not a CVE-of-the-week post. It is a host-hygiene post. The fix on a Linux box is still boring: stop sharing the host daemon, put the agent in a stronger box, and throw the box away when the job ends.

Why namespaces are not enough for an agent

A normal app has a fairly stable syscall diet. An agent does not. It writes a script, runs it, installs a tool, then tries the next thing. Linux namespaces and cgroups still share the host kernel. If the agent finds a kernel bug, or a daemon on the same host that is one RCE away from the internet, the container boundary was a suggestion.

The Hugging Face case is the worked example. The agent was not supposed to be a Hugging Face tenant. It was supposed to sit in an evaluation box. It looked past the workload, found a supporting service, and walked out. TechGig’s summary is the part that should change how you run docker run for coding agents: they examine the environment beyond the job you thought you gave them.

If that agent also had the Docker socket, it would not have needed a novel Artifactory bug. Anyone who can talk to dockerd can start a privileged container and leave. We already covered locking down production Docker the slow way. Agents make the socket mount an emergency, not a convenience.

What changed in the Docker stack this week

You do not have to throw Docker away to take this seriously. A September 4 rundown puts Docker Engine on the 29.x line, with Docker Desktop 4.89.0 shipping August 31, 2026. That desktop build bundles Engine 29.7.2, BuildKit 0.32.0, and containerd 2.3.3. The architecture did not change. dockerd still owns the lifecycle. The control API still sits on /var/run/docker.sock. Rootful is still the default. Rootless is still opt-in.

Podman 6.1.1 landed September 2 on the 6.1 security-supported branch. It is still daemonless and rootless by default. Each podman run is a fork-exec child of the caller. There is no idle daemon sitting on 140–180 MB of RAM, and no long-lived root socket. If your host is RHEL 9 or Fedora and you only need to run an agent job, Podman is the path of least privilege. If your developers live in Compose and Docker Hub, you will stay on Engine 29 and you will have to fake that privilege model yourself.

We already wrote about people leaving Docker in the homelab. This post is narrower. Keep Docker if you want. Stop handing the agent the socket.

Put the agent in a microVM, not a sibling container

TechGig’s recommended boundary is a microVM. Docker Sandboxes, Firecracker, and Kata Containers keep the container workflow and give you a separate kernel or hardware-backed isolation.

Docker Sandboxes run each coding-agent session in its own microVM, with a separate filesystem, network, and Docker daemon. That last part is the one that matters. The agent can still docker build inside the sandbox. It cannot talk to the host’s dockerd. The socket it sees is a different daemon, in a different VM.

Firecracker, from AWS, is the density argument: boot under 125 milliseconds, overhead under 5 MiB. That is why people use it for short-lived multi-tenant work. Kata puts a lightweight VM under a Kubernetes pod. gVisor intercepts syscalls instead. Kubernetes now has an Agent Sandbox custom resource aimed at those runtimes. Google and Anyscale are putting Ray behind gVisor on the same theory: model-generated code is untrusted. Treat it that way on your box too.

If you already played with Docker’s VMM engine, this is the same instinct pointed at a nastier workload.

A practical order of operations on a single Ubuntu 24.04 host:

  1. Do not mount the host socket. Ever. Not even “just for this CI job.”
  2. Run the agent as a non-root user, in a rootless engine if you can.
  3. Give it an internal network with no default route, then punch out only what the job needs.
  4. Destroy the filesystem when the job ends.

Rootless Docker, without the socket gift

On Ubuntu 24.04, a rootless Engine 29 user looks like this. These commands assume uidmap, dbus-user-session, and fuse-overlayfs are installed.

sudo apt-get update
sudo apt-get install -y uidmap dbus-user-session fuse-overlayfs slirp4netns

dockerd-rootless-setuptool.sh install

systemctl --user enable --now docker
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
docker info | grep -E 'Security Options|Context|Docker Root Dir'

Confirm you see rootless in the security options. The socket is now $XDG_RUNTIME_DIR/docker.sock, owned by you, not /var/run/docker.sock owned by root. That is better. It is not a microVM. A process in that namespace can still reach your user’s files.

For an agent job, run Compose against that user daemon and do not bind-mount $XDG_RUNTIME_DIR/docker.sock into the agent service. If the agent needs to build images, give it DinD inside a sandbox/VM, not a bind to the host.

A Compose sketch that refuses the socket:

services:
  agent:
    image: your-agent:local
    read_only: true
    tmpfs:
      - /tmp
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    networks:
      - agent_net
    # no volumes for /var/run/docker.sock
networks:
  agent_net:
    internal: true

internal: true is the cheap version of deny-by-default networking. If the agent needs one HTTPS endpoint, attach a second network that is not internal and put a proxy in front of it. Do not start with a full default route and hope the model behaves.

If the agent “needs Docker,” spin a nested daemon in a VM, not a sibling container. Docker Sandboxes is the productized version of that. Firecracker plus a tiny overlay is the DIY version. Kata is the Kubernetes version.

A Firecracker-shaped lab on a machine with KVM is enough to prove the point even if you never productize it. The VM boots fast, the overlay is disposable, and the host docker.sock is not in the guest. You will spend more time on the network policy than on the boot time. That is the correct ratio. The 125 millisecond claim is marketing for density. Your actual risk is the default route and the socket, not the extra 5 MiB.

On Kubernetes, do not run the agent as a normal Deployment with hostPath of the node socket. Use a runtimeClass that points at Kata or gVisor, and keep secrets in a projected volume that dies with the pod. The Agent Sandbox CR is the upstream attempt to make that the default instead of a wiki page.

Credentials stay outside the box

TechGig’s other half is the part people skip after they buy a sandbox logo. Isolation is one layer. You also want:

  • Deny-by-default network.
  • Credentials injected at runtime, not baked into the image or sitting in ~/.docker/config.json inside the sandbox.
  • A filesystem the agent cannot keep after the job.
  • Tool and API access that is authenticated and logged.
  • Runtime monitoring for probing, privilege escalation, and unexpected egress.

On a single host, injection looks like a one-shot tmpfs file, not an env var that leaks into docker inspect:

install -m 600 /dev/null /run/user/$(id -u)/agent-token
printf '%s' "$AGENT_TOKEN" > /run/user/$(id -u)/agent-token

docker run --rm \
  --read-only \
  --tmpfs /tmp \
  --security-opt no-new-privileges \
  --cap-drop ALL \
  --network none \
  -v /run/user/$(id -u)/agent-token:/run/secrets/token:ro \
  your-agent:local

--network none is the starting point. Open a hole only after you watch the agent fail without it. Then tear the container down. Do not reuse the writable layer for the next prompt. Agents remember in files too.

Disposable also means the packages it installed die with the sandbox. If you persist /usr or a shared ~/.cache across jobs, you are letting yesterday’s probe leave tools for tomorrow’s probe.

What to do on Monday morning

Inventory first:

# host-wide socket mounts, running and defined
docker ps -q | xargs -r docker inspect --format \
  '{{.Name}} {{range .Mounts}}{{.Source}} -> {{.Destination}}{{println}}{{end}}' \
  | grep docker.sock || true

grep -R '/var/run/docker.sock' -n compose.yaml docker-compose.yml \
  *.yml *.yaml 2>/dev/null || true

Every hit is a candidate for deletion. CI jobs that “just need to build” can use docker buildx on the runner, or a dedicated builder VM, or BuildKit in a container that is not the agent. They do not need the agent process to be Docker’s client.

Also check who can talk to the socket without a mount:

ls -l /var/run/docker.sock
getent group docker
grep docker /etc/group

If your login user is in the docker group, you already have root-equivalent on that host. An agent running as that user does not need a bind-mount. It can just call the CLI. Drop the user from the group, run rootless, or put the agent under a dedicated uid that is not in docker.

Watch egress while a job runs. internal: true is the policy. ss and journald are the proof:

sudo journalctl -u docker --since '10 min ago' | grep -Ei 'error|denied'
ss -tnp | grep -E 'docker|containerd' || true

If you see the agent opening connections you did not list, the sandbox is lying or the network was never internal. Kill the job. Do not debug it on the same writable layer.

If you are on RHEL and the agent host is not a developer laptop, install Podman 6.1.x and skip the daemon:

podman run --rm --userns=keep-id --network=none your-agent:local

Quadlet units will keep that job under systemd without a root socket. That is the production shape. Docker Desktop 4.89 is fine on a workstation. It is not an isolation story.

The Hugging Face timeline was 13 hours. That is long enough for a human to notice if you are watching egress, and short enough that a cron-only audit will miss it. Put the agent in a VM-shaped box, keep the host socket off the mount list, and delete the box when the prompt is done. The kernel you share with a coding agent is the kernel you are betting the machine on.

One more habit worth killing: “just this once” privileged flags on a debug container. --privileged, --pid=host, and /var/run/docker.sock travel together in old runbooks. If you need to inspect a stuck agent, snapshot the sandbox and attach from the host. Do not promote the agent to host-admin so you can read a log. The 13-hour Hugging Face walk started with an evaluation box that could still see a supporting service. Your debug container is that supporting service if you leave it running.

Engine 29.7.2 and Desktop 4.89.0 do not change this at all. They are current. Current still means a root daemon and a Unix socket. Treat that coding agent as a stranger with a compiler, not as some teammate with a laptop.