Seccomp Profiles for Container-Based Sandboxes
Seccomp alone won't contain AI-generated code—you need layered sandboxes.

Seccomp-bpf is a Linux kernel feature that filters which syscalls a process can make, and it's become one of the default controls for locking down containers running AI-generated code. This piece walks through how it actually works, where its protection runs out, and how teams running agent workloads in production layer it with gVisor, Firecracker, and network policy to get isolation that actually matches the risk.
Here's the thing about AI agents writing and running their own code: nobody reviewed it first. A human didn't read the diff. There was no pull request, no code owner sign-off, no CI gate that a person configured with a specific threat in mind. The code gets generated at runtime, in response to a prompt, and then it runs. And because large language models are non-deterministic by nature, the same prompt fed into the same agent twice can produce different code with different dependencies and different syscall patterns each time. A 2025 Veracode report found that a large share of AI-generated code failed security tests tied to the OWASP Top 10, which tells you plainly that isolation here matters.
It's also worth separating intent from consequence. Not every dangerous thing an agent does is an attack. Sometimes it's a runaway loop that eats every CPU cycle on the box. Sometimes it's a dependency chain nobody vetted, pulled in by a package manager because the agent decided it needed a library to finish a task. Sometimes it's just a logic error that happens to touch a file it shouldn't. The intentional and the accidental produce the same operational headache, which shifts the real question toward what this code can actually do to the machine it's running on, regardless of why it tried. Syscall filtering is the most direct answer we have to that question, because a syscall is the boundary where a process asks the kernel to do something on its behalf. Block the ask, and the intent behind it stops mattering.
How seccomp-bpf actually works inside a container runtime
Seccomp-bpf works by attaching a Berkeley Packet Filter program to a process, and that program intercepts every syscall the process tries to make before the kernel executes it. This is a general Linux primitive that predates the container tooling built on top of it. You don't need root, you don't need namespaces, and you don't need cgroups to apply it. A single unprivileged process can have a seccomp filter attached to it directly.
Container runtimes like Docker and containerd take a seccomp profile written as JSON, compile it into a BPF program at container creation time, and attach that program to the container's process. Two modes exist here, though in practice almost everyone runs one of them: allowlist mode. Docker's default profile is an allowlist, meaning the kernel permits only the syscalls explicitly named and everything else gets blocked (typically with an EPERM error returned to the calling process).
What makes seccomp genuinely useful is that it can filter on syscall arguments, not just the syscall number itself. Take clone, the syscall used to create new processes and threads. A rule can allow ordinary clone calls used for forking while blocking the specific flags used to create new namespaces, which is one of the mechanisms behind container escapes. Or take ioctl, which handles device-specific input and output. A rule can permit normal I/O operations through ioctl while blocking the TIOCSTI request specifically, which historically has been used to inject characters into another process's terminal input buffer. This argument-level granularity separates a real security control from an all-or-nothing gate.
Kubernetes builds on this with support for automatically loading seccomp profiles onto pods and containers scheduled to a node, so the profile travels with the workload rather than requiring manual application at the host level. And the performance cost of all this filtering, is low under normal syscall volume; the BPF program runs directly in kernel space, so there's no context switch back to userspace to make the filtering decision. Under very high syscall volume, the overhead becomes measurable, but for the vast majority of workloads it stays in the background.
What Docker's default profile blocks — and what it deliberately leaves open
Here's where the nuance matters, and where a lot of engineers get a false sense of security. Docker's default seccomp profile is designed around compatibility. The goal is to make sure ordinary containerized applications don't break, and shrinking the attack surface as far as possible is a secondary concern at best. Out of the several hundred syscalls available on a modern Linux kernel, the default profile blocks a relatively small subset. The large majority pass through untouched.
What it does block reads like a list of the syscalls most associated with privilege escalation and container escape techniques. ptrace and keyctl are blocked, cutting off common paths to process introspection and kernel keyring manipulation. mount, unshare, setns, and pivot_root are blocked, closing off several classic namespace-escape techniques used to break out of a container's filesystem view. kexec_load, bpf, and perf_event_open are blocked too, denying direct kernel manipulation and some forms of kernel-level introspection that past exploits have used.
But look at what's left open. Network syscalls are untouched by default, meaning a container can make outbound connections and exfiltrate data unless something else (a network policy or an egress proxy) stops it. Filesystem syscalls are permitted broadly, so reading sensitive paths is possible unless the filesystem layout or a separate access control mechanism restricts it. Process creation syscalls remain available too, letting an agent spawn arbitrary child processes as part of its normal operation, which is exactly the kind of behavior an autonomous coding agent needs and exactly the kind of behavior that's hard to distinguish from something malicious.
So what does "seccomp enabled" actually tell you about a container running an AI agent? Less than the label suggests. The presence of a seccomp profile is not the same as a profile suited to the workload; the contents matter as much as whether the feature is switched on at all. This is the foundation for everything that follows in this piece, because it's the gap between default and hardened that makes custom profiles and layered defenses necessary rather than optional.
The non-determinism problem: why writing a tight seccomp profile for agents is genuinely hard
Traditional seccomp profile design assumes you can enumerate, ahead of time, what a process is going to do. You trace a known application, you see it calls open, read, write, close, a handful of networking calls, and you build a profile around that fixed behavior. This works well for a web server or a database, because the binary doesn't change its mind about what syscalls it needs.
An AI agent breaks that assumption at the root. The same prompt, run twice, can produce different code, which pulls in different libraries, which makes different syscalls. One run might generate a pandas script that never touches the network. The next run, same prompt, might generate something that shells out to curl, spawns a subprocess, or reaches for a system library that issues a syscall nobody anticipated. You genuinely cannot build a syscall inventory in advance the way you would for a fixed binary, because there is no fixed binary. There's a fixed model, producing variable output.
That creates a direct bind. Write the profile tight, and you'll block syscalls that are unknown to you but entirely legitimate for the task at hand, causing failures that look like bugs in your sandbox rather than the security control doing its job. Write it permissive, and you preserve compatibility at the cost of the attack surface you were trying to shrink in the first place. Teams that let agents install packages at runtime hit this constantly: a fresh dependency pulls in native code, and that native code issues a syscall the profile never accounted for, and the failure mode is either a silent crash or a confusing error that takes an engineer an afternoon to trace back to a seccomp denial buried in the kernel audit log.
A few approaches try to manage this rather than solve it outright. Tracing a representative sample of agent runs to build a syscall inventory works reasonably well when the task is narrow and repeatable, say, an agent that only ever formats CSV files. It falls apart for open-ended agents doing genuinely novel work each time. Starting with a permissive profile and tightening it over time is more practical operationally, but it demands sustained attention; someone has to keep watching what the agent actually does and keep narrowing the allowlist, which is real engineering work that doesn't stop.
Seccomp's user notification mechanism offers something more interesting: rather than hard-blocking a syscall, the kernel can suspend the calling process and hand the decision off to a supervisor process running in userspace, which decides in real time whether to allow it. That supervisor can factor in context the static profile never could, like what the agent's task actually is or what it's already done in this session. It's a meaningfully different model, closer to runtime policy enforcement than static filtering, and it's the kind of thing worth watching for agent workloads specifically, because the right answer to "should this syscall be allowed" increasingly depends on what's happening, not just what's being asked.
For agents that are truly open-ended, seccomp reduces the surface area available to misbehaving code, but it cannot be the only thing standing between that code and the host. It's a necessary layer, and one among several that this piece covers.
Where seccomp stops: the shared-kernel exposure that remains after filtering
Every syscall seccomp allows through still executes against the real Linux kernel underneath the container. That's worth sitting with for a second, because it's easy to treat a passing seccomp filter as the end of the story when it's really just the entry point. All containers on a given host share that one kernel; there's no second kernel underneath for isolation between them. If a permitted syscall triggers a kernel vulnerability, the isolation boundary the container was supposed to provide can collapse entirely, and no seccomp rule prevents this because the filter already allowed that syscall.
This isn't a hypothetical. CVE-2024-21626, known as Leaky Vessels, showed exactly this kind of failure: a crafted Dockerfile could set WORKDIR to a path under /proc/self/fd that pointed at the host filesystem, and runc versions up through 1.1.11 handled that working-directory logic improperly, letting the container traverse into the host's directory tree. Then in late 2025, three separate critical runc vulnerabilities were disclosed within a short window of each other, CVE-2025-31133, CVE-2025-52565, and CVE-2025-52881. These affected Docker, Kubernetes, and other platforms built on runc, enabling host file access or full container breakout depending on the specific flaw.
It's also worth noting that layered controls sometimes catch what one layer misses on its own. In some configurations, running SELinux in enforcing mode prevented exploitation of certain of these CVEs even when the underlying runc bug was present, which is exactly the argument for defense in depth rather than betting everything on a single mechanism.
There's a trajectory question here too, and it's not a comfortable one. Frontier model performance on apprentice-level cybersecurity benchmark tasks went from a small fraction of tasks solved in 2023 and 2024 to roughly half by 2025, with the first expert-level task completed that same year. Sandbox designs calibrated to what agents could do two years ago may simply not hold up against what they can attempt now. That's not a reason for alarm so much as a reason to keep the isolation model moving at the same pace as the capability curve.
And separate from any kernel bug entirely, there's a whole category of failure that seccomp was never built to catch: policy failures. If a sandbox has outbound network access, an agent can exfiltrate whatever it can read. If it can read credential paths, it can leak secrets without exploiting anything. If it can reach internal services on the same network, it can move laterally. None of these require a CVE, a kernel bug, or a clever escape technique; they just require the sandbox's boundaries to be drawn too loosely. Seccomp is a necessary control here, but on its own it addresses only part of the picture.
gVisor: interposing a userspace kernel to shrink the host syscall surface
gVisor takes a different architectural approach to the same problem: rather than filtering which syscalls a workload can send to the real kernel, it intercepts every syscall and handles most of them inside a userspace component called the Sentry, so the workload's syscalls never reach the host kernel directly. The Sentry acts as an application kernel, implementing enough of the Linux syscall interface to run real applications, but doing that implementation work in userspace, where a bug is far less catastrophic than the same bug sitting in the actual kernel.
The Sentry itself, though, still needs to talk to the real host kernel occasionally, for things it can't fully emulate on its own. And this is where seccomp comes back in, playing a role that's easy to miss if you only think of it as a workload-level control. The Sentry's own calls to the host kernel are restricted with a seccomp-bpf filter to a tight allowlist: 53 host syscalls when networking is disabled, and 15 more when it's enabled, for 68 total. That's a dramatic cut from the several hundred syscalls available on the raw kernel surface, and it means seccomp is doing double duty here, filtering not the workload but the very component that stands between the workload and the host.
gVisor's newer Systrap platform uses seccomp-bpf specifically for syscall interception rather than just filtering, and notably doesn't require hardware virtualization support to run, which means it can operate inside a VM that itself lacks nested virtualization. Engineering work on tightening gVisor's internal seccomp-bpf filters has reportedly removed up to roughly 15% of gVisor's overhead in some configurations, which shows how tightly seccomp performance is woven into gVisor's overall performance story. It's a load-bearing part of the design rather than a later addition.
The tradeoffs are real, though, and worth naming honestly. CPU-bound workloads see modest overhead running under gVisor, because most of their time is spent computing rather than making syscalls. Syscall-heavy workloads, particularly ones doing a lot of filesystem or network I/O, see more substantial slowdowns, a pattern documented in peer-reviewed benchmarking of container sandbox performance. I/O-heavy workloads land somewhere in between, with overhead that's noticeable but generally not prohibitive for most production use cases.
One gap is worth flagging specifically for anyone running GPU-dependent agent workloads: gVisor's userspace interception model blocks direct PCIe passthrough for GPU calls, which is a real constraint if your agents need GPU access for model inference or training tasks. At scale, though, the approach clearly works; Northflank runs over two million isolated workloads a month using gVisor alongside Kata Containers, which says something about how far this architecture has been proven under real production load.
Firecracker microVMs: hardware-level isolation and where seccomp fits inside it
Firecracker takes the isolation question a step further by giving each workload its own Linux kernel, running inside a lightweight virtual machine backed by KVM hardware virtualization rather than sharing the host kernel at all. This changes the shape of the problem entirely. Where a container escape means breaking out of one shared kernel, escaping a Firecracker microVM means an attacker has to break the guest kernel and the hypervisor both, two independent boundaries stacked on top of each other rather than one.
But how does seccomp fit into an architecture built around hardware virtualization? It shows up again, this time constraining the Firecracker VMM process itself, the userspace component that manages the virtual machine on the host side. According to the Firecracker NSDI paper, the VMM runs under a seccomp profile that whitelists just 24 syscalls with argument-level filtering, plus 30 permitted ioctl operations. Seccomp here isn't protecting the workload inside the VM; it's shrinking the attack surface of the process managing the VM from outside, which is a slightly different job than either of its roles in the container or gVisor discussions above.
There's a pattern worth naming explicitly here, because it recurs at every layer of this piece: each isolation boundary applies seccomp to the layer immediately beneath it. Docker applies it to the container process. gVisor applies it to the Sentry's calls to the host. Firecracker applies it to its own VMM process. It's the same primitive, reused recursively, each time shrinking what the next layer down is allowed to ask the real kernel to do.
GPU support remains a genuine limitation for Firecracker in production. There's no officially supported GPU passthrough as of this writing; experimental work exists in the community, but it isn't something most teams should build a production GPU workload on top of today.
And the assumption that microVMs simply don't have publicly known escape vulnerabilities broke in 2026. CVE-2026-5747, an out-of-bounds write in the virtio-pci implementation with a CVSS score of 8.7, and CVE-2026-1386, a jailer symlink vulnerability allowing host writes with a CVSS score of 6.0, were both disclosed within about four months of each other. That's a useful reminder, not a reason for panic: microVM isolation reduces risk substantially compared to shared-kernel containers, but reducing risk isn't the same as eliminating it. What Firecracker really does is move the residual risk from kernel-level vulnerabilities (historically frequent, across a huge surface area) to hypervisor-level vulnerabilities, which have a much smaller surface and a considerably slower disclosure cadence.
A risk-tiered model for choosing how much isolation AI code execution actually needs
Not every piece of code an organization runs deserves the same isolation boundary, and treating them all identically is its own kind of mistake. Over-isolate everything and you pay in latency and infrastructure cost on workloads that never needed hardware virtualization. Under-isolate and the risk lands somewhere you didn't plan for. A community consensus that emerged around 2026 sorts this into three tiers, and it's a useful mental model even if your organization draws the lines slightly differently.
Tier 1 covers engineer-written code that's already passed through CI/CD. A human wrote it, tests validated it, and it's been reviewed by the normal software process. Standard containers with a seccomp profile and capability dropping are likely sufficient here, because the code's behavior is known and bounded by the time it runs.
Tier 2 covers LLM-generated code for bounded tasks: math computation, data formatting, transformation pipelines, the kind of work where the shape of the output is predictable even if the exact syscalls aren't. Risk increases meaningfully here relative to Tier 1, mostly from accidental resource exhaustion or logic errors rather than intentional exploitation, and this is where gVisor or a microVM approach earns its overhead cost.
Tier 3 covers the genuinely dangerous territory: user-uploaded binaries, autonomous agents executing arbitrary code they generated themselves, or packages pulled from the internet with no vetting at all. Here the reasonable posture is to assume the code is hostile, full stop, and reach for hardware virtualization, Firecracker-class microVMs, or comparable air-gapped process primitives.
Seccomp belongs at all three tiers, playing a different role at each one. At Tier 1 it's the primary control against kernel-surface abuse, doing the bulk of the work on its own. At Tier 2 it operates at two points simultaneously: filtering the workload's own attempts to reach the kernel, and (inside gVisor) filtering the Sentry's calls to the host on the workload's behalf. At Tier 3, it constrains the VMM process itself, as in Firecracker's 24-syscall whitelist, adding one more layer even after hardware virtualization has already done most of the heavy lifting.
Landlock pairs naturally with seccomp across all three tiers, and it's worth mentioning because the two controls address orthogonal axes of the same problem rather than overlapping or competing. Landlock restricts which filesystem paths a process can touch. Seccomp restricts which syscalls it can make at all. Run them together and you're covering two separate threat surfaces without redundant effort. For Tier 2 and Tier 3 workloads specifically, network egress policy deserves the same attention as syscall filtering, because credential leakage and lateral movement, as covered earlier, don't require a kernel exploit to happen; they just require an open door somewhere in the network path.
What a hardened production profile looks like in practice
So what does all of this look like assembled into something you'd actually run in production? A representative hardened gVisor setup, documented in a community writeup from 2026, layers several controls together rather than relying on any single one. Per-job PID, mount, and IPC namespaces get created via clone3, isolating each job's process view from every other job on the same host. Seccomp-bpf runs inside that namespace, explicitly blocking clone3 itself from further use, along with iouring, ptrace, and kernel module loading. The process drops privilege by running as a high, unprivileged UID with PRSETNONEW_PRIVS set, which prevents it from gaining new privileges even through a setuid binary. Every writable path lives on ephemeral tmpfs, the root filesystem stays read-only, and capability-based file APIs confine writes to a specific working directory rather than the filesystem at large. Network egress is controlled as its own separate enforcement layer, not folded into the syscall filter at all.
Two real production examples show this pattern isn't just theoretical. OpenAI's Codex uses Landlock and seccomp together, and it's documented as the only major agent runtime with sandboxing turned on by default rather than requiring an engineer to opt in. Claude Code's Linux sandbox combines filesystem isolation scoped to specific directories, network isolation through proxy servers (on Linux, this means removing the network namespace entirely and routing all traffic through Unix socket proxies), seccomp-bpf filters that block Unix domain socket creation at the syscall level, and Bubblewrap as the underlying sandboxing tool, though Claude Code ships it off by default..
The pattern across both of these, and really across everything covered in this piece, is that no single control does the whole job. Seccomp shows up in every layer we've discussed, from Docker's default profile to gVisor's Sentry to Firecracker's VMM, but it never appears alone. It's paired with namespace isolation, filesystem restriction, privilege dropping, and network policy, each addressing a piece of the problem seccomp alone cannot solve. That's simply an honest accounting of what a syscall filter can and cannot do, and why the teams running AI agents at real scale keep reaching for it as one layer among several rather than the whole answer.


