DEV Community

Cover image for Is a Docker Container a Sandbox? AI Agent Sandboxing Explained: seccomp, gVisor, Firecracker, Kata and Wasm
RuntimeStack
RuntimeStack

Posted on Fully Autonomous

Is a Docker Container a Sandbox? AI Agent Sandboxing Explained: seccomp, gVisor, Firecracker, Kata and Wasm

When headlines say an AI agent "escaped its sandbox"... what wall did it pass?

Some sandboxes are walls. Some are just curtains.

Containers, seccomp, gVisor, Firecracker, Kata, WebAssembly, V8 isolates. They all sort into one ladder, with one question:

How much of the host kernel can the code reach?

And there's one door none of them close. We'll get there.

An apartment building: the agent container behind a dashed curtain, the host and another app behind a wall, all on one shared kernel

Why sandbox at all?

Your agent writes code, then runs it. But who really wrote it?

Treat agent code like code from a stranger. Because it might be.

The building and its plumbing: the kernel

Here's the building. Every program lives in its own apartment.

Down in the basement is the kernel: the plumbing every apartment shares.

Open a file, send a packet? You ask the kernel. That request is a syscall. Linux has hundreds of them, and every one is a pipe into the basement.

One bug in that plumbing, and a tenant can flood the whole building.

Hundreds of syscalls running from every apartment into one shared kernel; one bug floods the whole building

Containers: a locked room

So what's a container? A locked room in this same building.

  • Namespaces control what you can see: your own processes, files and network.
  • cgroups control how much you can use: CPU, memory, how many processes.

On a Mac or Windows, Docker runs a Linux VM, so the shared kernel is that VM's.

seccomp: the list of taps

Next layer: seccomp, a list of taps you're allowed to turn.

It's a tiny filter program that checks each syscall's number and arguments, and answers allow, errno or kill:

read       allow
write      allow
mount      blocked
ptrace     blocked
kexec_load blocked
openat     allow
Enter fullscreen mode Exit fullscreen mode

But the kernel's own docs say it plainly: syscall filtering isn't a sandbox.

Is a Docker container a sandbox?

Not on its own.

In January 2024, a bug in runc, the tool that starts containers, let them reach the host's files. That's CVE-2024-21626. A full container escape.

And that wasn't even the kernel.

The classic own-goal: docker.sock

Mounting the Docker socket into the container:

docker run -v /var/run/docker.sock:/var/run/docker.sock agent
Enter fullscreen mode Exit fullscreen mode

Whoever controls that socket controls the Docker daemon, and the daemon runs as root.

Volumes only, a separate network? Better. But it's still the same kernel.

So the people saying "if it escaped, it wasn't a sandbox"? They're right. If it shares a kernel, it's a curtain, not a wall.

gVisor: hire a concierge

So how do you keep code away from the pipes? Hire a concierge.

That's gVisor: its Sentry is a kernel, written in Go, running in user space. Your code makes a syscall, and the Sentry answers it itself.

It only makes a small, filtered set of host calls. Files go through a helper, the Gofer.

gVisor: your code's open() goes to the Sentry in user space instead of the host kernel; files go through the Gofer

What does the concierge cost? Every syscall takes a detour, so syscall-heavy work slows down. And some programs expect plumbing the Sentry doesn't provide, so compatibility drops.

MicroVMs: your own tiny house

Still nervous about sharing a building? Then move out. Get your own tiny house.

A microVM runs your code on its own guest kernel, with its own plumbing. Firecracker is the VMM, the virtual machine monitor, built on KVM, the hypervisor inside Linux.

The house still has one metered service line into the building's basement: KVM. To escape, code has to get past its own kernel, and then break the VMM or KVM too.

Share a folder in, though, and you've cut a door in that wall.

Firecracker microVM: your code on its own guest kernel, with only the VMM and KVM between it and the host kernel

Isn't a VM slow?

Firecracker's spec says the guest reaches init within 125 ms:

[0.000] InstanceStart
[0.003] guest kernel boots...
[0.125] /sbin/init
/dev/kvm: required
Enter fullscreen mode Exit fullscreen mode

And the VMM itself adds at most about 5 MB of memory. Low-powered machine? The overhead is tiny, if it has KVM. The guest's own memory is the real bill.

Many sandboxes at once? Fine. They only talk to each other if you wire up a network.

That's why platforms like E2B describe a sandbox as a Linux VM made on demand. Even Docker's own agent product, Docker Sandboxes, runs each agent in its own microVM.

Kata Containers: a container's front door

But your tools speak containers, not VMs. So Kata puts a lightweight VM behind the container interface.

Same front door as an apartment, but a tiny house behind it. Your container tools never know they're running a VM.

