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.
| Layer | Sees | Misses |
|---|---|---|
| Prompt / model | Natural language | Everything after the decision |
| Panguard | Named tool + params + claims | Children spawned inside an allowed handler |
| Tool handler | Its own spawn / exec | Scripts and postinstalls it did not expect |
| Kernel / Tetragon | Actual execve paths and process tree | Nothing 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
| Role | Allow (examples) | Forbid at runtime (examples) |
|---|---|---|
| Agent runtime | node, clawql-agent, maybe git | curl, wget, npx, pip, shells |
| Test sidecar | node, npm (if contract), git | npx, curl, pip, arbitrary bash |
| Python tool sidecar | python3, pytest | Runtime pip install, curl, shells |
| Build image (CI only) | Toolchain as needed | Never 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:
| Pattern | Reading |
|---|---|
| Panguard allow(run_tests) + Tetragon kill | Approved tool spawned unauthorized child |
| Panguard block + Tetragon dangerous exec | Injection or runtime probing both layers |
| Tetragon kill(npx/pip) + egress/DNS deny | Supply-chain / exfil attempt |
Flow: Tetragon / Falco → Fluent Bit or OTel Collector → Presidio redaction if needed → WORM audit → Wazuh / SIEM dashboards + quarantine hooks.
Environment Matrix
| Environment | Primary containment |
|---|---|
| Kubernetes | Tetragon on Linux nodes + Kata/gVisor RuntimeClass |
| Linux edge / bare | Local Tetragon TracingPolicies under /etc/tetragon/ |
| macOS laptop agent | Seatbelt / 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.
