Est.

Podman Rootless Containers for Sandboxed AI Code Execution

AI agents executing untrusted code need isolation beyond what traditional container runtimes offer.

Staff Writer · · 13 min read
Cover illustration for “Podman Rootless Containers for Sandboxed AI Code Execution”
Sandbox Isolation Primitives · September 8, 2026 · 13 min read · 3,021 words

AI agents now write code and run it in the same breath, no human checking the diff before execution starts. That single fact changes what "sandboxing" has to mean, and it's why Podman's rootless, daemonless design is worth examining in operational detail rather than as one more entry on a container runtime feature chart.

The setup matters because the old assumptions about trust don't hold anymore. An AI agent reading a webpage, a PDF, or a Slack message can pick up instructions embedded in that content and act on them as if a human typed them in, a failure mode security researchers call prompt injection. The code that results gets treated as though it came from a developer who knows what they're doing, when really it might be following orders smuggled in from an untrusted source three steps back in the reasoning chain. Add to that the fact that agent sessions aren't one-shot: they evolve, install packages, clone repos, spin up servers, and generally behave like a long-running process with OS-level reach, not a stateless function call. And the ceiling on what these models can pull off keeps rising. Success rates on apprentice-level cybersecurity tasks went from under 10% in late 2023 and early 2024 to somewhere around 50% in 2025, according to augmentcode.com. A sandbox designed around what a 2023 model could plausibly attempt is already behind.

So the question isn't whether to isolate AI code execution. It's what isolation actually has to guarantee, layer by layer, given a threat model that includes untrusted inputs, autonomous execution, broad OS access, and an attacker surface that's getting sharper every few months. Podman is a good place to start answering that, because its architecture makes each layer inspectable rather than hidden behind a daemon.

What makes Podman's daemonless, rootless architecture structurally different from Docker for this workload

Docker runs a background service, dockerd, and that service runs as root. Every container Docker starts, it starts by asking a root-owned process to do it on its behalf. That's a convenient design for a lot of workloads, but it also means a single compromised daemon is a skeleton key to the whole host.

Podman skips the daemon. It's a daemonless, Linux-native tool built on runc and libpod, and it produces standard OCI-compliant containers without any background service holding the keys. More importantly for this discussion, Podman containers run rootless by default: each container operates inside the UID namespace of whichever user launched it, not as root on the host. Escape a rootless Podman container and an attacker lands as an unprivileged user, not as root. That's a meaningfully smaller blast radius, and it removes an entire category of privilege escalation paths that root-daemon architectures have to defend against separately.

Podman supports flexible user-space networking options, so containers can get network access without needing elevated host privileges to set it up, and it runs on runc as the underlying OCI runtime. The project reached a stable 6.1.0 release in August 2026 and was contributed to the CNCF in January 2025 alongside Podman Desktop, placing it on a foundation-governed track rather than a single-vendor one.

Adoption still favors Docker by a wide margin. The 2025 Stack Overflow developer survey, drawing on 20,070 professional respondents, put Docker usage at 73.8% against Podman's 10.9%. But that minority is growing where it counts most for this discussion: a 500-organization survey found Podman adoption among enterprises running rootless workloads up 41% year over year, driven largely by HIPAA and PCI-DSS mandates that push toward least-privilege deployment by default. Red Hat develops and maintains Podman, which is a fairly clear signal about where the enterprise AI deployment path is heading.

How UID namespace mapping actually works and what it guarantees at the kernel level

Here's the mechanism underneath the rootless claim. Linux user namespaces let a process inside a container believe it's running as UID 0, root, while the kernel maps that UID to something unprivileged on the host. The container process can do root-like things to its own filesystem view and its own process tree, but from the host kernel's perspective, it's just another unprivileged user. No administrative capability crosses that boundary.

The kernel itself enforces that mapping, not some userspace wrapper that a clever exploit might route around. It closes off the container escape techniques that depend on the runtime actually running as host root, which is a real and well-documented category of past exploits. What it does not do is protect against a vulnerability that lives in the kernel code every container shares. UID mapping constrains privilege. It doesn't shrink the kernel's attack surface, and those are two different problems that practitioners conflate more often than they should.