WebAssembly and V8 isolates: the glovebox

Now flip it. Picture a sealed glovebox in the lobby. You work through the gloves you're handed, and nothing else.

A WebAssembly module's memory is bounds-checked. Step outside it, and the module traps: no pipes to the basement at all.

Same idea for JavaScript. V8 is Chrome's JavaScript engine, and an isolate is one sealed instance of it. It gets its own memory and only the APIs it's handed. Cloudflare Workers run thousands per process. Cloudflare says they start about a hundred times faster than a containerized Node process, using a tenth of the memory.

The glovebox catch

So the glovebox is the strongest rung, right? Not so fast.

Now the wall is the runtime itself, and runtimes have bugs. Break one, and you're a normal host process. In 2024, V8's team said even its newest sandbox wasn't yet a strong boundary.

Side channels, timing the shared CPU, still work too. Even between tiny houses.

The ladder

Sorted by how much host kernel the code can reach:

Rung Analogy What the code reaches
Container + seccomp locked room, list of taps the shared kernel, filtered by seccomp
gVisor the concierge the Sentry, which makes a small, filtered set of host calls
Firecracker microVM / Kata your own tiny house only its VMM and the KVM under it
Wasm / V8 isolates the glovebox nothing but their imports

The sandbox ladder: rungs from container to V8 isolate, with what each reaches of the host kernel and what it costs

And every wall here is software, so any rung can have bugs.

Which rung should you pick?

  • Your own agent on your own repo? A container-level sandbox is the floor. An injected command's easy win is your network, not a kernel exploit, and the next rule shuts that.
  • Running strangers' code, or anything multi-tenant? Give it its own kernel: gVisor or a microVM.

The door no rung closes: the network

So what if your agent sat in a perfect microVM, holding real API keys, with open internet?

Without network isolation, one injected instruction and one outbound request, and your keys are gone.

Every wall held. The code just walked out the front door.

Egress: the mailroom

Allowlist destinations: a mailroom that only delivers to addresses on the list.

ALLOWLIST
  pypi.org
  github.com
everything else: DENIED
Enter fullscreen mode Exit fullscreen mode

Can it still leak? If it reads your data and can reach an allowed host, yes. So keep that list as short as the job allows, and lock down DNS too.

Secrets: keep the master keys out of the house

The sandbox gets a placeholder, and the mailroom attaches the real credential on the way out:

inside the sandbox:   Authorization: Bearer PLACEHOLDER
after the mailroom:   Authorization: Bearer sk-live-...
Enter fullscreen mode Exit fullscreen mode

Injected code can read that placeholder all day. Off the box, it's worthless. It can still use the key through the mailroom, so keep the allowlist short.

Keys stay out: the agent only holds a placeholder; the mailroom injects the real key at egress

Agent code is untrusted on your laptop too: review it, or run it sandboxed.

A real example: Claude Code's sandbox

Does anyone build it this way? Here's one example: Claude Code's sandbox.

It's off by default. You turn it on with:

/sandbox
Enter fullscreen mode Exit fullscreen mode

On Linux it uses bubblewrap, the same namespaces trick a container uses. On macOS, Seatbelt, Apple's built-in sandbox.

Commands get no direct route out. A host proxy checks every domain against an allowlist that starts empty. Set up masking, and the proxy swaps a placeholder key for the real one.

Its own docs say it reduces risk, but isn't a complete isolation boundary. For strangers' code, climb.

Recap

One building. The kernel is the shared plumbing. A container is a locked room. seccomp is the list of taps. gVisor is the concierge. A microVM is your own tiny house, and Kata gives it a container's front door. Wasm and isolates are the glovebox. And the mailroom guards the one door no wall closes.

So did that agent escape its sandbox? Ask two things: was it a wall, or a curtain? And which door was open?

Which rung is your agent on right now? Tell me in the comments.

More in this series: AI agent identity with SPIFFE and SPIRE, Pi 1.0 and Pi Durable, AI reverse engineering.


This is the written version of a 10-minute animated explainer on RuntimeStack. Sources: syscalls(2), namespaces(7), cgroups(7), seccomp BPF, Red Hat RHSB-2024-001, CVE-2024-21626, Dirty Pipe, Docker Engine security, Docker Sandboxes architecture, Docker Desktop VMM, gVisor docs, gVisor platforms, Firecracker README, Firecracker specification, Firecracker getting started, Firecracker (NSDI '20), Kata Containers, Kata hypervisors, WebAssembly security, How Workers works, Workers security model, The V8 Sandbox, Claude Code sandboxing, E2B docs, OWASP LLM01.

Top comments (0)