Agent Safety22 min read

Kernel-Level Kill-Switch: eBPF Process Containment for Agents

When a compromised agent spawns npx or curl, user-space policy is too late. Enforce exec allowlists in the kernel with Tetragon — Panguard sees the tool call; eBPF sees what actually ran.

When a compromised agent spawns npx or curl, user-space policy is too late. Enforce exec allowlists in the kernel with Tetragon — Panguard sees the tool call; eBPF sees what actually ran.

This is Part 4 and the start of Phase 2: Runtime Integrity. Parts 1–3 hardened how the agent talks to the world and what authority it holds. This post answers a different question: when the agent or something it launched still tries to exec npx, curl, or pip, who stops it — and how fast?


The Process Tree That Disagreed With the Prompt

A ClawQL agent is summarizing a repository. ATR claims allow file_read, file_write under /workspace, and a narrow run_tests tool. Panguard allows the test call. Somewhere in a dependency README, a poisoned instruction nudges the model toward “install the missing helper”:

npx some-helper@latest

The MCP layer saw one approved tool: run_tests. The kernel saw a process tree:

node (agent / tool handler)
  └─ sh
       └─ npx
            └─ npm
                 └─ curl | sh   # postinstall from typosquat

By the time a human reads the Langfuse span, credentials may already be on a remote host. User-space “please don’t run curl” policy never got a vote on the grandchildren.

Panguard answered: was this tool call allowed? The process tree answered: what actually ran.

If your security story ends at the MCP dispatcher, you’re auditing intent while the exfil rides execve.


Below the Tool Call

Part 3 put Panguard at the structured MCP boundary: ATR claims, JSON Schema, HITL, fail-closed nonces. That remains mandatory. Process containment covers the residual risk.

LayerSeesMisses
Prompt / modelNatural languageEverything after the decision
PanguardNamed tool + params + claimsChildren spawned inside an allowed handler
Tool handlerIts own spawn / execScripts and postinstalls it did not expect
Kernel / TetragonActual execve paths and process treeNothing that crossed execve without a hook match

Classic residual cases: approved tool with hostile grandchild (run_tests → npm script → npx → postinstall); package managers at runtime (pip install as “just fix the env”); shell pipes (curl … | bash after a model follows untrusted text); compromised runtime where app logs lie or go dark but eBPF still sees syscalls.

Panguard is the front door. Tetragon is the tripwire on every door the process tree opens afterward.


The Architecture Pattern: Kernel-Level Kill-Switch

Assume every agent workload will eventually attempt an unexpected execve. Put an eBPF policy on the host (Tetragon) that sees those attempts in-kernel. Start with Post (observe), graduate high-confidence binaries to Sigkill. Emit forensic events into the same correlation path as Panguard (WORM + SIEM). Pair with image distrolessness, read-only rootfs, seccomp (Part 5), FIM (Part 6), and sandboxes (Part 7) — the kill-switch is one layer, not the whole vault.

Agent / tool handler


   execve("/usr/bin/curl", …)


 Kernel hook (Tetragon eBPF)

        ├── no match → process continues (+ optional Post)
        └── match forbidden binary
                ├── Sigkill (synchronous)
                └── Post → Fluent Bit / OTel → WORM + SIEM

For execve, Sigkill stops the unauthorized process before useful work continues. Part 4’s primary control is kill on forbidden exec.


Observe First

Install Tetragon and watch the agent binary’s children before enforcing anything:

apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
  name: clawql-process-exec-observe
spec:
  kprobes:
    - call: 'sys_execve'
      syscall: true
      args:
        - index: 0
          type: 'string'
        - index: 1
          type: 'string'
      selectors:
        - matchBinaries:
            - operator: 'In'
              values:
                - '/usr/bin/node'
                - '/usr/local/bin/node'
                - '/app/clawql-agent'
          matchActions:
            - action: Post

Then baseline dangerous command and supply-chain families without enforcing:

apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
  name: clawql-dangerous-commands-observe
spec:
  kprobes:
    - call: 'sys_execve'
      syscall: true
      args:
        - index: 0
          type: 'string'
      selectors:
        - matchArgs:
            - index: 0
              operator: 'Postfix'
              values:
                - '/rm'
                - '/dd'
                - '/nc'
                - '/curl'
                - '/wget'
                - '/chmod'
          matchActions:
            - action: Post

Run observe-only long enough to build a per-image, per-role process baseline — a week is common. False positives from test runners and npm scripts are policy engineering problems.

Local edge/Linux:

sudo mkdir -p /etc/tetragon/tetragon.tp.d/clawql
sudo systemctl enable --now tetragon
sudo tetra getevents -o compact