One practical detail worth flagging: a script mounted into a sandbox can carry its original host SELinux label rather than the container's own context if the mount is not configured carefully. It's the first hint that UID mapping alone isn't the whole isolation story, just the floor of it.

The 2024 Enterprise Container Security Survey, covering 500 organizations, found that shops running Podman 5.0 rootless containers reported 27% fewer critical and high-severity vulnerabilities over a 12-month window compared to peers on Docker 27.0. That gap tracks pretty cleanly to UID namespace isolation eliminating a whole class of root-daemon privilege issues before they can even occur, not to some mysterious Podman advantage.

SELinux labeling as a mandatory access control layer on top of namespace isolation

Suppose, for a moment, that a piece of AI-generated code manages to escape its UID confinement anyway. What stops it next? On SELinux-enabled hosts (RHEL, Fedora, CentOS Stream), the answer is mandatory access control that doesn't care what UID a process is running as.

SELinux enforces policy independent of standard Unix permissions. A process that has somehow slipped past namespace constraints still has to get through SELinux policy, and Podman applies SELinux labels to every container automatically on hosts where SELinux is available, no extra configuration required. Each container gets its own MCS (Multi-Category Security) label, and two containers with different labels can't read each other's files even if both manage to escape their UID confinement. Their labels are simply incompatible, and the kernel enforces that incompatibility at every file access.

That :Z flag mentioned earlier does the actual relabeling work: something like -v ./script.py:/sandbox/script.py:ro,Z mounts the script read-only and stamps it with the container's own SELinux context. For an AI sandbox specifically, this is the layer that turns "harder to escape" into "actually bounded even after escape." It's the difference between hoping the walls hold and having a second, independent wall behind the first one.

One catch worth stating plainly: SELinux has to be in enforcing mode, not permissive. Permissive mode does not enforce policy, which is useful for debugging and genuinely dangerous if left on by mistake in production, because it looks like protection while doing nothing. It's a quiet misconfiguration, the kind that passes every functional test and fails the one test that matters. Teams running agent workloads on Debian or Ubuntu without SELinux at all lose this layer entirely and need to compensate with AppArmor profiles or other host-level access controls to get anything comparable.

Seccomp profiles and capability dropping: what syscalls and kernel interfaces AI sandbox containers should never reach

Root, on Linux, isn't actually one privilege. It's a bundle of discrete capabilities, things like CAP_NET_ADMIN (mess with network interfaces), CAP_SYS_PTRACE (attach a debugger to any process), CAP_SETUID (change what user a process runs as). Podman lets operators drop every single one with --cap-drop ALL, and for AI code execution, that should be the starting assumption, not an optional hardening step.

Ask what legitimate reason a piece of AI-generated code has to modify a network interface, load a kernel module, or change file ownership on the host. There isn't one. Dropping all capabilities removes those options before the code ever runs, which beats trying to catch misuse after the fact.

Pair that with --security-opt no-new-privileges, which blocks any process inside the container from picking up additional capabilities through setuid binaries or file capabilities, even if the container image happens to ship one. It closes an escalation path that capability dropping alone doesn't fully address, since a setuid binary can theoretically hand back privilege a container otherwise lost.

Then there's seccomp, which works at a different layer entirely: a BPF filter sitting at the kernel boundary that only lets through a specified list of syscalls. The kernel kills (SIGKILL) or rejects (EPERM) anything not on that list before it ever executes. Podman ships a default seccomp profile out of the box, and the sensible move for an AI sandbox is deriving a tighter, custom profile from that default rather than writing an allowlist from scratch. Hand-rolled allowlists have a habit of blocking syscalls a language runtime needs just to start up, which turns into a debugging session nobody enjoys. Apply a custom profile with --security-opt seccomp=sandbox-seccomp.json.

Put capability dropping, no-new-privileges, and a restrictive seccomp profile together, and even code that manages a container escape hits a wall: it doesn't have the privileged syscalls available to do anything meaningful on the host afterward. What none of this catches is a syscall that's technically permitted but gets used in some unexpected, malicious way, since seccomp filters on the syscall itself, not on intent. That gap is exactly where SELinux and namespace isolation pick up the slack.

