Agent Safety22 min read

Immutable Host Boundary: File Integrity Monitoring for Agents

Define filesystem no-go zones 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.

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

LayerSeesMisses
Prompt / modelNatural languagePath traversal dressed as “just read the config”
PanguardTool name + paramsWhether the path is a crown jewel
Tetragon exec (Part 4)New binariesReads inside an allowlisted runtime
Seccomp (Part 5)Syscall vocabularyWhich pathname an allowed openat targets
Path / FIM (this part)Sensitive opens, writes, integrityLogic bugs that only read still-allowed workspace I/O
Sidecars (Part 7)Where code runsHost 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 / patternConfidentialityIntegrityAgent policy
Agent workspace (/workspace, repo root)Low–medMedRead/write as ATR allows
Agent memory / vault dir (scoped)HighHighExplicit allow; encrypt at rest (Part 13)
~/.ssh, ~/.aws, ~/.gnupgCriticalHighDeny read+write
~/.kube, */.kube/configCriticalHighDeny read+write
/etc/shadow, /etc/sudoers*CriticalCriticalDeny read+write
/var/run/secrets/kubernetes.io/**CriticalHighDeny to agent containers; gateway only
Host Docker socket / containerd socketCriticalCriticalNever mount into agent
Package caches (~/.npm, pip cache)MedMedPrefer absent; alert on surprise writes
Agent launcher / unit files / crontabMedCriticalDeny 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

SignalLayerReading
Panguard allow(file_read)ProtocolIntent was in-policy
Tetragon kill(npx)ExecIntent escalated into a forbidden binary
Seccomp deny(ptrace)SyscallEscape-adjacent vocabulary
Path deny / FIM alert (~/.ssh/…)FilesystemIntent 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

EnvironmentPrimary path boundary
KubernetesNo crown-jewel mounts; Tetragon/Falco on nodes; Wazuh on node host where managed
Linux edge / bareTetragon path policy + Wazuh syscheck on home and /etc
macOS laptop agentSeatbelt 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.

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.