boxkite
← All posts
Security

Why Docker Containers Aren't Enough for Autonomous LLM Agents

"It runs in a Docker container" has been an acceptable answer to "is this isolated" for about a decade of CI runners and dev sandboxes. It stops being an acceptable answer the moment the code inside that container was written by an LLM agent a second ago, based on a prompt you don't fully control.

A container is a process, wearing a costume

A Docker container is not a virtual machine. It's a normal Linux process, given the appearance of isolation through namespaces (its own view of the filesystem, network, and process tree) and resource limits through cgroups. The kernel underneath is the same kernel every other container on that host is running on. For trusted, known workloads — your own CI jobs, your own microservices — that shared kernel is a fine trade: fast startup, high density, and the code running inside was written by your own team.

An autonomous agent inverts that trust assumption. The code executing inside the container wasn't written by your team — it was generated, possibly influenced by untrusted input (a scraped webpage, a user-supplied prompt, a tool result from somewhere else entirely), and it can decide what to run next based on what it just saw. The isolation boundary that was "good enough" for trusted CI jobs is now the only thing standing between arbitrary, LLM-generated syscalls and the host kernel every other tenant on that box shares.

Same host, same kernel

container Ayour own servicecontainer Bagent-executed codecontainer Cyour own serviceone shared host kernel
Every container on a Docker host shares one kernel. A kernel-level escape from any one of them is a host-level compromise — and a host running agent-generated code is exactly the container most likely to be handed a syscall nobody reviewed.

The specific gaps, not a vague "containers are insecure"

This isn't an argument that Docker is badly built — it's that its isolation model was designed for a threat model that doesn't match autonomous code execution. Three concrete gaps matter here:

Kernel-level escapes are a real, recurring category. Container breakouts via a shared kernel aren't hypothetical — runc (the low-level runtime underneath Docker and most container engines) has shipped and patched real host-escape CVEs before (CVE-2019-5736 being the best-known: a crafted container could overwrite the host runc binary itself). Each one gets fixed — but the category exists precisely because the isolation is namespace-and-cgroup based, not a hardware-enforced boundary.

Default capability and root posture is generous by default.A container run without hardening still frequently runs as root inside the container, with a broad default Linux capability set attached, and a writable root filesystem. None of that is malicious — it's just the path of least resistance for "get my CI job running." It's the wrong default for a workload where the next command wasn't written by a person.

Network egress is usually wide open.A default Docker network lets a container reach the internet freely. For an autonomous agent, that's a credential-exfiltration path sitting open by default, not a deliberate decision anyone made for this specific workload.

What a real pod boundary changes

boxkite's runtime mode gives every session its own Kubernetes pod, not a container sharing a host with arbitrary neighbors, and closes the three gaps above deliberately rather than by default-hardening after the fact:

Non-root execution with every Linux capability dropped, and a read-only root filesystem — so even a successful escape attempt from inside the sandboxed process has nothing to write to and no elevated capability to use. A fresh, empty network namespace on every exec call, independent of a pod-level default-deny NetworkPolicy— two boundaries, not one, so a misconfiguration in either still leaves the other standing. And pod-per-session isolation means there is no "other tenant's container" sharing the exact same kernel instance as the workload running agent-generated code, the way there would be on a shared Docker host.