Agent Safety22 min read

Syscall Allowlisting: The Strict Diet for Agents

Blocking binaries is not enough. Seccomp allowlists shrink the system vocabulary so common exploits fail closed — Tetragon decides which programs may start; seccomp decides which syscalls those programs may still speak.

Blocking binaries is not enough. Seccomp allowlists shrink the system vocabulary so common exploits fail closed — Tetragon decides which programs may start; seccomp decides which syscalls those programs may still speak.

Part 4 answered which programs may start after authority says yes. This post answers the residual question: when the binary you allow is still running — node, python3, the agent runtime itself — which syscalls may it speak, and what fails closed when an exploit needs a word you never taught it?


The Binary You Allowed Kept Talking

Part 4’s kill-switch is green. The agent image is near-distroless. Tetragon never sees curl, npx, or pip. The only execve that matters is the one you expected:

node (agent / tool handler)   ✅ allowlisted

Then a hostile skill — or a memory-safe-looking library that isn’t — needs an escape. It doesn’t spawn a new binary. It asks the kernel for tools the language runtime already exposes:

socket(AF_PACKET, …)           # sniff / craft outside the intended path
ptrace(PTRACE_ATTACH, …)       # inspect or mutate a sibling
process_vm_writev(…)           # write into another process's memory
unshare(CLONE_NEWUSER)         # start a namespace dance toward breakout
mount(…)                       # remount, bind, or pivot

Panguard saw an approved tool call. Tetragon saw an approved binary. The vocabulary of that binary was still the full Linux dictionary.

Process containment answers who may speak. Seccomp answers what words remain in their mouth.


After the Binary Is Allowed

Part 4’s residual risk was grandchildren. Part 5’s residual risk is the runtime’s own mouth.

LayerSeesMisses
Prompt / modelNatural languageEverything after the decision
PanguardNamed tool + params + claimsSyscalls inside an allowed handler
Tetragon (Part 4)execve paths / process treeAbuse that never starts a new binary
Seccomp (this part)Syscall number + args at the kernel gateLogic bugs that only use still-allowed calls
Kata / gVisor (Part 7)Where those calls may landNothing — when you need a stronger host boundary

Classic residual cases Part 4 doesn’t close: an approved runtime exploited without execve of curl; FD and socket tricks using raw sockets, packet sockets, or surprising dup3/sendmsg paths; container escape primitives like ptrace, process_vm_*, userfaultfd, mount, unshare, keyctl; debugging syscalls left in production profiles.

Tetragon is the tripwire on new doors. Seccomp is the lock on the doors the process already holds keys for.


The Architecture Pattern: Syscall Allowlisting

Assume every agent binary will eventually be coerced into an unexpected syscall. Prefer default deny — or a hardened baseline that is effectively a short allowlist of mundane I/O — over blocking the scary names and hoping. Attach the profile at container / sandbox launch so the process can’t widen its own diet after start. Role-shape diets: agent runtime, test sidecar, and untrusted-document worker each get different profiles. Treat denials as signals into WORM / SIEM; teach agents to surface them rather than silently invent workarounds.

Agent / tool handler (allowed binary)


   syscall(ptrace | mount | socket(AF_PACKET) | …)


 Kernel seccomp-BPF filter

        ├── allow → continues
        └── deny  → SCMP_ACT_ERRNO / KILL
                    └── Post / audit → Fluent Bit / OTel → WORM + SIEM

Seccomp is the universal baseline whether you later add Kata, gVisor, or stay on standard containers. Sandboxes shrink where code can reach the host kernel; seccomp shrinks which calls get that far even before you pay for a stronger runtime.


RuntimeDefault: The Boring Win

Before you write a custom JSON profile, ship this:

# Kubernetes pod / container securityContext
securityContext:
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  capabilities:
    drop: ['ALL']
  seccompProfile:
    type: RuntimeDefault
# Docker / Compose
services:
  clawql-agent:
    image: ghcr.io/example/clawql-agent:distroless
    read_only: true
    security_opt:
      - no-new-privileges:true
      - seccomp:/etc/clawql/seccomp/agent-runtime.json
    cap_drop:
      - ALL

