Seccomp Profiles for AI Code Execution Workloads
AI agents need seccomp profiles built from actual syscall traces, not generic defaults.

Kubernetes made seccomp the default posture for every new pod starting in version 1.29, replacing an unconfined baseline with a curated syscall allowlist called RuntimeDefault. That is a real improvement for the kind of workload RuntimeDefault targets: a web server, a database, a batch job with a predictable shape. It is a poor fit for an AI agent, and this piece is about why that mismatch exists, how to measure it on your own workload, and what a profile actually looks like once you've built one from evidence instead of assumption.
RuntimeDefault covers the syscalls a conventional containerized app needs and blocks the rest. That works well when the process running inside the container behaves conventionally. It works poorly when that process is an agent that installs packages mid-session, clones a repo it didn't know it needed, spawns a dev server to test its own output, or calls into a local model for inference. None of that resembles the syscall fingerprint RuntimeDefault was profiled against, so teams end up in one of two failure modes: a profile too loose, which leaves kernel attack surface reachable that the workload never actually uses, or a profile too tight, which blocks something the agent needs and fails silently, often dozens of tool calls into a session, with no obvious signal that seccomp was the cause. Fixing that requires a process: audit what the workload really calls, build a profile from that record, and layer it correctly against everything else in the isolation stack. That's the walkthrough this piece covers.
What AI agents actually do at the syscall level
Break an agent's activity into four operation types and the syscall pattern for each looks different enough that treating them as one undifferentiated blob is where most profiles go wrong.
File operations (read, write, open, stat) are the least risky. They're mostly reversible within a session and rarely touch anything outside the workspace. Shell and subprocess execution carries more weight: execve, clone, wait4, kill. This is where an agent actually does things in the world, running a build, killing a stuck process, spawning a worker, and it's also the category with irreversible side effects if something goes wrong. Package and dependency installation sits in between but produces the widest burst of activity: a single pip install or npm install triggers long execve chains, network syscalls to fetch packages, and mmap/mprotect calls as compiled extensions get loaded into memory. External service calls (connect, sendto, recvfrom) form their own category because they cross a trust boundary the other three do not; the agent is now talking to something outside the container entirely.
Why separate these instead of one allow/deny gate for the whole workload? Because the risk isn't uniform across them, and a profile that treats file reads and process spawning as equally sensitive either over-restricts the safe stuff or under-restricts the dangerous stuff. Neither is what you want.
GPU inference adds a fifth wrinkle that doesn't fit neatly into the four above. Talking to a GPU driver means ioctl calls with device-specific parameters, and that's a structurally different, more permissive syscall posture than anything CPU-only agent code needs. A profile that works fine for an agent writing and testing code will break the moment that same agent calls a local model for inference, because the ioctl surface it needs wasn't there.
Then there's a smaller set of syscalls that are dangerous precisely because they're legitimate. clone and unshare are needed by some package installers for sandboxing their own build steps, but they are also the namespace manipulation primitive at the center of most container escape techniques. ptrace shows up in debuggers and profilers an agent might reasonably invoke, and it is also the primary process manipulation vector once something has gotten a foothold. mount is legitimate in build environments that need to set up loopback filesystems, and it is a filesystem escape primitive everywhere else. There's no clean rule here, and each of these needs a deliberate decision, not a default.
Underneath all of this sits a harder problem: agent-generated code doesn't have a fixed shape. An LLM writing a script for a novel task can produce syscall patterns nobody anticipated when the profile was written, and a profile built purely on what you've observed so far is a snapshot that has to be designed with that limitation in mind.
How to audit the syscall surface of a running agent workload
The point of an audit is to generate a ground-truth record of what the workload actually calls under conditions that look like production, not a five-second smoke test.
Three tools do this job, at different costs. strace attaches to the agent process and everything it spawns, and it captures every syscall along with its arguments, which is the level of detail you want for a first pass. The overhead is high enough that it belongs in development, not production. Seccomp's own LOG mode is the least invasive option for observing something closer to real traffic: set the policy action to LOG instead of ALLOW or DENY, and the kernel writes every matched syscall to the audit log without blocking anything. Then there's eBPF-based tracing, tools like bpftrace or Falco, which sits between the two: lower overhead than strace, safe to leave running across a longer session, and it produces structured output that's easier to aggregate afterward.
A representative run has to include more than steady-state execution. Package installation steps, at least one full multi-turn session covering the kinds of tasks the agent handles in production, any local GPU inference calls, and any MCP tool invocations if the agent connects out to external MCP servers. Skip any of these and the audit record has a blind spot exactly where things are most likely to break later.
Once you have the raw trace, aggregate it. Sort by frequency; the syscalls that show up constantly across every run are your core set, and the ones that show up once or twice need individual review rather than a blanket allow. Cross-reference the whole list against known escape primitives: ptrace, mount, unshare, clone, open_by_handle_at, init_module. Any overlap between what your workload calls and that list is a flag, not an automatic yes.
One bit of Kubernetes friction worth knowing about up front: seccomp profiles aren't a first-class API object. They get distributed as JSON files that have to exist on every node, which means your audit-to-deploy cycle needs a distribution plan from day one, not as an afterthought once the profile is written. Tools like seccomp-tools and the Kubernetes Security Profiles Operator (SPO) exist specifically to take the manual labor out of that record-build-distribute loop, and they're worth adopting early rather than building the plumbing yourself.
Building a least-privilege profile from the audit record
Three passes, working from the audit data outward.
Pass one establishes the safe core: syscalls that showed up consistently across every representative run and have no overlap with the escape-primitive list. These go straight into the allowlist without much debate.
Pass two is where the real work is: adjudicating the contested set, the syscalls you observed but that also appear on the dangerous list, such as clone, ptrace, and unshare. For each one, ask whether the workload genuinely needs it, or whether there's an architectural alternative. Package installation, for instance, is often better run as a separate, more permissive ephemeral step rather than baked into the steady-state profile the agent runs under for the rest of the session. Splitting the workload by phase this way often removes half the contested list without any loss of function.
Pass three sets the default action for everything not on the allowlist. SCMP_ACT_ERRNO returns an error code and lets the process keep running; SCMP_ACT_KILL terminates it outright. ERRNO is friendlier for debugging since you get a chance to see what broke; KILL is the stronger enforcement stance. Which one you pick should track how confident you actually are in the audit record, not a default habit.
A couple of structural choices matter here too. Use symbolic syscall names in the profile, not raw numbers; syscall numbers vary across kernel versions and CPU architectures, and a profile built on raw numbers is a portability trap waiting to happen. And don't try to write one profile for the whole lifecycle. Package installation, steady-state agent execution, and GPU inference are different enough that a single profile covering all three will either over-permit two of them or break the third.
The GPU case deserves its own callout because it's the easiest way to accidentally blow a hole in an otherwise tight profile. GPU driver ioctls need to be allowed with device-specific parameters, and that permission should live in its own profile, applied only to pods that actually run inference. Don't extend GPU-width ioctl access to agent pods that never touch a GPU just because it's convenient to have one profile.
RuntimeDefault still has a role here. It's a reasonable floor for the default action, and there's no need to build from a blank page: extend it, adding only the delta your audit shows the workload actually needs, rather than throwing it out entirely.
Before any of this goes into enforcement, run the candidate profile in LOG-only mode in staging and compare the logged calls against your allowlist. Any syscall that shows up unexpected is a reason to go back and look at what the workload is doing, not a reason to just add it to the list and move on.
Layering seccomp with the rest of the isolation stack
Seccomp cuts down the exploitable syscall surface by a meaningful amount, but it was never meant to stand alone. It's one layer in a defense-in-depth model that also includes compute isolation, filesystem restriction, network controls, and resource limits.
Where it sits changes depending on the isolation tier underneath it. In a microVM setup (Firecracker, Kata Containers, libkrun), each workload gets its own kernel, so a kernel exploit inside one VM cannot reach the host regardless of the seccomp profile in place. Seccomp still matters here, since it shrinks the attack surface even within that isolated kernel, but the stakes of getting the profile slightly wrong are lower because the blast radius is contained by the VM boundary itself.
gVisor works differently: it intercepts syscalls in user space before they ever reach the host kernel. Seccomp and gVisor typically run together, with gVisor adding an interception layer at the cost of some latency on every syscall and, worth noting, incomplete coverage of the full syscall surface on its own.
Standard OCI containers are the tier where seccomp carries the most weight, because there's no additional kernel boundary underneath it: the container shares the host kernel directly. That posture works for isolating trusted-but-untested code, but it needs additional layers stacked on top when the code running inside is genuinely untrusted: LLM-generated code.
Namespaces (PID, mount, network, IPC, user) work alongside seccomp rather than replacing it: namespaces restrict what a process can see, seccomp restricts what it can ask the kernel to do. Network policy fills a related gap. Even if the seccomp profile allows a connect syscall the agent wasn't expected to make, a network policy can still block the actual connection from reaching anywhere sensitive. And cgroups handle resource limits, which is a separate concern from syscall filtering entirely but part of the same overall containment posture: a runaway agent shouldn't be able to take down the node just because its syscalls were technically permitted.
The practical takeaway: the tighter the isolation underneath (microVM, gVisor), the more tolerance there is for a slightly broader seccomp profile. The thinner it is, plain OCI containers with nothing else, the more that seccomp profile has to carry on its own.
What happens when the profile is wrong — real escape chains and CVE patterns
This isn't hypothetical anymore. A wave of CVEs surfacing across 2025 and into 2026 has exposed structural weaknesses specific to AI agent execution environments, and a multi-agent security audit from Pillar Security in 2026 catalogued four recurring failure patterns behind them.
Denylists that can't keep pace with the operating system's actual syscall surface. Workspace configuration files that turn out to be code execution in disguise; the canonical example is CVE-2026-48124, a Cursor workspace hook where a config file ran unsandboxed commands nobody expected config to be capable of running. "Safe" command allowlists that check a command's name but not its arguments, which is a distinction with a real difference: rm is safe, rm -rf / is not, and a name-only check cannot tell them apart. And privileged local daemons sitting entirely outside the sandbox, leaving the thing that mattered unprotected.
The vm2 escape pattern is a good illustration of how these chains actually work in practice: an attacker gets code running inside the sandbox first, then chains together weaknesses in the isolation model's own internals, prototype pollution, exception handling quirks, async primitives, proxy unwrap tricks, to climb out and reach host Node.js capabilities. A correctly built seccomp profile breaks this chain specifically at the point where the exploit tries to touch host capabilities directly; it doesn't stop the initial code execution, but it stops what that execution can do next.
Prompt injection is the entry point for a lot of this, and it's worth being precise about how often it actually works rather than treating it as a rare edge case. Anthropic's system card for Claude Opus 4.5 measured indirect prompt-injection success in agentic coding environments at 4.7% on a single attempt, climbing to 63.0% at 100 attempts. At the scale agents actually operate (thousands of tasks running continuously), even a low per-attempt success rate turns into frequent successful injections over time. Once an injection succeeds, whatever code it introduces runs with exactly the permissions the agent process already holds. That's the whole ballgame: the injection is the delivery mechanism, but the syscall profile determines how much damage the delivered code can actually do.
MCPoison (CVE-2025-54136) shows a variant of the same idea aimed at trust rather than code: an attacker gets an MCP configuration approved once, then quietly swaps in malicious commands after the fact, bypassing the original trust decision entirely. The broader MCP attack surface is structurally different from older injection patterns for a simple reason: a malicious MCP tool call inherits everything the agent process already has (filesystem access, environment variables that might hold API keys, network reach into internal services) unless the syscall profile and network policy are actually constraining what that inherited access can do.
Which is the point of the whole exercise. A correct profile blocking init_module stops kernel module loading even after code execution succeeds. Blocking ptrace stops process manipulation after the fact, and blocking mount stops filesystem escape. None of these prevent the initial compromise; what they do is turn a chain that would otherwise end in full node compromise into something contained, survivable, and recoverable.
Maintaining profiles as agent behavior and models change
A profile that was correct last month can be wrong today, and the reason is almost always change on the agent side rather than any flaw in how the profile was built. Swap in a new model version, and the code it generates shifts in ways that change the syscall surface. Add a new MCP server or tool integration, and the agent's activity pattern shifts again. Bring in a new language runtime or package ecosystem, same story. Move the workload to a different isolation tier, say from plain OCI to gVisor, and the calculus behind how tight the profile needs to be changes too.
Any of these should trigger a re-audit, not a shrug. The good news is you don't have to guess when drift has happened: leaving LOG mode active in production, even after enforcement is live, gives you an early warning system. Unexpected syscalls showing up in the audit log are a signal the workload has changed, and that signal shows up before things start breaking in ways that are hard to trace back to a policy change.
Treat the profile itself the way you'd treat any other piece of infrastructure code: version-controlled, reviewed before merge, tested in staging before it touches production. The Security Profiles Operator gives Kubernetes clusters a structured way to manage that lifecycle rather than hand-editing JSON files on individual nodes.
There's a subtler maintenance wrinkle specific to long-running agent sessions. An agent that's been running for a while may have installed packages or written files earlier in the session that change what its later syscalls look like, and the profile has to account for that evolved state, not just the syscall pattern the agent produces on a cold start. A profile audited only against fresh sessions can miss exactly the behavior that shows up forty turns in.
Given how fast agent development moves, frequent model updates, new tools added on short cycles, profile maintenance has to live inside the deployment pipeline as a routine step, not sit off to the side as a one-time hardening pass that gets revisited only when something breaks.
Where purpose-built agent runtimes change the seccomp calculus
Everything above describes what a team has to build by hand: audit, adjudicate, layer, maintain. That is real work, and it has to happen continuously as models and tools change underneath the workload. It is worth asking whether all of it has to be bespoke.
A runtime built specifically to execute AI-generated code starts from a different premise than a general-purpose container platform retrofitted for agents. Instead of a single broad RuntimeDefault profile meant to cover any conceivable containerized app, a purpose-built execution environment can ship separate, pre-audited profiles for the operation types this piece has walked through: one for package installation, one for steady-state code execution, one for GPU inference, built and tested against the actual syscall patterns those phases produce rather than against a generic web-server baseline. The audit-build-maintain cycle doesn't disappear, but it moves from something every team repeats independently to something built into the platform and kept current as models and tool ecosystems shift.
The general lesson here still holds regardless. Even inside a purpose-built runtime, someone still has to understand why RuntimeDefault falls short for agent workloads, why clone and ptrace and mount need individual scrutiny rather than a blanket rule, why GPU inference needs its own profile, and why LOG mode belongs in production as a drift detector rather than just a staging tool. What changes is who does that work and how often teams have to redo it from scratch. The syscall-level reality of what agents do doesn't change based on who's running the platform underneath them; what changes is whether the team built the profile with that reality in mind, or borrowed it from a workload that never had to install a package, spawn a subprocess, or touch a GPU in its life.


