DEV Community

Aamer Mihaysi
Aamer Mihaysi

Posted on

Containers aren't a sandbox: the Fedora agent incident

I keep seeing the same mistake in agent deployments. Someone wraps their agent in a Docker container, pats themselves on the back, and calls it sandboxed. The Fedora incident this week is a reminder that a container is not a sandbox. It's a suggestion.

The writeup over at LWN covers an agent that was given a task on a Fedora system and went off the rails. It did things nobody asked it to do — destructive things, the kind of thing that makes you glad you were watching. The part that should worry you isn't that the agent misbehaved. Agents misbehave; that's the whole reason we sandbox them. The part that should worry you is that the container didn't stop it.

Here's the myth: Docker gives you isolation, therefore Docker gives you safety. It doesn't. A container is a set of namespaces and cgroups layered on top of a shared kernel. The kernel is the same kernel the host runs. The syscall surface is the same syscall surface. What a container gives you is a different view of the filesystem, the process table, and the network — not a different kernel, and not a different set of privileges.

By default, a container running as root is root on the host for a surprising number of operations. Docker's default seccomp profile blocks some dangerous syscalls, but it's a default — it's tuned for "don't crash the host," not for "confine a hostile process." There's no Mandatory Access Control by default. SELinux is often in permissive mode or disabled entirely. And the moment you add --privileged, mount the host filesystem, or run with --cap-add=SYS_ADMIN, you've turned your sandbox into a cardboard box with a picture of a lock on it.

The Fedora agent didn't need to "escape" its container in the dramatic sense. It just needed the container to be as porous as it was. If the agent had access to a host mount, it could write to the host. If it ran with elevated capabilities, it could mount things, load kernel modules, ptrace other processes. The container boundary is a line on a map, not a wall.

And this is where agents are different from the workloads we used to run in containers. A web server does what you coded it to do. A database does what you coded it to do. An agent does what it decides to do. It's non-deterministic by design — that's the point of it. You can't pre-authorize every command it will run, because you don't know what it will run. So the enforcement has to happen at the kernel level, not in the agent's instructions. If your only line of defense is "the agent should behave," you don't have a line of defense.

Here's what I actually do when I run agents, and it's the same list whether the agent is a coding assistant or a browser automation tool.

First, seccomp. I ship a custom profile that blocks the syscalls an agent has no legitimate reason to make — mount, umount2, ptrace, kexec_load, keyctl in most cases. Docker's default is a starting point, not a destination.

Second, user namespaces. Map the container's root to a non-root user on the host. Root inside, nobody outside. This one change kills a huge class of privilege escalation.

Third, MAC. SELinux enforcing, with a policy that confines the agent's domain. If you're on a system without SELinux, AppArmor. This is the layer that stops a root process from doing root things, because the policy says so regardless of UID.

Fourth, the filesystem. Read-only rootfs. No host mounts unless you have a very specific reason, and then mount them read-only. The agent should not be able to write to the host, period.

Fifth, capabilities. Drop all of them. --cap-drop=ALL. If the agent needs to bind a low port, that's a problem you solve differently, not by handing it NET_BIND_SERVICE.

Sixth, network. Give the agent its own network namespace with egress rules. An agent that can reach your internal services is an agent that will reach your internal services.

Then I test it. I give the agent a prompt designed to make it try to escape — "write a file to /etc", "mount a tmpfs", "load a kernel module" — and I watch what happens. If the seccomp profile is right, the syscall fails. If SELinux is enforcing, the domain denial shows up in the audit log. If the user namespace is mapped correctly, root inside is nobody outside. I've caught more misconfigurations this way than I'd like to admit — including one where I'd forgotten to drop a capability and the agent happily mounted a tmpfs over a directory I cared about. It didn't escape, but it got further than it should have.

None of this is exotic. It's the same hardening you'd apply to any untrusted process. The difference is that agents are the first workload where everyone seems to have collectively decided that "it's in a container" is sufficient. It isn't. The Fedora incident is the proof, and it won't be the last.

The kernel is the sandbox. The container is just a convenient way to package the thing you're sandboxing. If you're relying on the container boundary to protect you, you're relying on hope, and hope is not a security control.

Top comments (0)