RuntimeDefault (containerd / CRI-O default, close cousins of Docker’s default profile) already blocks a long list of escape-adjacent calls — including common ptrace/process_vm_* paths and other favorites from the Kubernetes restricted story. Many agent pods still ship with Unconfined because someone pasted a debug profile into production.

If your “hardened agent” is seccompProfile: Unconfined, you have a documentation site, not a diet.


Default-Deny Sketch

ClawQL core agent paths are boring on purpose: read/write under a workspace, talk to a gateway over ordinary TCP, maybe clone/futex/poll for a runtime. They don’t need packet sockets, mount, or cross-process memory writes.

Conceptual allowlist (always derive from strace / runtime traces of your actual image):

ALLOW (illustrative core)
  read, write, close, lseek, fstat, newfstatat
  openat, readlinkat, getdents64
  mmap, mprotect, brk, munmap
  futex, nanosleep, clock_gettime, getrandom
  clone, exit, exit_group, wait4, rt_sig*
  socket(AF_INET|AF_INET6, SOCK_STREAM|SOCK_DGRAM)
  connect, sendto, recvfrom, epoll_*, poll, ppoll

DENY / kill (high-confidence agent diet)
  ptrace
  process_vm_readv, process_vm_writev
  userfaultfd
  perf_event_open
  bpf
  mount, umount2, pivot_root, swapon, swapoff
  unshare, setns
  keyctl, add_key, request_key
  init_module, finit_module, delete_module
  reboot, kexec_load
  open_by_handle_at
  socket(AF_PACKET | AF_RAW | …)

JSON profile shape (Docker / cri-o localhost file — trimmed):

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "defaultErrnoRet": 1,
  "archMap": [
    {
      "architecture": "SCMP_ARCH_X86_64",
      "subArchitectures": ["SCMP_ARCH_X86", "SCMP_ARCH_X32"]
    }
  ],
  "syscalls": [
    {
      "names": [
        "read",
        "write",
        "close",
        "openat",
        "newfstatat",
        "mmap",
        "futex",
        "clone",
        "exit_group"
      ],
      "action": "SCMP_ACT_ALLOW"
    },
    {
      "names": [
        "ptrace",
        "process_vm_readv",
        "process_vm_writev",
        "userfaultfd",
        "mount",
        "unshare"
      ],
      "action": "SCMP_ACT_ERRNO",
      "errnoRet": 1
    }
  ]
}

Wire a custom file in Kubernetes:

securityContext:
  seccompProfile:
    type: Localhost
    localhostProfile: clawql/agent-runtime.json

localhostProfile is relative to the node’s seccomp profile root — pin the file via your node image / DaemonSet, not a hopeful README.

Start from RuntimeDefault, subtract what your agent never needs, and only then add rare exceptions with an owner and an expiry. Inventing a 12-call allowlist from scratch on day one is how you brick node’s thread pool and blame “seccomp is too hard.”


Observe the Vocabulary First

Do for syscalls what Part 4 did for execve: observe before enforcing.

# Inside a representative agent container (dev only)
strace -f -c -p "$(pgrep -n node)"

# Or capture a full trace of a golden path
strace -f -o /tmp/agent.strace \
  node /app/agent.js --once summarize /workspace/README.md

Cross-check denials under a candidate profile:

# Typical failure surface when the diet is too tight
EPERM / ENOSYS on: clone flags, mmap hints, getrandom, eventfd2,

Stage the rollout: RuntimeDefault everywhere still running Unconfined, then trace golden paths (idle, tool call, egress to gateway, crash recovery), then subtract high-risk calls with SCMP_ACT_ERRNO (debuggable) before SCMP_ACT_KILL for the final few, then promote per role.

A surprise EPERM in staging is a gift. The same surprise in production, with silent retries, is how you train agents to wander into worse paths.


Role Diets

RoleSeccomp postureNotes
Agent runtimeRuntimeDefault → custom subtract of ptrace/mount/raw socketsMust keep ordinary TCP + workspace I/O
Trusted short tool sidecarOften tighter; may use gVisor later (Part 7)High invocation frequency, shorter lifetime
Exec / untrusted documentsStrictest diet + stronger sandbox (Kata / gVisor in Part 7)Assume hostile content; seccomp is necessary but not sufficient here
Build / CI imageDifferent profile; never ship as the agentToolchains need syscalls agents must not keep

