Define filesystem no-go zones (.env, SSH keys, kubeconfig) and block agent reads at the host policy layer — Tetragon and Wazuh FIM enforce an Immutable Host Boundary when an allowlisted, diet-constrained runtime still goes looking for secrets on disk.
Part 4 decided which programs may start. Part 5 decided which syscalls those programs may speak. This post answers the residual question: when the allowlisted, diet-constrained runtime still calls openat, which paths are crown jewels — and who blocks the read before the bytes become model context?
The Read That Never Needed curl
Parts 4–5 look green. No npx. No raw sockets. The agent summarizes a repo under /workspace. Then a poisoned README, a “helpful” skill, or a confused tool argument nudges:
file_read("/home/runner/.ssh/id_rsa")
file_read("/home/runner/.kube/config")
file_read("/var/run/secrets/kubernetes.io/serviceaccount/token")
file_read("/workspace/../.env")
Panguard may even allow file_read — ATR said the agent can read files. Seccomp allows openat. Tetragon never sees a new binary. The crown jewels walk into the context window as ordinary UTF-8.
On a developer Mac, Seatbelt already treats this as a first-class deny. On Linux hosts and nodes, that deny has to be an explicit host policy — not a hopeful .gitignore and not a system prompt that says “don’t look.”
If your security story ends at “no shells and a short syscall diet,” you’re still one openat away from shipping the kubeconfig to the prompt.
After the Diet, the Filesystem
| Layer | Sees | Misses |
|---|---|---|
| Prompt / model | Natural language | Path traversal dressed as “just read the config” |
| Panguard | Tool name + params | Whether the path is a crown jewel |
| Tetragon exec (Part 4) | New binaries | Reads inside an allowlisted runtime |
| Seccomp (Part 5) | Syscall vocabulary | Which pathname an allowed openat targets |
| Path / FIM (this part) | Sensitive opens, writes, integrity | Logic bugs that only read still-allowed workspace I/O |
| Sidecars (Part 7) | Where code runs | Host paths bind-mounted into the sandbox carelessly |
Classic residual cases Parts 4–5 don’t close: ATR allows file_read but the argument is ~/.ssh/id_rsa; traversal and symlink games (/workspace/../.env, bind mounts, skill-install symlinks); in-cluster service account tokens still reachable as a file; and agents that write into /etc, crontab, or their own launcher — a different class of problem FIM must cover.
Panguard guards the verb. FIM guards the noun.
The Architecture Pattern: Immutable Host Boundary
Enumerate crown-jewel paths for every agent role — human home dirs on edge hosts; service-account and host secrets on nodes. Prefer absence (Part 2 dynamic secrets, no static .env next to the workspace) over detection. For paths that must remain: enforce no-go at the host policy layer, blocking open/read/write for the agent UID rather than hashing and hoping. Run integrity monitoring so silent mutation of those paths pages someone. Emit denials and integrity events into the same WORM / SIEM path as Panguard and Tetragon. Never bind-mount $HOME, Docker socket, or kubeconfig into an exec sidecar.
Agent / tool handler (allowed binary, short syscall diet)
│
▼
openat("~/.ssh/id_rsa", O_RDONLY)
│
▼
Host path policy (Tetragon / Falco / Seatbelt)
│
├── deny → EPERM + Post → WORM / SIEM
└── allow only if path ∈ workspace (or explicit allow)
│
▼
Wazuh syscheck (realtime / scheduled)
└── hash / ownership change on crown jewels → alert
Absence beats monitoring. Monitoring beats prayer. Monitoring without block is a diary of your breach.
Watched-Path Map
Draw the map before you write YAML. For a Linux edge agent running as clawql or a K8s service account:
| Path / pattern | Confidentiality | Integrity | Agent policy |
|---|---|---|---|
Agent workspace (/workspace, repo root) | Low–med | Med | Read/write as ATR allows |
| Agent memory / vault dir (scoped) | High | High | Explicit allow; encrypt at rest (Part 13) |
~/.ssh, ~/.aws, ~/.gnupg | Critical | High | Deny read+write |
~/.kube, */.kube/config | Critical | High | Deny read+write |
/etc/shadow, /etc/sudoers* | Critical | Critical | Deny read+write |
/var/run/secrets/kubernetes.io/** | Critical | High | Deny to agent containers; gateway only |
| Host Docker socket / containerd socket | Critical | Critical | Never mount into agent |
Package caches (~/.npm, pip cache) | Med | Med | Prefer absent; alert on surprise writes |
| Agent launcher / unit files / crontab | Med | Critical | Deny write; FIM realtime |
On macOS, the Seatbelt profile already encodes the left column as (deny file-read* …) subpaths. Your Linux map should be at least as strict — written down, owned, and tested with a deliberate openat.
Block First: Tetragon on Sensitive Opens
Alert-only FIM is insufficient. Start with an observe policy, then promote to kill/override for the agent binary touching crown jewels.
Observe (baseline what the agent already touches):
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
name: clawql-sensitive-open-observe
spec:
kprobes:
- call: 'sys_openat'
syscall: true
args:
- index: 0
type: 'int'
- index: 1
type: 'string'
- index: 2
type: 'int'
selectors:
- matchBinaries:
- operator: 'In'
values:
- '/usr/bin/node'
- '/usr/local/bin/node'
- '/app/clawql-agent'
matchArgs:
- index: 1
operator: 'Prefix'
values:
- '/root/.ssh'
- '/home/'
- '/etc/shadow'
- '/var/run/secrets/kubernetes.io'
matchActions:
- action: Post
Then tighten — prefer explicit prefixes over broad /home/ (which drowns you in workspace noise if homes hold projects):
matchArgs:
- index: 1
operator: 'Prefix'
values:
- '/home/clawql/.ssh'
- '/home/clawql/.aws'
- '/home/clawql/.kube'
- '/root/.ssh'
- '/etc/shadow'
- '/etc/sudoers'
- '/var/run/secrets/kubernetes.io'
matchActions:
- action: Sigkill
- action: Post
Exact action names and arg indexes depend on your Tetragon / kernel version — pin the policy to the platform the same way you pin seccomp profiles in Part 5. Some environments prefer Override returning -EPERM so the runtime can surface “blocked by path policy” rather than a hard 137.
Falco ships the same idea as “sensitive file read outside declared paths.” Use one runtime sensor on the node — Tetragon or Falco for path blocks — and send both denials and Wazuh integrity events into Alloy / SIEM. Two overlapping kill policies without clear ownership is how you train yourself to ignore both.
If the first time you learn about ~/.kube/config is a Langfuse span that already contains the YAML, you built a journal, not a boundary.
Integrity: Wazuh syscheck
Blocking opens stops the agent UID. Integrity monitoring catches everyone else — including root kits, bad deploy hooks, and humans who “just edited the agent unit file.”
<ossec_config>
<syscheck>
<disabled>no</disabled>
<frequency>300</frequency>
<directories check_all="yes" realtime="yes">/etc</directories>
<directories check_all="yes" realtime="yes">/root/.ssh</directories>
<directories check_all="yes" realtime="yes">/home/clawql/.ssh</directories>
<directories check_all="yes" realtime="yes">/home/clawql/.kube</directories>
<directories check_all="yes" realtime="yes">/var/run/secrets</directories>
<directories check_all="yes" realtime="yes">/etc/systemd/system</directories>
<ignore>/etc/mtab</ignore>
<ignore type="sregex">.log$|.swp$</ignore>
</syscheck>
<localfile>
<log_format>syslog</log_format>
<location>/var/log/auth.log</location>
</localfile>
<active-response>
<command>firewall-drop</command>
<location>local</location>
<level>12</level>
<timeout>600</timeout>
</active-response>
</ossec_config>
Pipeline:
Tetragon / Falco (path deny)
→ Fluent Bit or OTel Collector
Wazuh agent (syscheck + auth logs)
→ Wazuh manager
→ Alloy
→ Loki / SIEM + WORM
When a sensitive open is blocked, log the path class and rule id, not the file contents. Confirming “agent tried to read ssh private key” doesn’t require pasting the key into Loki.
Absence Still Wins
FIM doesn’t excuse bad residency. Prefer dynamic secrets and short leases over a .env in the parent of /workspace. Don’t mount the host Docker socket or a broad $HOME into agent pods. Keep kubeconfigs and cloud creds out of the agent filesystem; exchange at the gateway (Part 3 scopes + secrets module). If memory must live on disk, encrypt it and treat writes as classification events (Part 13) — FIM watches the store; it doesn’t make plaintext acceptable.
The best no-go zone is an empty path. FIM is for the remainder you couldn’t delete.
How Denials Join Panguard and Tetragon
| Signal | Layer | Reading |
|---|---|---|
| Panguard allow(file_read) | Protocol | Intent was in-policy |
| Tetragon kill(npx) | Exec | Intent escalated into a forbidden binary |
| Seccomp deny(ptrace) | Syscall | Escape-adjacent vocabulary |
| Path deny / FIM alert (~/.ssh/…) | Filesystem | Intent reached for crown jewels |
Correlation worth shipping on day one: Panguard allow(file_read) + Tetragon/Falco path deny within the same session indicates prompt or tooling probing secrets. Wazuh integrity change on agent unit files plus unexpected egress indicates a persistence attempt. A path deny storm from one agent UID should trigger quarantine and lease revocation (Parts 2 and 15).
Session, pod, agent UID, rule id, and path class on every deny — enough to join without leaking contents.
Environment Matrix
| Environment | Primary path boundary |
|---|---|
| Kubernetes | No crown-jewel mounts; Tetragon/Falco on nodes; Wazuh on node host where managed |
| Linux edge / bare | Tetragon path policy + Wazuh syscheck on home and /etc |
| macOS laptop agent | Seatbelt deny lists for ~/.ssh, ~/.aws, … |
Don’t cargo-cult Seatbelt onto Linux nodes. A Wazuh scan every 12 hours isn’t a substitute for a synchronous openat deny when the agent is holding a shell-equivalent tool. Part 7 (sidecars / Kata / gVisor) shrinks where leftover code runs — but bind-mounting $HOME into the sidecar recreates this problem inside a prettier runtime. Path policy and mount hygiene travel together.
Honest Failure Modes
Prefix rules are clumsy. /home/ is too wide; a single file allowlist is too narrow. Maintain an owned path inventory per role and test with intentional denies.
Workspace vs secrets adjacency. Developers love $HOME/projects/app next to $HOME/.ssh. Path policy must deny the latter even when ATR allows broad reads under “the project.” Prefer a dedicated work root.
Symlinks and openat. Attackers will alias sensitive files into the workspace. Prefer policies that evaluate the real path where your sensor supports it; treat unexpected symlinks into no-go zones as incidents.
Integrity noise. /etc realtime FIM without ignores will page on DHCP leases and noisy writable files. Ignore thoughtfully; never ignore shadow, ssh, kube, systemd units, or cron.
Block vs alert. Hash-only FIM tells you after the model already ingested the key. Aim for deny on read for crown jewels; use integrity monitoring for mutation and for principals other than the agent.
Multi-tenant nodes. Path policy must be labeled by workload. A deny for agent A must not require turning off observability for the whole node.
Getting Started
Inventory crown jewels on every host / node pool that runs agents (ssh, cloud creds, kubeconfig, SA tokens, unit files). Delete or relocate what you can (Part 2); stop bind-mounting the rest into agent sandboxes. Draft the watched-path map per role; peer-review it like an ATR change. Ship Tetragon/Falco observe → deny on agent binary plus sensitive prefixes; canary test. Enable Wazuh syscheck realtime on the same set; forward to Alloy → SIEM / WORM without file contents. Teach the agent UX to surface “blocked by path policy,” and page on deny storms and integrity changes.
Part 7: move unsafe tools off the agent host into ephemeral sidecars so even a missed path deny isn’t this host’s problem.
Companion: DevSecOps-boilerplate. Docs: Security monitoring / SIEM · Secrets at rest · Seatbelt essay.