Enforce: Sigkill the High-Confidence Set

After the baseline, promote what should never run in production agent/tool images:

apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
  name: clawql-kill-unauthorized-child-exec
spec:
  kprobes:
    - call: 'sys_execve'
      syscall: true
      args:
        - index: 0
          type: 'string'
      selectors:
        - matchArgs:
            - index: 0
              operator: 'Postfix'
              values:
                - '/curl'
                - '/wget'
                - '/nc'
                - '/npx'
                - '/pip'
                - '/pip3'
          matchActions:
            - action: Sigkill
            - action: Post

Surface exit 137 / SIGKILL to the user as a policy block — silent retries with wget instead of curl turn the kill-switch into whack-a-mole.

Denylist + distroless beats denylist on a fat image. Renamed or copied binaries evade string Postfix matching; remove the tools from the filesystem when you can.


Exec Matrix: Role-Shaped Diets

RoleAllow (examples)Forbid at runtime (examples)
Agent runtimenode, clawql-agent, maybe gitcurl, wget, npx, pip, shells
Test sidecarnode, npm (if contract), gitnpx, curl, pip, arbitrary bash
Python tool sidecarpython3, pytestRuntime pip install, curl, shells
Build image (CI only)Toolchain as neededNever ship this image as the agent

Layer pairing that actually holds: distroless/minimal image (unauthorized binaries absent) + read-only root filesystem + Panguard command/ATR allowlists + Tetragon kill on forbidden exec + Falco as complementary detection + Wazuh/SIEM correlation + WORM. Seccomp (Part 5) shrinks which syscalls remain; this part shrinks which programs may start. Sandboxes (Part 7) shrink where those programs can reach if they somehow start.


Correlating Kills With Panguard

{
  "schemaVersion": "1.0",
  "source": { "component": "tetragon", "namespace": "clawql-agents" },
  "principal": { "agentId": "agent_123", "sessionId": "sess_456" },
  "event": {
    "type": "POLICY",
    "subtype": "UNAUTHORIZED_EXEC_KILLED",
    "outcome": "BLOCKED",
    "severity": "HIGH"
  },
  "detail": {
    "policy": "clawql-kill-unauthorized-child-exec",
    "binary": "/usr/bin/curl",
    "parentBinary": "/usr/bin/node",
    "action": "SIGKILL",
    "exitCode": 137
  },
  "traceContext": { "traceId": "…" }
}

Correlation patterns:

PatternReading
Panguard allow(run_tests) + Tetragon killApproved tool spawned unauthorized child
Panguard block + Tetragon dangerous execInjection or runtime probing both layers
Tetragon kill(npx/pip) + egress/DNS denySupply-chain / exfil attempt

Flow: Tetragon / Falco → Fluent Bit or OTel Collector → Presidio redaction if needed → WORM audit → Wazuh / SIEM dashboards + quarantine hooks.


Environment Matrix

EnvironmentPrimary containment
KubernetesTetragon on Linux nodes + Kata/gVisor RuntimeClass
Linux edge / bareLocal Tetragon TracingPolicies under /etc/tetragon/
macOS laptop agentSeatbelt / clawql sandbox — not Tetragon

Seatbelt limits workspace paths and fork/exec on developer Macs. It’s not a production substitute for Kata or Tetragon — same defense-in-depth idea, different OS primitives.


Honest Failure Modes

Denylists are incomplete. Renamed curl to /tmp/helper bypasses Postfix. Prefer absent binaries + read-only rootfs + kill of remaining high-confidence names.

Sigkill is blunt. Agents see 137. Teach the runtime to report “blocked by process policy” rather than inventing workarounds.

Observe before enforce. npm scripts spawning sh, pytest helpers, and git hooks will surprise you.

Platform constraints. Tetragon needs Linux eBPF/BTF. Some managed node images restrict probes. Kernel function names for advanced hooks can vary — pin policy versions to platform AMI/OS.

Noise. Logging every execve without selectors drowns SIEM budget. Scope selectors to agent binaries and namespaces.


Getting Started

Inventory agent/tool images — remove shells and package managers from runtime roles that don’t need them. Deploy Tetragon; ship observe-only TracingPolicies for agent parents and dangerous/supply-chain sets. Baseline one week; document legitimate children per role. Promote high-confidence binaries to Sigkill + Post. Wire events into Fluent Bit / OTel → WORM → SIEM with session correlation. Teach the agent UX to surface policy kills rather than mute-retry.

Part 5: blocking binaries is not enough when the binary you allow still speaks too many syscalls.


Companion: DevSecOps-boilerplate. Docs: Sandboxing · Panguard · Observability.

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.