<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Sandbox on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/sandbox/</link><description>Recent content in Sandbox on lo0 — Tech Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Tue, 09 Jun 2026 17:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/en/tags/sandbox/index.xml" rel="self" type="application/rss+xml"/><item><title>Runbook: caging the AI agent — bubblewrap on the client, Tetragon on the cluster</title><link>https://blog.lo0.es/en/posts/runbook-caging-ai-agent-bubblewrap-tetragon/</link><pubDate>Tue, 09 Jun 2026 17:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/runbook-caging-ai-agent-bubblewrap-tetragon/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>Operational&lt;/strong> companion to &lt;a href="https://blog.lo0.es/en/posts/isolating-ai-agents-workstation-to-cluster/">The contractor with the master key&lt;/a>. That post explains the &lt;em>why&lt;/em> and the &lt;em>where&lt;/em>: the threat model, the five isolation families, which domain uses each. This one is the &lt;em>how&lt;/em>, with commands. If you have not read it, read it first: here I take for granted what the blast radius is, why &lt;code>bwrap&lt;/code> runs without root and what Tetragon watches. The procedure comes in two independent tracks, &lt;strong>client&lt;/strong> and &lt;strong>cluster&lt;/strong>, because, as the sibling post argues, the control extrapolates but the primitive gets rewritten.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Two reproducible procedures. &lt;strong>Client (workstation):&lt;/strong> install &lt;code>ai-jail&lt;/code> (it wraps &lt;code>bubblewrap&lt;/code>), generate the per-project &lt;code>.ai-jail&lt;/code>, audit with &lt;code>--dry-run&lt;/code>, fix the allowlists with &lt;code>--bootstrap&lt;/code>, use &lt;code>--lockdown&lt;/code> for anything you do not trust, and leave the agent without &lt;code>git push&lt;/code> permission. &lt;strong>Cluster (RKE2 with Cilium + Tetragon):&lt;/strong> put the pod baseline in place (unprivileged &lt;code>securityContext&lt;/code>, &lt;code>seccomp: RuntimeDefault&lt;/code>, default-deny &lt;code>NetworkPolicy&lt;/code>), put the untrusted agent&amp;rsquo;s pod in a microVM with &lt;code>runtimeClassName: kata&lt;/code>, and deploy the Tetragon &lt;code>TracingPolicy&lt;/code> files in &lt;strong>two phases&lt;/strong>: observe with &lt;code>action: Post&lt;/code> to raise the baseline, then promote to &lt;code>action: Sigkill&lt;/code> on &lt;code>tcp_connect&lt;/code> (egress) and &lt;code>security_file_open&lt;/code> (secret paths). The golden rule of the Tetragon phase: &lt;strong>adopt first, block later&lt;/strong>; never put a &lt;code>Sigkill&lt;/code> into production without having seen the events in observation mode first.&lt;/p>
&lt;h2 id="the-flow-of-the-two-tracks">The flow of the two tracks&lt;/h2>
&lt;div class="diagram" style="max-width:800px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 800 250" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Two operational tracks: client (install, configure, bootstrap, lockdown) and cluster (baseline, RuntimeClass, observe, enforce)">
&lt;defs>&lt;marker id="rm2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="400" y="22" text-anchor="middle" font-family="sans-serif" font-size="13" font-weight="700" fill="currentColor">CLIENT track — workstation&lt;/text>
&lt;rect x="20" y="36" width="150" height="46" rx="7" fill="#d4ecff" stroke="#1f5fa8" stroke-width="1.4"/>
&lt;text x="95" y="56" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="600" fill="#0d3a66">1 · Install&lt;/text>
&lt;text x="95" y="72" text-anchor="middle" font-family="sans-serif" font-size="9.5" fill="#0d3a66">ai-jail + bwrap&lt;/text>
&lt;rect x="200" y="36" width="150" height="46" rx="7" fill="#d4ecff" stroke="#1f5fa8" stroke-width="1.4"/>
&lt;text x="275" y="56" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="600" fill="#0d3a66">2 · .ai-jail&lt;/text>
&lt;text x="275" y="72" text-anchor="middle" font-family="sans-serif" font-size="9.5" fill="#0d3a66">--dry-run&lt;/text>
&lt;rect x="380" y="36" width="150" height="46" rx="7" fill="#d4ecff" stroke="#1f5fa8" stroke-width="1.4"/>
&lt;text x="455" y="56" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="600" fill="#0d3a66">3 · --bootstrap&lt;/text>
&lt;text x="455" y="72" text-anchor="middle" font-family="sans-serif" font-size="9.5" fill="#0d3a66">allow/deny/ask&lt;/text>
&lt;rect x="560" y="36" width="150" height="46" rx="7" fill="#d4ecff" stroke="#1f5fa8" stroke-width="1.4"/>
&lt;text x="635" y="52" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="600" fill="#0d3a66">4 · lockdown&lt;/text>
&lt;text x="635" y="68" text-anchor="middle" font-family="sans-serif" font-size="9.5" fill="#0d3a66">+ git with no push&lt;/text>
&lt;path d="M170,59 L198,59" stroke="#666" stroke-width="1.5" fill="none" marker-end="url(#rm2)"/>
&lt;path d="M350,59 L378,59" stroke="#666" stroke-width="1.5" fill="none" marker-end="url(#rm2)"/>
&lt;path d="M530,59 L558,59" stroke="#666" stroke-width="1.5" fill="none" marker-end="url(#rm2)"/>
&lt;line x1="20" y1="118" x2="780" y2="118" stroke="#ccc" stroke-width="1" stroke-dasharray="3 3"/>
&lt;text x="400" y="150" text-anchor="middle" font-family="sans-serif" font-size="13" font-weight="700" fill="currentColor">CLUSTER track — RKE2 + Cilium/Tetragon&lt;/text>
&lt;rect x="20" y="164" width="150" height="46" rx="7" fill="#e6d9f2" stroke="#5a2db0" stroke-width="1.4"/>
&lt;text x="95" y="184" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="600" fill="#42208a">1 · Baseline&lt;/text>
&lt;text x="95" y="200" text-anchor="middle" font-family="sans-serif" font-size="9.5" fill="#42208a">secCtx+NetPol&lt;/text>
&lt;rect x="200" y="164" width="150" height="46" rx="7" fill="#e6d9f2" stroke="#5a2db0" stroke-width="1.4"/>
&lt;text x="275" y="184" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="600" fill="#42208a">2 · RuntimeClass&lt;/text>
&lt;text x="275" y="200" text-anchor="middle" font-family="sans-serif" font-size="9.5" fill="#42208a">kata microVM&lt;/text>
&lt;rect x="380" y="164" width="150" height="46" rx="7" fill="#fde9d6" stroke="#a85a00" stroke-width="1.6"/>
&lt;text x="455" y="184" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="600" fill="#8a4a00">3 · Observe&lt;/text>
&lt;text x="455" y="200" text-anchor="middle" font-family="sans-serif" font-size="9.5" fill="#8a4a00">Tetragon · Post&lt;/text>
&lt;rect x="560" y="164" width="150" height="46" rx="7" fill="#fbd4b8" stroke="#a85a00" stroke-width="1.8"/>
&lt;text x="635" y="184" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="600" fill="#8a4a00">4 · Enforce&lt;/text>
&lt;text x="635" y="200" text-anchor="middle" font-family="sans-serif" font-size="9.5" fill="#8a4a00">Tetragon · Sigkill&lt;/text>
&lt;path d="M170,187 L198,187" stroke="#666" stroke-width="1.5" fill="none" marker-end="url(#rm2)"/>
&lt;path d="M350,187 L378,187" stroke="#666" stroke-width="1.5" fill="none" marker-end="url(#rm2)"/>
&lt;path d="M530,187 L558,187" stroke="#666" stroke-width="1.5" fill="none" marker-end="url(#rm2)"/>
&lt;text x="400" y="236" text-anchor="middle" font-family="sans-serif" font-size="10" font-style="italic" fill="#555">adopt first (observe), block later (enforce)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;hr>
&lt;h1 id="track-a--client-the-developers-workstation">Track A — Client (the developer&amp;rsquo;s workstation)&lt;/h1>
&lt;h2 id="a0--install-ai-jail-and-bubblewrap">A0 — Install ai-jail and bubblewrap&lt;/h2>
&lt;p>&lt;code>ai-jail&lt;/code> wraps the sandbox; on Linux it needs &lt;code>bubblewrap&lt;/code> separately, on macOS it needs no extra dependency.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># ai-jail (macOS and Linux)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">brew tap akitaonrails/tap &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> brew install ai-jail
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># or, with cargo:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cargo install ai-jail
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># or, with mise:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">mise use -g ubi:akitaonrails/ai-jail
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># bubblewrap on Linux (pick your distro)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo pacman -S bubblewrap &lt;span class="c1"># Arch&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo apt install bubblewrap &lt;span class="c1"># Debian / Ubuntu&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo dnf install bubblewrap &lt;span class="c1"># Fedora&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Check the binary is there and that &lt;code>bwrap&lt;/code> runs without root (it must not ask for &lt;code>sudo&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">ai-jail --version
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">bwrap --ro-bind / / --unshare-all &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;bwrap ok without root&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If &lt;code>bwrap&lt;/code> fails asking for privileges, your kernel has &lt;em>unprivileged user namespaces&lt;/em> disabled; enable them (&lt;code>sysctl kernel.unprivileged_userns_clone=1&lt;/code> on older Debian/Ubuntu) before continuing.&lt;/p>
&lt;h2 id="a1--the-per-project-ai-jail-file">A1 — The per-project .ai-jail file&lt;/h2>
&lt;p>On the first launch inside the project, &lt;code>ai-jail&lt;/code> creates a &lt;code>.ai-jail&lt;/code> (TOML) that is &lt;strong>committable to the repo&lt;/strong>: any colleague who clones inherits the same policy.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> ~/Projects/mi-app
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ai-jail claude &lt;span class="c1"># creates .ai-jail and launches Claude Code inside the sandbox&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The generated file:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># .ai-jail — sandbox configuration (commit it to the repo)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">command&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;claude&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">rw_maps&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">[]&lt;/span> &lt;span class="c"># extra writable directories&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">ro_maps&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">[]&lt;/span> &lt;span class="c"># extra read-only directories&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Before trusting the sandbox, audit it.&lt;/strong> &lt;code>--dry-run --verbose&lt;/code> prints every mount point, every isolation flag and the full &lt;code>bwrap&lt;/code> command, without executing anything:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">ai-jail --dry-run --verbose claude
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Read the output and confirm three things: that &lt;code>$HOME&lt;/code> is mounted as a tmpfs (not the real one), that &lt;code>~/.ssh&lt;/code>, &lt;code>~/.aws&lt;/code> and &lt;code>~/.gnupg&lt;/code> &lt;strong>do not appear&lt;/strong> among the mounts, and that the only writable directory is the project one. If you need an extra directory:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">ai-jail --rw-map ~/Projects/shared-lib claude &lt;span class="c1"># extra, writable&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ai-jail --map /opt/datasets claude &lt;span class="c1"># extra, read-only&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Other agents, same binary:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">ai-jail codex
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ai-jail opencode
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ai-jail bash &lt;span class="c1"># bare shell to debug the sandbox&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ai-jail -- python script.py &lt;span class="c1"># any command&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="a2--permission-allowlists-with---bootstrap">A2 — Permission allowlists with &amp;ndash;bootstrap&lt;/h2>
&lt;p>&lt;code>--bootstrap&lt;/code> generates each agent&amp;rsquo;s permission configuration, with sensible allow/deny/ask, and takes a backup before overwriting:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">ai-jail --bootstrap
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>What it produces, in summary:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Agent&lt;/th>
&lt;th>File&lt;/th>
&lt;th>Base policy&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Claude Code&lt;/td>
&lt;td>&lt;code>~/.claude/settings.json&lt;/code>&lt;/td>
&lt;td>&lt;strong>allow&lt;/strong>: &lt;code>git status/diff/log&lt;/code>, &lt;code>ls&lt;/code>, &lt;code>grep&lt;/code>, &lt;code>cargo&lt;/code>, &lt;code>npm&lt;/code>, &lt;code>python&lt;/code>, &lt;code>docker compose&lt;/code> · &lt;strong>ask&lt;/strong>: &lt;code>git push&lt;/code>, &lt;code>rm&lt;/code>, &lt;code>docker run&lt;/code> · &lt;strong>deny&lt;/strong>: &lt;code>rm -rf&lt;/code>, &lt;code>sudo&lt;/code>, &lt;code>chmod 777&lt;/code>, &lt;code>git push --force&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Codex&lt;/td>
&lt;td>&lt;code>~/.codex/config.toml&lt;/code>&lt;/td>
&lt;td>&lt;code>approval_policy = &amp;quot;on-request&amp;quot;&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>OpenCode&lt;/td>
&lt;td>&lt;code>~/.config/opencode/opencode.json&lt;/code>&lt;/td>
&lt;td>permissions for &lt;code>bash&lt;/code>, &lt;code>edit&lt;/code>, &lt;code>write&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The operational key: &lt;code>git push&lt;/code> is under &lt;strong>ask&lt;/strong>, not &lt;strong>allow&lt;/strong>, and &lt;code>git push --force&lt;/code> under &lt;strong>deny&lt;/strong>. The agent can commit, branch and rebase locally as much as it wants; none of that touches the remote. (If you use the Claude Code &lt;code>/sandbox&lt;/code>, also set &lt;code>&amp;quot;allowUnsandboxedCommands&amp;quot;: false&lt;/code> to close the &lt;code>dangerouslyDisableSandbox&lt;/code> &lt;em>escape hatch&lt;/em>, which is opt-out out of the box.)&lt;/p>
&lt;h2 id="a3--lockdown-for-anything-you-do-not-trust">A3 — Lockdown for anything you do not trust&lt;/h2>
&lt;p>To audit third-party code or run an agent over a project you do not know, &lt;code>--lockdown&lt;/code> goes further: project mounted &lt;strong>read-only&lt;/strong>, GPU/Docker/display disabled, &lt;code>--rw-map&lt;/code>/&lt;code>--map&lt;/code> ignored, &lt;code>$HOME&lt;/code> a pure tmpfs with no host dotfiles, network cut with &lt;code>--unshare-net&lt;/code> and the environment wiped with &lt;code>--clearenv&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">ai-jail --lockdown bash
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>It is the most restrictive sandbox possible short of a VM. Use it as your mental default for anything that is not your own code on your own machine.&lt;/p>
&lt;h2 id="a4--the-safety-net-git-with-no-push">A4 — The safety net: git with no push&lt;/h2>
&lt;p>It is not a flag, it is a property of the environment that changes the risk arithmetic. If the project is in git with a remote, and the agent does &lt;strong>not&lt;/strong> have &lt;code>push&lt;/code> permission, the worst case, that it corrupts every file in the project, is reverted with:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git checkout . &lt;span class="c1"># back to the last commit&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># and if it touched .git (unlikely): delete the dir and re-clone&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The remote was never touched. &lt;strong>Sandbox for the filesystem + git for the code + manual push&lt;/strong> is already a reasonable level for daily use: &lt;code>ai-jail&lt;/code> protects your data and the system, git protects the code, and the decision to publish is still yours.&lt;/p>
&lt;hr>
&lt;h1 id="track-b--cluster-rke2-with-cilium--tetragon">Track B — Cluster (RKE2 with Cilium + Tetragon)&lt;/h1>
&lt;p>The untrusted agent, or the inference that executes generated code, runs as a pod. The same principle as on the client, different primitives. We assume a generic RKE2 cluster with Cilium as CNI and Tetragon already deployed (the eBPF agent &lt;code>DaemonSet&lt;/code> on every node).&lt;/p>
&lt;h2 id="b0--the-pod-baseline">B0 — The pod baseline&lt;/h2>
&lt;p>Before any eBPF, the standard kit. Unprivileged &lt;code>securityContext&lt;/code>, read-only root, default seccomp:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Pod&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ai-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentes&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ai-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">securityContext&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runAsNonRoot&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runAsUser&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10001&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">seccompProfile&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RuntimeDefault&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">registry.interno/ai-agent:pinned&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">securityContext&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowPrivilegeEscalation&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">readOnlyRootFilesystem&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">capabilities&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">drop&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;ALL&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumeMounts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">name: work, mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/work } &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># the only writable one&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">work&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">emptyDir&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And the default egress cut, the cluster twin of &lt;code>--unshare-net&lt;/code>. A default-deny egress NetworkPolicy in the namespace, opening only DNS and the bare essentials:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">NetworkPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-deny-egress&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentes&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">podSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policyTypes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;Egress&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">egress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">to&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">namespaceSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">kubernetes.io/metadata.name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kube-system }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol: UDP, port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">53&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol: TCP, port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">53&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="b1--kata-runtimeclass-the-untrusted-pod-in-its-own-microvm">B1 — Kata RuntimeClass: the untrusted pod in its own microVM&lt;/h2>
&lt;p>For genuinely untrusted code, take it out of the shared kernel. With Kata deployed there is a &lt;code>RuntimeClass&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">node.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RuntimeClass&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kata&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">handler&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kata&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And the pod asks for it with one line, &lt;code>runtimeClassName: kata&lt;/code>, running in its own microVM with a dedicated kernel instead of sharing the node&amp;rsquo;s:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runtimeClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kata &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># ← the pod runs in a microVM, not on the node&amp;#39;s kernel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># ...the rest the same as B0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>It is the cluster twin of isolation by construction: a kernel exploit inside the pod does not reach the node.&lt;/p>
&lt;h2 id="b2--tetragon-observation-phase-post">B2 — Tetragon, observation phase (Post)&lt;/h2>
&lt;p>Now the layer that separates a platform with runtime visibility from one without. &lt;strong>Observe first, never kill from the outset.&lt;/strong> A &lt;code>TracingPolicyNamespaced&lt;/code>, scoped to the namespace and to the agent&amp;rsquo;s label, that reports (does not kill) three things: process executions, network connections and opens of sensitive paths. &lt;code>action: Post&lt;/code> only emits the event.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cilium.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TracingPolicyNamespaced&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agente-observa&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentes&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">podSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ai-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kprobes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># --- outbound connections ---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">call&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;tcp_connect&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">syscall&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">index&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;sock&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selectors&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matchActions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">action&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Post&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># --- opens of sensitive files ---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">call&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;security_file_open&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">syscall&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">index&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;file&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selectors&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matchArgs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">index&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">operator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Prefix&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">values&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;/var/run/secrets&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;/work/.git/config&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchActions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">action&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Post&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>(Process executions need no kprobe: Tetragon emits &lt;code>process_exec&lt;/code>/&lt;code>process_exit&lt;/code> natively.) Deploy it and watch the events live from the node&amp;rsquo;s Tetragon pod:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f agente-observa.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># readable events, filtered by namespace:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">exec&lt;/span> -n kube-system ds/tetragon -c tetragon -- &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> tetra getevents -o compact --namespace agentes
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Leave this running for a typical working day of the agent. Note &lt;strong>which destinations&lt;/strong> it really connects to (your internal registry, your HF mirror, your vLLM endpoint) and &lt;strong>which paths&lt;/strong> it opens. That is your baseline: the list of what is legitimate. Without this step, a &lt;code>Sigkill&lt;/code> kills good work and generates an availability incident, precisely what ENS asks you to avoid.&lt;/p>
&lt;h2 id="b3--tetragon-enforcement-phase-sigkill">B3 — Tetragon, enforcement phase (Sigkill)&lt;/h2>
&lt;p>With the baseline in hand, promote to blocking. Two rules. The first: &lt;strong>kill any connection whose destination is not on the allowlist&lt;/strong> — &lt;code>NotDAddr&lt;/code> inverts the match: it fires for everything that is &lt;em>not&lt;/em> those networks. The second: &lt;strong>kill any attempt to open a secrets path&lt;/strong>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cilium.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TracingPolicyNamespaced&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agente-enforce&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentes&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">podSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ai-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kprobes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># --- egress: kill everything that is NOT on the allowlist ---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">call&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;tcp_connect&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">syscall&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">index&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;sock&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selectors&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matchArgs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">index&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">operator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;NotDAddr&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">values&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;127.0.0.1&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;10.0.0.0/8&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># internal cluster network&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;172.16.10.20&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># internal registry (example)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchActions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">action&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Sigkill&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># --- reading secrets: kill the process ---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">call&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;security_file_open&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">syscall&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">index&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;file&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selectors&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matchArgs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">index&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">operator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Prefix&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">values&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;/var/run/secrets/kubernetes.io/serviceaccount/token&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;/work/.ssh&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchActions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">action&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Sigkill&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f agente-enforce.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now the agent can do whatever it likes inside the pod, but &lt;strong>the instant&lt;/strong> it tries to connect to a disallowed destination or read the service account token, Tetragon kills it in the kernel, before the packet leaves or the &lt;code>read&lt;/code> returns bytes. It is the cluster twin of the &lt;code>curl&lt;/code> blocklist and the unmounted &lt;code>~/.ssh&lt;/code>, but applied at runtime and over &lt;em>any&lt;/em> binary, not just the ones you know about.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Operational warning.&lt;/strong> Enforcement with &lt;code>Sigkill&lt;/code> requires a recent kernel with eBPF support for the action (5.10+ is safe). Deploy &lt;code>agente-enforce&lt;/code> in a test namespace first, and keep &lt;code>agente-observa&lt;/code> active in parallel: if the block fires, the &lt;code>Post&lt;/code> event tells you exactly what triggered it. Adopt first, block later.&lt;/p>
&lt;/blockquote>
&lt;h2 id="the-client--cluster-equivalence-table">The client ↔ cluster equivalence table&lt;/h2>
&lt;p>The same vector, the two primitives. This is &amp;ldquo;extrapolating the technology&amp;rdquo; made explicit:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Threat vector&lt;/th>
&lt;th>Client (workstation)&lt;/th>
&lt;th>Cluster (RKE2)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>$HOME&lt;/code> / writable root&lt;/td>
&lt;td>&lt;code>$HOME&lt;/code> as ephemeral tmpfs (&lt;code>bwrap&lt;/code>)&lt;/td>
&lt;td>&lt;code>readOnlyRootFilesystem: true&lt;/code> + &lt;code>emptyDir&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Arbitrary egress&lt;/td>
&lt;td>&lt;code>curl&lt;/code>/&lt;code>wget&lt;/code> blocklist · &lt;code>--unshare-net&lt;/code>&lt;/td>
&lt;td>default-deny NetworkPolicy + Tetragon &lt;code>NotDAddr&lt;/code>→&lt;code>Sigkill&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Reading secrets&lt;/td>
&lt;td>&lt;code>~/.ssh&lt;/code>/&lt;code>~/.aws&lt;/code>/&lt;code>~/.gnupg&lt;/code> not mounted&lt;/td>
&lt;td>secrets outside the pod + Tetragon &lt;code>security_file_open&lt;/code>→&lt;code>Sigkill&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Kernel escape&lt;/td>
&lt;td>Landlock (2nd VFS barrier)&lt;/td>
&lt;td>&lt;code>runtimeClassName: kata&lt;/code> (microVM, own kernel)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>No escape hatch&lt;/td>
&lt;td>process inside &lt;code>bwrap&lt;/code>, no way out&lt;/td>
&lt;td>no &lt;code>privileged&lt;/code>, &lt;code>drop ALL&lt;/code>, &lt;code>allowPrivilegeEscalation:false&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Damage to the code&lt;/td>
&lt;td>git remote with no &lt;code>push&lt;/code> → &lt;code>git checkout .&lt;/code>&lt;/td>
&lt;td>GitOps + PR review, the agent does not apply to &lt;code>main&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Visibility&lt;/td>
&lt;td>&lt;code>--dry-run --verbose&lt;/code> (static, pre-run)&lt;/td>
&lt;td>Tetragon &lt;code>tetra getevents&lt;/code> (dynamic, at runtime)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="checklist-of-gotchas">Checklist of gotchas&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Do not put in a &lt;code>Sigkill&lt;/code> without passing through &lt;code>Post&lt;/code>.&lt;/strong> The observation baseline is not optional: it is what separates &amp;ldquo;blocking a C2&amp;rdquo; from &amp;ldquo;killing your own fine-tuning job&amp;rdquo;.&lt;/li>
&lt;li>&lt;strong>The &lt;code>.ai-jail&lt;/code> gets committed; secrets do not.&lt;/strong> The TOML is policy, not credentials. Check you are not putting paths holding sensitive data into &lt;code>rw_maps&lt;/code>.&lt;/li>
&lt;li>&lt;strong>&lt;code>readOnlyRootFilesystem&lt;/code> breaks apps that write to &lt;code>/tmp&lt;/code>.&lt;/strong> Mount an &lt;code>emptyDir&lt;/code> at &lt;code>/tmp&lt;/code> as well as the work one.&lt;/li>
&lt;li>&lt;strong>A NetworkPolicy with no DNS rule leaves the pod blind.&lt;/strong> Open port 53 to &lt;code>kube-system&lt;/code> or nothing resolves.&lt;/li>
&lt;li>&lt;strong>Kata is not free.&lt;/strong> It adds startup latency and not every workload with special devices (GPU passthrough) fits; reserve it for the untrusted, not for everything.&lt;/li>
&lt;li>&lt;strong>The Claude Code &lt;code>/sandbox&lt;/code> does not cover MCP or hooks&lt;/strong> unless you enable &lt;code>sandbox-runtime&lt;/code>. If your agent uses MCP servers, assume they run with full permissions until you do.&lt;/li>
&lt;li>&lt;strong>&lt;code>NotDAddr&lt;/code> with literal IPs ages badly.&lt;/strong> Document the allowlist and review it when the registry or the inference endpoint changes; consider stable internal CIDRs instead of loose IPs.&lt;/li>
&lt;/ul>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/hardening-secrets-sovereign-llm-stack-defence-in-depth/">Hardening and secrets in the sovereign LLM stack: defence in depth&lt;/a> — the hardening of the whole cluster beyond agent isolation.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/isolating-ai-agents-workstation-to-cluster/">The contractor with the master key: isolating AI agents from workstation to cluster&lt;/a> — the landscape this runbook executes: threat model, the five isolation families and why client and cluster use different primitives.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/network-numa-cilium-ebpf-dranet-fourth-leg-pinning/">The kitchen door the maître never checked: Cilium eBPF and DRANET&lt;/a> — the Cilium eBPF layer on which Tetragon hooks its kprobes; the datapath you already have in the cluster.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/technical-controls-ens-iso-42001-eu-ai-act-cross-mapping/">Technical controls: ENS × ISO 42001 × EU AI Act&lt;/a> — Tetragon events as technical evidence for &lt;code>op.mon&lt;/code>/&lt;code>op.exp&lt;/code>; enforcement as a protection measure.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">Guardrails and safety in LLMs&lt;/a> — mitigation on the content plane; this runbook, on the execution plane.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/greenfield-deployment-phases-on-premise-llm-platform/">Seven phases of deploying an on-premise LLM platform&lt;/a> — where runtime hardening fits in the deployment sequence (F4 identity/policies, F5 platform).&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>ai-jail (Fabio Akita), GPL-3.0: &lt;a href="https://github.com/akitaonrails/ai-jail">https://github.com/akitaonrails/ai-jail&lt;/a>&lt;/li>
&lt;li>bubblewrap: &lt;a href="https://github.com/containers/bubblewrap">https://github.com/containers/bubblewrap&lt;/a>&lt;/li>
&lt;li>Landlock LSM: &lt;a href="https://landlock.io">https://landlock.io&lt;/a>&lt;/li>
&lt;li>Tetragon — TracingPolicy: &lt;a href="https://tetragon.io/docs/concepts/tracing-policy/">https://tetragon.io/docs/concepts/tracing-policy/&lt;/a>&lt;/li>
&lt;li>Tetragon — enforcement (Sigkill/Override): &lt;a href="https://tetragon.io/docs/concepts/enforcement/">https://tetragon.io/docs/concepts/enforcement/&lt;/a>&lt;/li>
&lt;li>Kata Containers — Kubernetes RuntimeClass: &lt;a href="https://katacontainers.io">https://katacontainers.io&lt;/a>&lt;/li>
&lt;li>Kubernetes — Pod Security &amp;amp; seccomp: &lt;a href="https://kubernetes.io/docs/tutorials/security/seccomp/">https://kubernetes.io/docs/tutorials/security/seccomp/&lt;/a>&lt;/li>
&lt;li>Kubernetes — Network Policies: &lt;a href="https://kubernetes.io/docs/concepts/services-networking/network-policies/">https://kubernetes.io/docs/concepts/services-networking/network-policies/&lt;/a>&lt;/li>
&lt;li>Cilium: &lt;a href="https://cilium.io">https://cilium.io&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>The contractor with the master key: isolating AI agents from workstation to cluster</title><link>https://blog.lo0.es/en/posts/isolating-ai-agents-workstation-to-cluster/</link><pubDate>Tue, 09 Jun 2026 16:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/isolating-ai-agents-workstation-to-cluster/</guid><description>&lt;blockquote>
&lt;p>First post of a pair on &lt;strong>AI agent isolation&lt;/strong>. This one fixes the &lt;em>what&lt;/em> and the &lt;em>where&lt;/em>: the complete map of isolation primitives and which domain each belongs to. The &lt;a href="https://blog.lo0.es/en/posts/runbook-caging-ai-agent-bubblewrap-tetragon/">sibling runbook&lt;/a> fixes the &lt;em>how&lt;/em>, with commands: &lt;code>ai-jail&lt;/code> and bubblewrap on the client, Tetragon &lt;code>TracingPolicy&lt;/code> and &lt;code>RuntimeClass&lt;/code> on the cluster. If you are only going to read one, this one gives you the mental model; the other one gives you the files to copy and paste.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>An AI agent that runs code needs access to your filesystem and your tools: compiler, linter, &lt;code>grep&lt;/code>, &lt;code>make&lt;/code>, &lt;code>cargo&lt;/code>, &lt;code>npm&lt;/code>. That is the minimum to be useful. The problem is that alongside that access travels the ability to read &lt;code>~/.aws/credentials&lt;/code>, exfiltrate your SSH keys or fire an &lt;code>rm -rf&lt;/code> outside the project directory. And no malicious model is needed: a compromised dependency in an &lt;code>npm install&lt;/code> is enough, because the well-meaning agent and the poisoned &lt;em>post-install script&lt;/em> &lt;strong>run with the same permissions&lt;/strong>. The answer is not to trust the LLM&amp;rsquo;s good intentions; it is to &lt;strong>isolate in order to bound the blast radius&lt;/strong>. This post walks the five isolation families of 2026, from the process sandbox to the full VM, and sorts them into two columns: what applies on the &lt;strong>client&lt;/strong> (the developer&amp;rsquo;s workstation: bubblewrap, &lt;code>ai-jail&lt;/code>, sandbox-exec, Landlock, the native Claude Code and Codex sandboxes) and what applies on the &lt;strong>cluster&lt;/strong> (where the agent or the inference runs on Kubernetes: namespaces+seccomp, gVisor, Firecracker/Kata microVMs and &lt;strong>eBPF/Tetragon&lt;/strong> as the layer for live observation and enforcement). The thesis: the threat model is the same in both places; the tools are not. Policy extrapolates; the primitive gets rewritten.&lt;/p>
&lt;h2 id="the-analogy-the-contractor-with-the-master-key">The analogy: the contractor with the master key&lt;/h2>
&lt;p>You hire a builder for a renovation. He is competent and acts in good faith. But two things happen that you do not control. The first: he may misread the instruction and knock down the wrong partition wall. The second, worse: his toolbox may have been tampered with before he came through your door — somebody put something inside — and when he opens it in your living room, that something activates.&lt;/p>
&lt;p>Nobody sensible gives him the &lt;strong>master key&lt;/strong> to the whole building. You open the room he works in, you leave him the tools he needs, and you keep the study with the safe and the server room locked. If the renovation goes wrong, by error or by sabotage, the damage stays in that room.&lt;/p>
&lt;p>An AI agent is that contractor. The sandbox is the key policy: &lt;strong>you give it the project room and the tools, not the master key to the system.&lt;/strong> And here is the twist that justifies two posts: the builder works in two different buildings. One is &lt;strong>your flat&lt;/strong>, the developer&amp;rsquo;s workstation, with your credentials, your &lt;code>~/.ssh&lt;/code>, the browser&amp;rsquo;s password vault. The other is &lt;strong>the datacenter&lt;/strong>, the cluster where inference and autonomous agents serve customers, with data from several tenants at once. The key policy is identical in both: least privilege, bound the radius. But the lock on your flat&amp;rsquo;s door is not the same as the one at the datacenter. In the flat you fit a cylinder lock (&lt;code>bubblewrap&lt;/code>). At the datacenter you put a guard watching every door and a separate wing of the building (Tetragon + microVM). Same principle, different hardware. That is &lt;strong>extrapolating the technology&lt;/strong>, not copying it.&lt;/p>
&lt;h2 id="the-threat-model-what-a-runaway-agent-can-do">The threat model: what a runaway agent can do&lt;/h2>
&lt;p>Before choosing a lock it helps to enumerate the burglar. The attack surface of an agent that executes arbitrary bash breaks down into five concrete threats. Not all are defended by the same layer and, an uncomfortable fact that the security documentation itself acknowledges, &lt;strong>no single layer covers them all&lt;/strong>.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Threat&lt;/th>
&lt;th>What the agent does&lt;/th>
&lt;th>Minimum layer that cuts it&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Filesystem out of scope&lt;/strong>&lt;/td>
&lt;td>Reads &lt;code>.env&lt;/code>, &lt;code>~/.ssh/id_rsa&lt;/code>, system secrets; modifies sources outside the project&lt;/td>
&lt;td>Process sandbox (path allowlist)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Arbitrary network egress&lt;/strong>&lt;/td>
&lt;td>Exfiltrates data, receives instructions from a remote C2, calls unauthorised APIs&lt;/td>
&lt;td>Network blocking / NetworkPolicy / microVM&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Kernel syscall surface&lt;/strong>&lt;/td>
&lt;td>A kernel exploit from inside the container escalates to the host (shared kernel)&lt;/td>
&lt;td>gVisor or microVM (dedicated kernel)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Cross-tenant leakage&lt;/strong>&lt;/td>
&lt;td>One customer&amp;rsquo;s workload reads another&amp;rsquo;s data on a multi-tenant platform&lt;/td>
&lt;td>microVM (de facto standard)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Secret exfiltration&lt;/strong>&lt;/td>
&lt;td>Extracts tokens and environment variables via &lt;code>/proc&lt;/code> or the environment&lt;/td>
&lt;td>&lt;code>--clearenv&lt;/code> / tmpfs &lt;code>$HOME&lt;/code> / secrets outside the pod&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>There is a sixth threat that &lt;strong>no sandbox solves&lt;/strong>: &lt;em>prompt injection&lt;/em>. If an attacker manages to slip instructions into the agent&amp;rsquo;s context — a poisoned comment in the code, a malicious file the agent reads, an adversarial response from a tool — the agent will execute those instructions with whatever permissions the sandbox grants it. Isolation &lt;strong>shrinks the impact radius&lt;/strong> of a successful injection; it does not prevent the injection. That is why the sandbox is a layer, not the solution: on top go input validation, tool-call allowlists and output auditing. The sentence to internalise: &lt;em>isolation does not make the agent trustworthy; it bounds what an untrustworthy agent can break.&lt;/em>&lt;/p>
&lt;h2 id="two-domains-one-policy">Two domains, one policy&lt;/h2>
&lt;p>The builder works in two buildings. The distribution of tools reads better in two columns:&lt;/p>
&lt;div class="diagram" style="max-width:820px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 820 430" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Two-column map: client domain (workstation) and cluster domain, with their isolation layers and the shared policy bridge">
&lt;defs>&lt;marker id="am" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="410" y="26" text-anchor="middle" font-family="sans-serif" font-size="14" font-weight="700" fill="currentColor">Same least-privilege policy · different primitive&lt;/text>
&lt;!-- client column -->
&lt;rect x="24" y="46" width="360" height="350" rx="10" fill="#eef4fb" stroke="#1f5fa8" stroke-width="1.6"/>
&lt;text x="204" y="72" text-anchor="middle" font-family="sans-serif" font-size="14" font-weight="700" fill="#0d3a66">CLIENT · dev workstation&lt;/text>
&lt;text x="204" y="90" text-anchor="middle" font-family="sans-serif" font-size="11" fill="#0d3a66">one process, one user, personal data&lt;/text>
&lt;rect x="48" y="106" width="312" height="50" rx="6" fill="#d4ecff" stroke="#1f5fa8" stroke-width="1.2"/>
&lt;text x="204" y="126" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="#0d3a66">Process sandbox&lt;/text>
&lt;text x="204" y="143" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#0d3a66">bubblewrap (Linux) · sandbox-exec (macOS)&lt;/text>
&lt;rect x="48" y="166" width="312" height="42" rx="6" fill="#dff0ff" stroke="#1f5fa8" stroke-width="1.1"/>
&lt;text x="204" y="183" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="#0d3a66">Landlock (LSM, 2nd VFS barrier)&lt;/text>
&lt;text x="204" y="199" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#0d3a66">defence in depth, kernel ≥5.13&lt;/text>
&lt;rect x="48" y="218" width="312" height="42" rx="6" fill="#eaf4ff" stroke="#1f5fa8" stroke-width="1.1"/>
&lt;text x="204" y="235" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="#0d3a66">Dev container (optional)&lt;/text>
&lt;text x="204" y="251" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#0d3a66">reproducibility + reset, shared kernel&lt;/text>
&lt;text x="204" y="284" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="600" fill="#0d3a66">What wraps it:&lt;/text>
&lt;text x="204" y="304" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="#0d3a66">ai-jail · Claude Code /sandbox&lt;/text>
&lt;text x="204" y="320" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="#0d3a66">Codex --sandbox · Cursor /worktree&lt;/text>
&lt;text x="204" y="352" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="#0d3a66">Safety net: git remote with no push&lt;/text>
&lt;text x="204" y="376" text-anchor="middle" font-family="sans-serif" font-size="10.5" font-style="italic" fill="#555">startup ~0 ms · no daemon&lt;/text>
&lt;!-- cluster column -->
&lt;rect x="436" y="46" width="360" height="350" rx="10" fill="#f3eefb" stroke="#5a2db0" stroke-width="1.6"/>
&lt;text x="616" y="72" text-anchor="middle" font-family="sans-serif" font-size="14" font-weight="700" fill="#42208a">CLUSTER · inference / agents in prod&lt;/text>
&lt;text x="616" y="90" text-anchor="middle" font-family="sans-serif" font-size="11" fill="#42208a">multi-pod, multi-tenant, RKE2&lt;/text>
&lt;rect x="460" y="106" width="312" height="50" rx="6" fill="#e6d9f2" stroke="#5a2db0" stroke-width="1.2"/>
&lt;text x="616" y="126" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="#42208a">Pod baseline&lt;/text>
&lt;text x="616" y="143" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#42208a">namespaces + seccomp + cgroups + NetworkPolicy&lt;/text>
&lt;rect x="460" y="166" width="312" height="42" rx="6" fill="#ede1f7" stroke="#5a2db0" stroke-width="1.1"/>
&lt;text x="616" y="183" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="#42208a">gVisor (runsc) — user-space kernel&lt;/text>
&lt;text x="616" y="199" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#42208a">cuts the syscall surface to the host&lt;/text>
&lt;rect x="460" y="218" width="312" height="42" rx="6" fill="#f0e6fb" stroke="#5a2db0" stroke-width="1.1"/>
&lt;text x="616" y="235" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="#42208a">Firecracker / Kata microVM — own kernel&lt;/text>
&lt;text x="616" y="251" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#42208a">isolation by construction for untrusted code&lt;/text>
&lt;rect x="460" y="270" width="312" height="62" rx="6" fill="#fde9d6" stroke="#a85a00" stroke-width="1.6"/>
&lt;text x="616" y="290" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="700" fill="#8a4a00">eBPF / Tetragon (what we already have)&lt;/text>
&lt;text x="616" y="307" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#8a4a00">watches every exec · connect · open in the kernel&lt;/text>
&lt;text x="616" y="322" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#8a4a00">and kills (Sigkill) whatever strays from the script&lt;/text>
&lt;text x="616" y="356" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="#42208a">Safety net: GitOps + PR review&lt;/text>
&lt;text x="616" y="376" text-anchor="middle" font-family="sans-serif" font-size="10.5" font-style="italic" fill="#555">startup &amp;lt;1 s (microVM) · runtime layer always on&lt;/text>
&lt;!-- bridge -->
&lt;path d="M384,221 L436,221" stroke="#666" stroke-width="1.6" fill="none" marker-end="url(#am)"/>
&lt;path d="M436,241 L384,241" stroke="#666" stroke-width="1.6" fill="none" marker-end="url(#am)"/>
&lt;text x="410" y="415" text-anchor="middle" font-family="sans-serif" font-size="11" font-style="italic" fill="#555">extrapolate: each client control has its cluster analogue (equivalence table in the runbook)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="the-client-isolating-the-agent-on-the-workstation">The client: isolating the agent on the workstation&lt;/h2>
&lt;p>Here the agent is a coding assistant — Claude Code, Codex, OpenCode, Cursor — that a developer launches on their machine. One process, one user, and right next to it the juiciest assets that exist: &lt;code>~/.aws/credentials&lt;/code>, &lt;code>~/.ssh&lt;/code>, &lt;code>~/.gnupg&lt;/code>, the browser&amp;rsquo;s password store. The matching tier is the lightest one: the &lt;strong>process sandbox&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>bubblewrap (Linux) and sandbox-exec (macOS).&lt;/strong> &lt;code>bubblewrap&lt;/code> (&lt;code>bwrap&lt;/code>) is the same sandbox Flatpak uses to isolate each desktop app: a ~50 KB binary, ~4,000 lines of C, maintained by the GNOME team and, the key property, it runs &lt;strong>without root&lt;/strong> via &lt;code>CLONE_NEWUSER&lt;/code>, creating namespaces with no elevated privileges. It mounts &lt;code>$HOME&lt;/code> as an ephemeral tmpfs and only exposes the project directory, writable; the rest of the system becomes invisible. On macOS the equivalent is &lt;code>sandbox-exec&lt;/code> with SBPL profiles: a legacy Apple API, officially deprecated and with no public replacement, but working today. Parity between the two is not exact — on macOS the GPU (Metal) and the display (Cocoa) are system services and &lt;code>sandbox-exec&lt;/code> does not restrict them — but both protect what matters: access to the sensitive areas of the filesystem.&lt;/p>
&lt;p>&lt;strong>Landlock as a second barrier.&lt;/strong> &lt;code>bubblewrap&lt;/code> isolates via &lt;em>namespaces and mounts&lt;/em>; Landlock, a Linux Security Module available since kernel 5.13, restricts access at the &lt;strong>VFS&lt;/strong> level, independently of namespaces. It does not replace &lt;code>bwrap&lt;/code>: it complements it. It closes vectors that mount-based isolation does not cover on its own (escape routes via &lt;code>/proc&lt;/code>, symlink tricks inside allowed mounts) and acts as a safety net should the namespace machinery have a bug. It is defence in depth inside the client itself, and it degrades cleanly to a no-op on kernels that do not support it.&lt;/p>
&lt;p>&lt;strong>Dev containers, when reproducibility is needed.&lt;/strong> A dev container (&lt;code>devcontainer.json&lt;/code>, what Codespaces and Cursor use) is a Docker container with a configuration layer on top. It gives reasonable filesystem isolation and easy &lt;em>reset&lt;/em> (destroy and recreate), but it &lt;strong>shares the host kernel&lt;/strong>, the same limitation as any Docker, and it tends to be long-lived, accumulating state. For an agent running your own team&amp;rsquo;s code on your own machine it is a good repeatability story; it is not, on its own, the isolation layer for untrusted code.&lt;/p>
&lt;p>&lt;strong>What wraps all of this.&lt;/strong> A hand-written bash script works, but it does not scale to a team. The tools that package it:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>ai-jail&lt;/code>&lt;/strong> (Rust, GPL-3.0): wraps &lt;code>bwrap&lt;/code>/&lt;code>sandbox-exec&lt;/code> with per-project config in a &lt;code>.ai-jail&lt;/code> file (TOML, committable to the repo, so the whole team inherits the same policy), auto-detection of GPU/Docker/display, a &lt;code>--lockdown&lt;/code> mode (project read-only, network cut with &lt;code>--unshare-net&lt;/code>, &lt;code>--clearenv&lt;/code>), &lt;code>--dry-run&lt;/code> for auditing, and &lt;code>--bootstrap&lt;/code> to generate each agent&amp;rsquo;s permission allowlists. It is tool-agnostic: the same binary serves Claude, Codex, OpenCode or Crush. It also applies Landlock automatically on 5.13+ kernels as defence in depth.&lt;/li>
&lt;li>&lt;strong>The Claude Code &lt;code>/sandbox&lt;/code>&lt;/strong>: since October 2025 Claude Code ships its own sandbox that uses, exactly, &lt;code>bubblewrap&lt;/code> on Linux and &lt;code>sandbox-exec&lt;/code> on macOS. Its &lt;em>Sandboxed Bash&lt;/em> isolates shell commands, but &lt;strong>not&lt;/strong> the file tools, the MCP servers or the hooks, which run with the process&amp;rsquo;s full permissions unless you enable the beta &lt;code>sandbox-runtime&lt;/code> package, which wraps the whole process. There is one nuance worth knowing: if a command fails because of a restriction, the agent can retry with &lt;code>dangerouslyDisableSandbox&lt;/code> — it is &lt;em>opt-out&lt;/em>, not &lt;em>opt-in&lt;/em>.&lt;/li>
&lt;li>&lt;strong>Codex CLI&lt;/strong>: three modes via &lt;code>--sandbox&lt;/code> (&lt;code>read-only&lt;/code>, &lt;code>workspace-write&lt;/code>, &lt;code>danger-full-access&lt;/code>); the recommended default is &lt;code>workspace-write&lt;/code>. The philosophy is deliberate: Codex does not provide the isolation, it delegates it to the environment that wraps it. &lt;code>danger-full-access&lt;/code> only makes sense &lt;strong>inside&lt;/strong> a microVM.&lt;/li>
&lt;li>&lt;strong>Cursor&lt;/strong>: its cloud agents run in isolated VMs; &lt;code>/worktree&lt;/code> creates a single-use isolated worktree per task, and &lt;code>/best-of-n&lt;/code> launches several attempts in parallel in separate worktrees.&lt;/li>
&lt;/ul>
&lt;h2 id="the-cluster-isolating-the-agent-in-production">The cluster: isolating the agent in production&lt;/h2>
&lt;p>The second building is the datacenter. Here the &amp;ldquo;agent&amp;rdquo; can be an autonomous agent running with no human in front of it, or the inference service itself executing generated code, or a multi-tenant workload where one customer&amp;rsquo;s pod must not touch another&amp;rsquo;s data. The process is no longer one: they are pods in a Kubernetes cluster (RKE2/RKE3 on a typical sovereign platform). The primitives change in nature.&lt;/p>
&lt;p>&lt;strong>The pod baseline.&lt;/strong> First of all, the standard kit: Linux namespaces, &lt;code>seccomp&lt;/code> (&lt;code>RuntimeDefault&lt;/code>) to trim the syscall surface, cgroups for resource limits, an unprivileged &lt;code>securityContext&lt;/code> (&lt;code>runAsNonRoot&lt;/code>, &lt;code>readOnlyRootFilesystem&lt;/code>, dropping all capabilities) and &lt;strong>NetworkPolicy&lt;/strong> to cut egress. It is the cluster equivalent of the process sandbox allowlist. Necessary, but it shares the kernel with the host: insufficient for genuinely untrusted code.&lt;/p>
&lt;p>&lt;strong>gVisor (&lt;code>runsc&lt;/code>).&lt;/strong> Google&amp;rsquo;s user-space kernel: it intercepts the workload&amp;rsquo;s syscalls before they reach the host kernel and serves them inside a Linux kernel reimplemented in Go (the &lt;em>Sentry&lt;/em>). The surface exposed to host kernel vulnerabilities drops sharply, while keeping fast startup and a low footprint. It is the middle ground when the risk of a kernel escape is real but the overhead of a microVM is not acceptable.&lt;/p>
&lt;p>&lt;strong>Firecracker / Kata microVMs.&lt;/strong> The de facto standard for untrusted code in 2026. Firecracker (AWS&amp;rsquo;s VMM written in Rust, on top of KVM) gives each sandbox a &lt;strong>dedicated Linux kernel&lt;/strong>: a kernel exploit inside the microVM does not reach the host &lt;em>by construction&lt;/em>. It is what sits underneath Vercel Sandbox (GA January 2026) and E2B. On Kubernetes, &lt;strong>Kata Containers&lt;/strong> brings that model to a &lt;code>RuntimeClass&lt;/code>: you mark the untrusted agent&amp;rsquo;s pod with &lt;code>runtimeClassName: kata&lt;/code> and it runs in its own microVM instead of sharing the node&amp;rsquo;s kernel. For multi-tenant with generated code, this is the baseline, not the luxury.&lt;/p>
&lt;p>&lt;strong>eBPF / Tetragon: the layer we already have.&lt;/strong> Here is the piece that separates a platform with runtime observability from one that only trusts its configuration. The previous layers are &lt;em>static&lt;/em>: they define what the pod can do before it starts. &lt;strong>Tetragon&lt;/strong>, Cilium&amp;rsquo;s runtime security component, based on eBPF, is &lt;em>dynamic&lt;/em>: it observes, in the kernel and at minimal cost, &lt;strong>every&lt;/strong> process execution, &lt;strong>every&lt;/strong> network connection and &lt;strong>every&lt;/strong> file open in every pod, and it can act inline. It does not replace the sandbox; it watches it from inside the kernel. Where &lt;code>bubblewrap&lt;/code> on the client blocks &lt;code>curl&lt;/code> with a command blocklist, Tetragon on the cluster hooks &lt;code>tcp_connect&lt;/code> in the kernel and, if the destination is not allowed, &lt;strong>kills the process with &lt;code>Sigkill&lt;/code>&lt;/strong> before the packet leaves. Where the client hides &lt;code>~/.ssh&lt;/code> behind a tmpfs, Tetragon hooks &lt;code>security_file_open&lt;/code> and reports, or kills, any attempt to read a sensitive mounted path. It is the guard patrolling the corridors while the microVMs are the walls. And it is exactly the kind of control that materialises the ENS monitoring and traceability measures (&lt;code>op.mon&lt;/code>, &lt;code>op.exp&lt;/code>) without instrumenting the application: visibility lives in the kernel, not in the agent&amp;rsquo;s code.&lt;/p>
&lt;h2 id="the-landscape-table">The landscape table&lt;/h2>
&lt;p>The five families, their relative isolation strength, their startup cost and the domain where they live:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tier&lt;/th>
&lt;th>Primitive&lt;/th>
&lt;th style="text-align:center">Isolation&lt;/th>
&lt;th style="text-align:center">Startup&lt;/th>
&lt;th>Natural domain&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Process sandbox&lt;/td>
&lt;td>Seatbelt · bubblewrap&lt;/td>
&lt;td style="text-align:center">Baseline&lt;/td>
&lt;td style="text-align:center">~0 ms&lt;/td>
&lt;td>&lt;strong>Client&lt;/strong> (Claude Code default)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Dev container&lt;/td>
&lt;td>Docker + seccomp&lt;/td>
&lt;td style="text-align:center">Moderate&lt;/td>
&lt;td style="text-align:center">seconds&lt;/td>
&lt;td>Client / cluster (repeatability)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>User-space kernel&lt;/td>
&lt;td>gVisor (&lt;code>runsc&lt;/code>)&lt;/td>
&lt;td style="text-align:center">Strong&lt;/td>
&lt;td style="text-align:center">ms&lt;/td>
&lt;td>&lt;strong>Cluster&lt;/strong> (medium multi-tenant)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>microVM&lt;/td>
&lt;td>Firecracker · Kata&lt;/td>
&lt;td style="text-align:center">Strongest (practical)&lt;/td>
&lt;td style="text-align:center">&amp;lt;1 s&lt;/td>
&lt;td>&lt;strong>Cluster&lt;/strong> (untrusted code)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Full VM&lt;/td>
&lt;td>KVM · EC2&lt;/td>
&lt;td style="text-align:center">Maximum&lt;/td>
&lt;td style="text-align:center">30 s+&lt;/td>
&lt;td>Cluster (external boundary, compliance)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;em>Runtime enforcement&lt;/em>&lt;/td>
&lt;td>&lt;strong>eBPF · Tetragon&lt;/strong>&lt;/td>
&lt;td style="text-align:center">&lt;em>Cross-cutting&lt;/em>&lt;/td>
&lt;td style="text-align:center">&lt;em>always on&lt;/em>&lt;/td>
&lt;td>&lt;strong>Cluster&lt;/strong> (watches+kills on top of any tier)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Tetragon gets its own row on purpose: it is not a rung on the ladder, it is a &lt;strong>cross-cutting layer&lt;/strong> that operates &lt;em>on top of&lt;/em> any of the others. It stacks with all of them.&lt;/p>
&lt;p>A numerical note on why the &amp;ldquo;startup&amp;rdquo; column decides as much as the &amp;ldquo;isolation&amp;rdquo; column. A full VM wins on raw isolation but takes tens of seconds to provision; for an agent that needs a fresh environment &lt;em>per request or per session&lt;/em>, that cost is prohibitive. A Firecracker microVM starts in &lt;strong>under 1 second&lt;/strong> and a process sandbox in &lt;strong>~0 ms&lt;/strong>. That is why the dominant pattern in 2026 is not &amp;ldquo;the most isolated VM&amp;rdquo;, but &lt;strong>a full VM as the external boundary plus a microVM as the per-request execution unit inside&lt;/strong>, the architecture of Vercel, AWS Lambda and E2B. On the client the arithmetic is the opposite: the developer launches the agent dozens of times a day interactively, and a startup measured in seconds would break the flow; hence the process sandbox, with its microsecond overhead, is the correct default.&lt;/p>
&lt;h2 id="extrapolate-do-not-copy">Extrapolate, do not copy&lt;/h2>
&lt;p>The thesis of this pair of posts fits in one sentence: &lt;strong>the threat model is invariant across domains; the primitive that implements it is not.&lt;/strong> The client and the cluster defend exactly the same five vectors — filesystem, network, kernel, multi-tenant, secrets — but with toolboxes that do not overlap. Every control has its twin on the other side:&lt;/p>
&lt;ul>
&lt;li>&lt;code>$HOME&lt;/code> as an ephemeral tmpfs (client) ↔ &lt;code>readOnlyRootFilesystem&lt;/code> + &lt;code>emptyDir&lt;/code> (cluster).&lt;/li>
&lt;li>A &lt;code>curl&lt;/code>/&lt;code>wget&lt;/code> blocklist in &lt;code>bwrap&lt;/code> (client) ↔ a &lt;code>TracingPolicy&lt;/code> on &lt;code>tcp_connect&lt;/code> in Tetragon plus NetworkPolicy (cluster).&lt;/li>
&lt;li>&lt;code>--unshare-net&lt;/code> in lockdown (client) ↔ default-deny NetworkPolicy (cluster).&lt;/li>
&lt;li>No &lt;em>escape hatch&lt;/em>, the process lives inside &lt;code>bwrap&lt;/code> (client) ↔ no &lt;code>privileged&lt;/code>, no &lt;code>hostPath&lt;/code>, &lt;code>RuntimeClass&lt;/code> kata (cluster).&lt;/li>
&lt;li>&lt;code>~/.ssh&lt;/code> and &lt;code>~/.aws&lt;/code> never mounted (client) ↔ secrets outside the pod plus Tetragon watching &lt;code>security_file_open&lt;/code> (cluster).&lt;/li>
&lt;/ul>
&lt;p>The sibling runbook turns each of these equivalences into concrete files. What matters here is the method: when someone shows you an agent sandbox, be it the Claude Code &lt;code>/sandbox&lt;/code> on a laptop or a microVM on a PaaS, the useful question is not &amp;ldquo;which tool does it use?&amp;rdquo;, but &amp;ldquo;which of the five vectors does it close, and which does it leave open?&amp;rdquo;. The tool gets replaced; the threat map stays.&lt;/p>
&lt;h2 id="what-no-sandbox-solves">What no sandbox solves&lt;/h2>
&lt;p>Three limits that Anthropic&amp;rsquo;s own documentation states, and which are worth keeping in view so as not to sell smoke:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Egress remains a risk&lt;/strong> in any sandbox that allows outbound connections. If the agent can open a connection, it can exfiltrate. That is why the client lockdown &lt;em>cuts&lt;/em> the network and the cluster uses default-deny NetworkPolicy plus Tetragon: you do not trust &amp;ldquo;filtering well&amp;rdquo;, you trust &amp;ldquo;not letting anything out&amp;rdquo;.&lt;/li>
&lt;li>&lt;strong>Code modification remains possible&lt;/strong> in any sandbox with the project directory mounted writable. The remedy is not a sandbox-technical one, it is &lt;strong>git&lt;/strong>: with the remote untouched and no &lt;code>push&lt;/code> permission, the worst case is corrupting the local working copy — &lt;code>git checkout .&lt;/code> and start again. The damage never reaches the remote.&lt;/li>
&lt;li>&lt;strong>No sandbox prevents a compromised prompt from reaching the API.&lt;/strong> Isolation bounds the impact of an injection; it does not prevent it. The complementary defences — input validation, tool-call allowlists, output auditing — are mandatory &lt;em>alongside&lt;/em> isolation, not instead of it.&lt;/li>
&lt;/ol>
&lt;p>The operational conclusion: &lt;strong>isolation shrinks the blast radius; defence in depth is what closes the circle.&lt;/strong> A process sandbox for trusted code on a known machine is appropriate and practically free. For an agent acting on user prompts, executing generated code or running multi-tenant, the acceptable minimum in 2026 is a microVM, with Tetragon watching from above. Pick the tier that matches your real threat, check which vector it leaves open, and stack complementary controls on top.&lt;/p>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/hardening-secrets-sovereign-llm-stack-defence-in-depth/">Hardening and secrets in the sovereign LLM stack: defence in depth&lt;/a> — the hardening of the stack that hosts those agents: network, secrets, runtime.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/runbook-caging-ai-agent-bubblewrap-tetragon/">Runbook: caging the AI agent — bubblewrap on the client, Tetragon on the cluster&lt;/a> — the operational companion to this post: the &lt;code>.ai-jail&lt;/code> files, the permissions &lt;code>--bootstrap&lt;/code> and the Tetragon &lt;code>TracingPolicy&lt;/code> files to copy and paste. With commands.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/network-numa-cilium-ebpf-dranet-fourth-leg-pinning/">The kitchen door the maître never checked: network NUMA, Cilium eBPF and DRANET&lt;/a> — the Cilium eBPF datapath Tetragon leans on in the cluster; the same kernel layer, a different use.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">Guardrails and safety in LLMs&lt;/a> — mitigation on the &lt;em>content&lt;/em> plane (what the model says and what is said to it); this post is mitigation on the &lt;em>execution&lt;/em> plane (what the agent&amp;rsquo;s process can do).&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/technical-controls-ens-iso-42001-eu-ai-act-cross-mapping/">Technical controls: ENS × ISO 42001 × EU AI Act&lt;/a> — the compliance framework that runtime isolation materialises: Tetragon as technical evidence for &lt;code>op.mon&lt;/code>/&lt;code>op.exp&lt;/code>.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/oss-catalogue-llmops-six-stages-tool-by-tool/">Catalogue of OSS tools for LLMOps&lt;/a> — where the runtime security layer fits in the complete open stack.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>bubblewrap: &lt;a href="https://github.com/containers/bubblewrap">https://github.com/containers/bubblewrap&lt;/a>&lt;/li>
&lt;li>Landlock LSM: &lt;a href="https://landlock.io">https://landlock.io&lt;/a> · &lt;a href="https://docs.rs/landlock">https://docs.rs/landlock&lt;/a>&lt;/li>
&lt;li>ai-jail (Fabio Akita): &lt;a href="https://github.com/akitaonrails/ai-jail">https://github.com/akitaonrails/ai-jail&lt;/a>&lt;/li>
&lt;li>gVisor: &lt;a href="https://gvisor.dev">https://gvisor.dev&lt;/a>&lt;/li>
&lt;li>Firecracker: &lt;a href="https://firecracker-microvm.github.io">https://firecracker-microvm.github.io&lt;/a>&lt;/li>
&lt;li>Kata Containers: &lt;a href="https://katacontainers.io">https://katacontainers.io&lt;/a>&lt;/li>
&lt;li>Tetragon (Cilium): &lt;a href="https://tetragon.io">https://tetragon.io&lt;/a>&lt;/li>
&lt;li>Vercel Sandbox — concepts: &lt;a href="https://vercel.com/docs/vercel-sandbox/concepts">https://vercel.com/docs/vercel-sandbox/concepts&lt;/a>&lt;/li>
&lt;li>E2B: &lt;a href="https://github.com/e2b-dev/E2B">https://github.com/e2b-dev/E2B&lt;/a>&lt;/li>
&lt;li>Claude Code sandboxing: &lt;a href="https://docs.claude.com/en/docs/claude-code">https://docs.claude.com/en/docs/claude-code&lt;/a>&lt;/li>
&lt;li>Codex CLI: &lt;a href="https://github.com/openai/codex">https://github.com/openai/codex&lt;/a>&lt;/li>
&lt;/ul></description></item></channel></rss>