The full hardened Podman invocation for AI-generated code, flag by flag

Start with the image itself: Alpine base, only the language runtime the sandbox actually needs, an unprivileged user created inside the Dockerfile (adduser -D -s /bin/sh sandbox), and USER sandbox set so nothing runs as root even before namespace mapping kicks in. No CMD baked in, since the sandbox script supplies the command at run time.

The invocation itself stacks flags, each one closing a specific door:

--network none shuts off all network access, full stop. AI-generated code has no business phoning home, exfiltrating data, or pulling down a second-stage payload, so the simplest fix is removing the network entirely rather than trying to filter it. --read-only mounts the root filesystem read-only, so the code can't touch the container's own image layers. Podman would otherwise add writable tmpfs mounts under /run, /tmp, and /var/tmp automatically, so --read-only-tmpfs=false turns that off, and --tmpfs /tmp:size= adds back a bounded scratch space instead, capped so nothing can fill the disk trying.

Resource limits come next. --memory 256m sets a hard ceiling enforced through cgroups, --cpus 0.5 caps CPU share so a runaway loop can't starve everything else on the host, and --pids-limit 50 caps the process count outright, which is the direct countermeasure to a fork bomb. Then the security flags from the prior section return: --security-opt no-new-privileges, --cap-drop ALL, and --user sandbox to make sure the process runs as the unprivileged account the image defines rather than defaulting to root inside the container. Code gets mounted in with -v ./script.py:/sandbox/script.py:ro,Z, read-only and relabeled.

Wrap the whole thing in a shell script that calls timeout around the podman run command, and a runaway process gets killed on a wall-clock limit, with exit code 124 signaling a timeout distinctly from a normal execution error, so the calling code can tell the difference. Redirect stdout and stderr to separate files in a temp directory, and read them back with a size cap to bound how much output the host actually processes, so a verbose or malicious script doesn't consume unbounded resources on the host side.

A minimal API layer around this might be a Flask endpoint that takes code as JSON, writes it to a temp file, shells out to the sandbox script as a subprocess, and returns exit code and bounded output, keeping execution entirely outside the host's own process space. Recent academic work on agent containment, the Springdrift system described in a 2026 research paper, runs this exact pattern at pool scale: a manager keeps a small pool of containers (two by default, three max), runs health checks every 30 seconds, and restarts anything that fails. The single hardened invocation and the pooled production system are the same idea, just at different scale.

Where shared-kernel isolation reaches its limits and what that means for AI workloads specifically

Diagram: Container Isolation: A Hierarchy of Strength. Visualizes: Visualize a four-level ranked stack showing increasing isolation strength for AI code execution sandboxes, as described in the article.

Every flag covered so far operates within one unavoidable constraint: all containers on a host share that host's kernel. A vulnerability in the kernel code path a container touches can, in principle, compromise every other container on that host and the host itself, and no amount of namespace mapping, SELinux labeling, or seccomp filtering changes that fact, because those controls all live inside the kernel they're trying to defend.

This isn't hypothetical. The 2023 to 2026 window produced a steady run of CVEs in runc, containerd, and the kernel itself, container escape bugs that broke the isolation boundary regardless of how carefully the container was otherwise configured. Academic research on coding agent containment states the problem plainly: attackers can escape naive containers, and even rootless Podman has known CVEs of its own. Rootless shrinks the blast radius of a successful escape. It doesn't take escape off the table.

That's produced something like a hierarchy in how researchers and practitioners think about isolation strength going into 2026. Containers, Docker or Podman alike, sit at the first level: namespace and cgroup separation, shared kernel underneath. Fine for trusted, internally written code. Not enough on its own as the sole defense for code an LLM generated, especially when that LLM might be acting on injected instructions it can't distinguish from the real ones.