Helm attachment (same RuntimeClass story Part 4 started; Part 7 deepens):

helm upgrade --install clawql ./charts/clawql-mcp -n openclaw --create-namespace \
  --set security.seccomp.default=RuntimeDefault \
  --set security.seccomp.agentLocalhostProfile=clawql/agent-runtime.json \
  --set security.kata.enabled=true \
  --set security.kata.runtimeClassName=kata-qemu

How Denials Sit Next to Panguard and Tetragon

SignalLayerReading
Panguard allow(tool)ProtocolIntent was in-policy
Tetragon kill(npx)ExecIntent escalated into a forbidden binary
Seccomp deny(ptrace/mount)SyscallIntent stayed in an allowed binary and still reached for escape primitives
Panguard (MCP / ATR)
  → agent runtime (allowed exec)
      → seccomp deny OR Tetragon kill on grandchild
        → Fluent Bit / OTel
        → WORM audit
        → SIEM (+ quarantine hooks)

Label workloads with session, pod, profile name, and role so a seccomp EPERM is not an orphan syslog line. Part 8 deepens TraceID propagation into kernel and sandbox events.

When Panguard allows a call but seccomp (or Seatbelt / Kata) denies the underlying syscall, the sandbox wins — same complementary-layer rule as Part 3’s fail-closed gating.


Environment Matrix

EnvironmentPrimary syscall diet
KubernetesRuntimeDefault / Localhost seccomp on every agent pod + RuntimeClass (Part 7)
Linux edge / bareDocker/Podman seccomp profiles; same JSON you pin on nodes
macOS laptop agentSeatbelt — not Linux seccomp JSON

Seatbelt is the macOS expression of the same idea: the process can’t expand its own policy after launch; forbidden syscalls/paths fail before completion. It’s not a production substitute for container seccomp + Kata/gVisor. gVisor re-implements syscalls in userspace; Kata gives the workload its own kernel. Both still pair with a baseline seccomp story. Part 7 owns the runtime choice; this part owns the diet you apply even when you stay on standard containers.


Honest Failure Modes

Default deny is easy to overfit. Trace real workloads. Agent runtimes use more syscalls than README fiction admits (eventfd, rseq, membarrier, …).

Argument filters are sharper — and sharper to get wrong. Filtering socket by domain (deny AF_PACKET) beats a blanket deny that also breaks healthchecks. Start with name-level denies for high-risk calls; add arg filters when the blunter diet breaks legitimate paths.

SCMP_ACT_KILL vs ERRNO. Kill is excellent for never-should-happen primitives (ptrace in an agent pod). ERRNO is better while learning — and for anything the runtime might handle gracefully. Mirror Part 4’s observe → enforce staging.

Profiles drift with glibc / runtime upgrades. Pin profile versions next to image digests. A Node LTS bump that starts using a newly required call will look like “random production flakes” if nobody owns the diet.

Debug holes. Leaving ptrace allowed “temporarily” is how temporary becomes the threat model. Break-glass profiles need the same time-bound, audited posture as ATR scope expansion in Part 3.

Seccomp is not FIM. Deny openat of /etc/shadow by path belongs to Part 6 and Seatbelt path rules on Mac. Seccomp can sometimes constrain via args, but filesystem no-go zones deserve their own control plane.


Getting Started

Inventory agent/tool pods and containers — list anything still Unconfined. Enforce RuntimeDefault + no-new-privileges + dropped caps + read-only rootfs as the floor. Trace one golden path per role (strace -c / full traces); document required syscalls. Subtract high-risk calls into a Localhost profile; stage ERRNO before KILL. Wire denials into Fluent Bit / OTel → WORM → SIEM with session/pod/profile labels. Teach the agent UX to surface “blocked by syscall policy” rather than mute-retrying into a worse tool.

Part 6: shrink which paths an allowlisted, diet-constrained runtime may still open.


Companion: DevSecOps-boilerplate. Docs: Sandboxing (Kata, gVisor, Seatbelt, seccomp) · Seatbelt essay.

About the author

Daniel Smith builds ClawQL, an agent operating system for token-efficient discovery and execution over APIs — with observability, hardened tool boundaries, and production routing for LLM workloads. He writes here about the systems problems behind shipping agents.