One level up sits something like gVisor, a user-space kernel that intercepts and re-implements syscalls itself rather than passing them straight through to the host kernel. Stronger isolation than a plain container, less overhead than a full virtual machine, and it's the approach behind Google's Agent Sandbox on GKE. The tradeoff is syscall compatibility: reimplementing the kernel interface in userspace means some syscalls behave slightly differently or aren't supported at all, which occasionally trips up software that expects exact Linux kernel behavior.

The next level up is full hardware virtualization: microVMs like Firecracker, Kata Containers, or libkrun, where each workload gets its own kernel running on top of KVM. A kernel exploit inside one of these VMs has no path to the host or to any other VM, because there's no shared kernel to exploit in the first place. Podman's own path toward this level runs through its libkrun integration, which lets Podman use VM-level isolation while keeping the same container-image workflow developers already know. Recent multi-runtime academic research on agent containment describes exactly this kind of split: rootless Podman with crun and pasta for GPU workloads, where VM passthrough adds too much friction, and VMs for everything else.

What does that mean for an engineer actually building one of these systems? Podman's layered controls, namespace mapping, SELinux, seccomp, and capability dropping are a real and substantial improvement over unsandboxed execution or a root-daemon setup. But where the input to a sandbox might include adversarial prompt injection, shared-kernel isolation should be treated as one layer in a larger stack, not the last line of defense. That's not a knock on Podman. It's just an honest accounting of what a shared kernel can and can't promise.

Complementary primitives that fill the gaps Podman's namespace model leaves open

Podman doesn't have to solve this alone, and it's built to plug into the tools that cover its remaining gaps.

gVisor can run as a Podman OCI runtime directly, intercepting every syscall in userspace before it ever reaches the real kernel. The sandboxed process never makes a direct kernel call at all, which closes off the shared-kernel exploit path without the overhead of standing up a full VM for every sandbox. libkrun goes further, acting as a microVM backend for Podman so each container effectively gets its own kernel through KVM, at close to normal container startup speed. That closes kernel CVE exposure almost entirely, for workloads that can tolerate a small amount of added latency in exchange.

There's also a smaller, more surgical option worth knowing about: Sandlock, an open-source Rust process sandbox available on GitHub. It uses Landlock for static filesystem, TCP-port, and IPC scoping, seccomp-bpf for unconditional syscall filtering, and seccomp user notification with pidfd_getfd for cases that need a dynamic decision rather than a static rule. It adds roughly 5 milliseconds of startup overhead and runs at bare-metal throughput afterward, all without needing root, cgroups, container images, or even namespaces in the traditional sense. It is not a replacement for Podman, but rather a lightweight layer that can run inside a Podman container to add per-process confinement on top of everything else already in place.

For teams that want one API across different backends, the llm-sandbox Python library (MIT licensed, first released in July 2024, around 1,100 GitHub stars) wraps Docker, Podman, or Kubernetes behind a single context-managed session that returns stdout, stderr, exit code, and any artifacts produced. The practical upside is that a Podman-backed sandbox running on a laptop during development uses the same calling code as a Kubernetes-backed deployment in production, so the isolation backend can change without the application logic changing with it.

And on networking, --network none is a blunt instrument, appropriate when a sandbox genuinely needs zero outbound access but too restrictive for agents that legitimately need to hit an API or pull a package. An egress filtering proxy, sitting between the sandbox and the internet, lets a team apply allow-list rules instead of an all-or-nothing switch, so a sandbox can reach exactly the endpoints it's supposed to and nothing else.

Layered together, that's roughly the shape of a Podman-native AI sandbox stack: --network none (or a filtered egress proxy where network access is required), --cap-drop ALL, a tuned seccomp profile, and SELinux enforcing, with gVisor or libkrun added underneath for workloads where the code being executed carries enough risk, or enough uncertainty about its origin, to justify moving past shared-kernel isolation altogether. None of these pieces individually claims to solve untrusted AI code execution. Together, they describe what taking the problem seriously actually looks like in practice.

Sources

  1. GitHub - restyler/awesome-sandbox: Awesome Code Sandboxing for AI
  2. Sandlock: Confining AI Agent Code with Unprivileged Linux Primitives
  3. arxiv.org

More in Sandbox Isolation Primitives