<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Networkpolicy on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/networkpolicy/</link><description>Recent content in Networkpolicy on lo0 — Tech Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Thu, 11 Jun 2026 11:00:00 +0000</lastBuildDate><atom:link href="https://blog.lo0.es/en/tags/networkpolicy/index.xml" rel="self" type="application/rss+xml"/><item><title>Hardening and secrets in the sovereign LLM stack: defence in depth</title><link>https://blog.lo0.es/en/posts/hardening-secrets-sovereign-llm-stack-defence-in-depth/</link><pubDate>Thu, 11 Jun 2026 11:00:00 +0000</pubDate><guid>https://blog.lo0.es/en/posts/hardening-secrets-sovereign-llm-stack-defence-in-depth/</guid><description>&lt;blockquote>
&lt;p>Part of the operational series on squeezing a generic on-premise 4×H100 SXM 80GB LLM cluster. The sibling pieces: the &lt;a href="https://blog.lo0.es/en/posts/end-to-end-document-ingestion-pdf-to-indexed-chunk/">document ingestion from PDF to indexed chunk&lt;/a> that fills the vector database, the &lt;a href="https://blog.lo0.es/en/posts/serving-embeddings-rerankers-tei-production/">embeddings and rerankers service with TEI in production&lt;/a> that feeds it, and, the most directly coupled to this post, the &lt;a href="https://blog.lo0.es/en/posts/gitops-inference-stack-flux/">GitOps of the inference stack with Flux&lt;/a>, because GitOps and secrets share a chicken-and-egg problem we solve here. Assembling all of this into a conversational assistant (LibreChat + LiteLLM + RAG) is covered by another post in the series, still in draft.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>A sovereign LLM inference stack is, at minimum, six services: a gateway (LiteLLM or equivalent), an inference engine (vLLM), an embeddings/rerankers service (TEI), a vector database, a state database (conversations, users) and a front end (LibreChat). Six services are six attack surfaces, and the assistant &amp;ldquo;works&amp;rdquo; as soon as the gateway returns tokens, long before it is hardened. This post walks through defence in depth layer by layer:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Secrets&lt;/strong>: never in the clear in git. &lt;code>sealed-secrets&lt;/code> (asymmetric encryption, controller in the cluster) vs SOPS/age (file encryption, simple, pure GitOps) vs External Secrets Operator + Vault (dynamic secrets, native rotation). GitOps&amp;rsquo;s chicken-and-egg problem.&lt;/li>
&lt;li>&lt;strong>Network&lt;/strong>: default-deny &lt;code>NetworkPolicy&lt;/code> + explicit allow; L3/L4 with the standard NetworkPolicy, L7 and DNS-based egress with Cilium (eBPF); egress control so the data does not leave the perimeter; internal mTLS.&lt;/li>
&lt;li>&lt;strong>Pod security&lt;/strong>: &lt;code>runAsNonRoot&lt;/code>, &lt;code>readOnlyRootFilesystem&lt;/code>, dropped capabilities, seccomp &lt;code>RuntimeDefault&lt;/code>, restricted Pod Security Standards, nothing privileged.&lt;/li>
&lt;li>&lt;strong>Supply chain&lt;/strong>: pin by digest, scan with Trivy, sign with cosign, admission control that rejects anything unsigned.&lt;/li>
&lt;li>&lt;strong>AuthN/Z&lt;/strong>: LiteLLM virtual keys, OIDC/LDAP in the front end, Kubernetes RBAC with least privilege.&lt;/li>
&lt;li>&lt;strong>Runtime&lt;/strong>: detection and, optionally, enforcement with Tetragon.&lt;/li>
&lt;li>&lt;strong>Data at rest&lt;/strong>: storage encryption, credentials for the vector database and the state database.&lt;/li>
&lt;/ol>
&lt;p>The thesis: hardening reduces the blast radius, it does not eliminate it. That is why you prioritise by impact, and the first job on a 4×H100 is the gateway↔vector store pair and egress-deny.&lt;/p>
&lt;h2 id="the-analogy-the-sovereign-office">The analogy: the sovereign office&lt;/h2>
&lt;p>Picture an office holding sensitive documentation: the corpus that feeds the RAG, the users&amp;rsquo; conversations, the engine&amp;rsquo;s credentials. It is not enough for the street door to lock. Real security is &lt;strong>defence in depth&lt;/strong>: several layers, each assuming the previous one can fail.&lt;/p>
&lt;div class="diagram" style="max-width:760px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 760 520" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Defence in depth as concentric layers: perimeter, access control, fire partitions, safe, cameras, data">
&lt;defs>&lt;marker id="hd1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#888"/>&lt;/marker>&lt;/defs>
&lt;rect x="20" y="20" width="720" height="480" rx="14" fill="none" stroke="#3b82f6" stroke-width="2"/>
&lt;text x="380" y="44" text-anchor="middle" font-family="sans-serif" font-size="13" font-weight="700" fill="#3b82f6">Perimeter — egress control (the data does not leave)&lt;/text>
&lt;rect x="58" y="58" width="644" height="404" rx="12" fill="none" stroke="#22c55e" stroke-width="2"/>
&lt;text x="380" y="80" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="#22c55e">Access control at every door — default-deny NetworkPolicy&lt;/text>
&lt;rect x="100" y="94" width="560" height="332" rx="10" fill="none" stroke="#f59e0b" stroke-width="2"/>
&lt;text x="380" y="114" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="#f59e0b">Fire partitions — Pod Security restricted + namespaces&lt;/text>
&lt;rect x="142" y="128" width="476" height="262" rx="9" fill="none" stroke="#888" stroke-width="1.6"/>
&lt;text x="380" y="148" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="currentColor">Doorman who checks credentials — admission control (cosign)&lt;/text>
&lt;rect x="184" y="162" width="392" height="194" rx="8" fill="none" stroke="#888" stroke-width="1.4"/>
&lt;text x="380" y="182" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="currentColor">Cameras — Tetragon/Falco (logging and detection)&lt;/text>
&lt;rect x="248" y="208" width="264" height="120" rx="10" fill="#ef4444" fill-opacity="0.12" stroke="#ef4444" stroke-width="2"/>
&lt;text x="380" y="238" text-anchor="middle" font-family="sans-serif" font-size="13" font-weight="700" fill="#ef4444">Safe&lt;/text>
&lt;text x="380" y="262" text-anchor="middle" font-family="sans-serif" font-size="11" fill="currentColor">encrypted secrets + rotated key&lt;/text>
&lt;text x="380" y="284" text-anchor="middle" font-family="sans-serif" font-size="11" fill="currentColor">RAG corpus · conversations&lt;/text>
&lt;text x="380" y="306" text-anchor="middle" font-family="sans-serif" font-size="11" fill="currentColor">engine credentials&lt;/text>
&lt;text x="380" y="490" text-anchor="middle" font-family="sans-serif" font-size="10.5" font-style="italic" fill="#888">Each layer assumes the outer one can fall. Crossing them all is expensive; that cost is the security.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>The mapping is direct:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>The building&amp;rsquo;s perimeter&lt;/strong> is &lt;strong>egress control&lt;/strong>: making sure a leaked secret or a compromised process cannot exfiltrate the corpus to an external server. It is the most underrated layer and the first one I put in place in a sovereign deployment.&lt;/li>
&lt;li>&lt;strong>Access control at every interior door&lt;/strong> is the default-deny &lt;code>NetworkPolicy&lt;/code>: the front end does not talk to the vector database directly because it has no reason to; only the strictly necessary pairs are open.&lt;/li>
&lt;li>&lt;strong>The fire partitions&lt;/strong> are pod isolation (&lt;code>restricted&lt;/code>) and namespaces: if one service catches fire, the fire does not jump to the one next door.&lt;/li>
&lt;li>&lt;strong>The doorman who checks credentials&lt;/strong> is the admission control that verifies each image&amp;rsquo;s &lt;strong>cosign signature&lt;/strong> before letting it into the cluster.&lt;/li>
&lt;li>&lt;strong>The cameras&lt;/strong> are Tetragon/Falco: they record what each process did and detect (or kill) anything anomalous.&lt;/li>
&lt;li>&lt;strong>The safe with a rotated key&lt;/strong> is the secrets: encrypted at rest, out of git, and rotated so a stolen key expires.&lt;/li>
&lt;/ul>
&lt;p>No layer is sufficient on its own. The building is secure because crossing them all is expensive.&lt;/p>
&lt;h2 id="layer-1--secrets-gitopss-chicken-and-egg-problem">Layer 1 — Secrets: GitOps&amp;rsquo;s chicken-and-egg problem&lt;/h2>
&lt;p>The stack is deployed by GitOps: the sibling piece on &lt;a href="https://blog.lo0.es/en/posts/gitops-inference-stack-flux/">Flux&lt;/a> reconciles the state declared in git against the cluster. That is excellent for manifests, but there is a foundational problem: the inference engine needs the Hugging Face token to download the model, the state database needs its password, the gateway needs its master key. &lt;strong>Those secrets cannot go into git in the clear.&lt;/strong> Anyone with read access to the repo, and in an organisation with generous &lt;code>read&lt;/code> that is a lot of people, plus every repo backup, plus every fork, would read them.&lt;/p>
&lt;p>The chicken and the egg: GitOps wants all state to be in git, but secrets cannot be in git in the clear. The solution comes in three families:&lt;/p>
&lt;h3 id="family-a--sealed-secrets-asymmetric-encryption-controller-in-the-cluster">Family A — sealed-secrets (asymmetric encryption, controller in the cluster)&lt;/h3>
&lt;p>&lt;a href="https://github.com/bitnami-labs/sealed-secrets">Bitnami Sealed Secrets&lt;/a> is a controller running in the cluster plus a client CLI, &lt;code>kubeseal&lt;/code>. It uses asymmetric cryptography: there is a key pair. The public one is held by developers and is used to encrypt; the private one lives only in the cluster&amp;rsquo;s controller and is used to decrypt. The flow:&lt;/p>
&lt;ol>
&lt;li>The developer takes a normal &lt;code>Secret&lt;/code> and encrypts it with &lt;code>kubeseal&lt;/code>, which obtains the controller&amp;rsquo;s public key. Result: a &lt;code>SealedSecret&lt;/code> resource.&lt;/li>
&lt;li>That &lt;code>SealedSecret&lt;/code> &lt;strong>is&lt;/strong> committed to git in the clear — it is encrypted, it is not readable.&lt;/li>
&lt;li>The controller in the cluster detects it, decrypts it with its private key and creates the real Kubernetes &lt;code>Secret&lt;/code> in the target namespace.&lt;/li>
&lt;/ol>
&lt;p>The key property: because the private key &lt;strong>never leaves the cluster&lt;/strong>, neither the developer nor anyone with access to git can decrypt. And the encryption includes the namespace name: a &lt;code>SealedSecret&lt;/code> sealed for &lt;code>inferencia&lt;/code> cannot be moved to &lt;code>front&lt;/code> and decrypted there — it behaves as if each namespace had its own key. The controller also manages rotation of the sealing keys, labelling them as &lt;code>active&lt;/code> or &lt;code>compromised&lt;/code>.&lt;/p>
&lt;p>Advantage: it fits pure GitOps perfectly, the encrypted secret lives with the rest of the state. Limitation: the secret, once decrypted, ends up as a normal Kubernetes &lt;code>Secret&lt;/code> — it sits in &lt;code>etcd&lt;/code>, and &lt;code>etcd&lt;/code> has to be encrypted separately (we will see that in layer 7).&lt;/p>
&lt;h3 id="family-b--sops--age-file-encryption-simple">Family B — SOPS + age (file encryption, simple)&lt;/h3>
&lt;p>&lt;a href="https://github.com/getsops/sops">SOPS&lt;/a> encrypts the whole YAML/JSON file (or only its values) and leaves it in git encrypted, decrypting it only at deployment time. It supports cloud KMS (AWS/GCP/Azure), PGP and, the relevant one for on-premise sovereignty, age, a simple serverless offline encryption scheme. The GitOps operator (Flux brings native SOPS integration) decrypts on reconciliation using the age key stored in the cluster.&lt;/p>
&lt;p>Advantage: simple, scriptable, with no server to maintain; the operational weight falls on safeguarding the age key. It is the usual recommendation for small teams or for getting started. Limitation: rotation is manual (re-encrypt everything with the new key) and there are no dynamic secrets.&lt;/p>
&lt;h3 id="family-c--external-secrets-operator--vault-dynamic-secrets-native-rotation">Family C — External Secrets Operator + Vault (dynamic secrets, native rotation)&lt;/h3>
&lt;p>The &lt;a href="https://external-secrets.io/">External Secrets Operator (ESO)&lt;/a> &lt;strong>keeps no secrets in git at all&lt;/strong>. All that goes into git is an &lt;code>ExternalSecret&lt;/code>: a reference saying &amp;ldquo;the &lt;code>password&lt;/code> field comes from path &lt;code>secret/data/vectordb&lt;/code> in such-and-such a &lt;code>SecretStore&lt;/code>&amp;rdquo;. The operator reads from an external store, typically &lt;a href="https://developer.hashicorp.com/vault">HashiCorp Vault&lt;/a>, and synchronises the Kubernetes &lt;code>Secret&lt;/code>. Vault keeps the secrets in its own encrypted backend and serves them over an authenticated API, so that they &lt;strong>never reside in git&lt;/strong> and, with its dynamic engines, it can issue short-lived database credentials that expire on their own.&lt;/p>
&lt;p>Advantage: native rotation, dynamic secrets, centralised auditing, a single point of governance. Limitation: you have to operate Vault (sealing/unsealing, high availability, access policy), which is real work.&lt;/p>
&lt;h3 id="how-to-choose-and-rotation">How to choose, and rotation&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Criterion&lt;/th>
&lt;th>sealed-secrets&lt;/th>
&lt;th>SOPS + age&lt;/th>
&lt;th>ESO + Vault&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Secret in git&lt;/td>
&lt;td>encrypted&lt;/td>
&lt;td>encrypted&lt;/td>
&lt;td>reference only&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Extra server&lt;/td>
&lt;td>lightweight controller&lt;/td>
&lt;td>none&lt;/td>
&lt;td>Vault (heavy)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Rotation&lt;/td>
&lt;td>automatic sealing keys&lt;/td>
&lt;td>manual (re-encrypt)&lt;/td>
&lt;td>native / dynamic&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Dynamic (short-lived) secrets&lt;/td>
&lt;td>no&lt;/td>
&lt;td>no&lt;/td>
&lt;td>yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Fit with pure GitOps&lt;/td>
&lt;td>excellent&lt;/td>
&lt;td>excellent&lt;/td>
&lt;td>good (references)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Operational curve&lt;/td>
&lt;td>low&lt;/td>
&lt;td>very low&lt;/td>
&lt;td>high&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>For a sovereign 4×H100 getting started, &lt;strong>SOPS/age or sealed-secrets&lt;/strong> cover 90% at minimal cost. When the system grows and requirements for frequent rotation and auditing appear, typical in ENS High Category scenarios, you migrate to &lt;strong>ESO + Vault&lt;/strong>. The rotation rule: &lt;strong>a secret that never rotates is a secret that, once leaked, is leaked forever&lt;/strong>. Rotation does not prevent the leak; it bounds its window of validity. We will come back to that when we talk about blast radius.&lt;/p>
&lt;h2 id="layer-2--network-default-deny-egress-and-the-arithmetic-of-the-surface">Layer 2 — Network: default-deny, egress and the arithmetic of the surface&lt;/h2>
&lt;h3 id="the-arithmetic-of-communication-pairs">The arithmetic of communication pairs&lt;/h3>
&lt;p>Without a network policy, in Kubernetes &lt;strong>every pod can talk to every pod&lt;/strong>. That is the default posture, and it is the worst one for a sovereign system. With $N$ services, the number of possible ordered communication pairs (who-can-call-whom) is:&lt;/p>
$$P_{\text{abierto}} = N \cdot (N-1) \approx N^2$$
&lt;p>With our $N = 8$ components (gateway, vLLM, TEI, vector database, state database, front end, plus the secrets controller and the observability one), that is $8 \cdot 7 = 56$ possible directed pairs. Fifty-six paths along which a compromised service could pivot laterally.&lt;/p>
&lt;p>Now we apply a default-deny &lt;code>NetworkPolicy&lt;/code>: nothing talks to anything except what is explicitly allowed. The real whitelist of an LLM stack is small. The strictly necessary pairs:&lt;/p>
&lt;ul>
&lt;li>front end → gateway&lt;/li>
&lt;li>gateway → vLLM&lt;/li>
&lt;li>gateway → TEI&lt;/li>
&lt;li>gateway → vector database&lt;/li>
&lt;li>gateway → state database&lt;/li>
&lt;li>front end → state database (sessions/users)&lt;/li>
&lt;/ul>
&lt;p>That is $E = 6$ edges. The communication surface falls from $56$ to $6$:&lt;/p>
$$\frac{E}{P_{\text{abierto}}} = \frac{6}{56} \approx 0.107$$
&lt;p>Almost &lt;strong>89% of the possible paths are closed&lt;/strong>. The front end can no longer touch vLLM or TEI or the vector database directly; if somebody compromises the front end, they have no network route to the corpus. This is the difference between $\sim N^2$ and a whitelist $E \ll N^2$.&lt;/p>
&lt;div class="diagram" style="max-width:720px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 720 380" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Service graph with default-deny: allowed edges in green, blocked ones in dotted red">
&lt;text x="360" y="24" text-anchor="middle" font-family="sans-serif" font-size="13" font-weight="700" fill="currentColor">Default-deny model — green allowed, red blocked&lt;/text>
&lt;!-- nodes -->
&lt;circle cx="120" cy="90" r="34" fill="#3b82f6" fill-opacity="0.15" stroke="#3b82f6" stroke-width="2"/>
&lt;text x="120" y="94" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="600" fill="currentColor">front&lt;/text>
&lt;circle cx="360" cy="90" r="34" fill="#3b82f6" fill-opacity="0.15" stroke="#3b82f6" stroke-width="2"/>
&lt;text x="360" y="94" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="600" fill="currentColor">gateway&lt;/text>
&lt;circle cx="600" cy="60" r="34" fill="#3b82f6" fill-opacity="0.15" stroke="#3b82f6" stroke-width="2"/>
&lt;text x="600" y="64" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="600" fill="currentColor">vLLM&lt;/text>
&lt;circle cx="600" cy="160" r="34" fill="#3b82f6" fill-opacity="0.15" stroke="#3b82f6" stroke-width="2"/>
&lt;text x="600" y="164" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="600" fill="currentColor">TEI&lt;/text>
&lt;circle cx="360" cy="270" r="34" fill="#3b82f6" fill-opacity="0.15" stroke="#3b82f6" stroke-width="2"/>
&lt;text x="360" y="267" text-anchor="middle" font-family="sans-serif" font-size="10" font-weight="600" fill="currentColor">vector&lt;/text>
&lt;text x="360" y="280" text-anchor="middle" font-family="sans-serif" font-size="10" font-weight="600" fill="currentColor">DB&lt;/text>
&lt;circle cx="120" cy="270" r="34" fill="#3b82f6" fill-opacity="0.15" stroke="#3b82f6" stroke-width="2"/>
&lt;text x="120" y="267" text-anchor="middle" font-family="sans-serif" font-size="10" font-weight="600" fill="currentColor">state&lt;/text>
&lt;text x="120" y="280" text-anchor="middle" font-family="sans-serif" font-size="10" font-weight="600" fill="currentColor">DB&lt;/text>
&lt;!-- allowed edges (green) -->
&lt;path d="M154,90 L326,90" stroke="#22c55e" stroke-width="2.4" fill="none"/>
&lt;path d="M392,78 L568,66" stroke="#22c55e" stroke-width="2.4" fill="none"/>
&lt;path d="M392,102 L568,150" stroke="#22c55e" stroke-width="2.4" fill="none"/>
&lt;path d="M372,122 L360,236" stroke="#22c55e" stroke-width="2.4" fill="none"/>
&lt;path d="M340,120 L140,238" stroke="#22c55e" stroke-width="2.4" fill="none"/>
&lt;path d="M120,124 L120,236" stroke="#22c55e" stroke-width="2.4" fill="none"/>
&lt;!-- blocked edges (red dashed) -->
&lt;path d="M146,116 L334,250" stroke="#ef4444" stroke-width="1.6" stroke-dasharray="5 4" fill="none"/>
&lt;path d="M150,108 L572,150" stroke="#ef4444" stroke-width="1.6" stroke-dasharray="5 4" fill="none"/>
&lt;path d="M154,96 L566,62" stroke="#ef4444" stroke-width="1.6" stroke-dasharray="5 4" fill="none"/>
&lt;path d="M388,256 L572,170" stroke="#ef4444" stroke-width="1.6" stroke-dasharray="5 4" fill="none"/>
&lt;text x="360" y="350" text-anchor="middle" font-family="sans-serif" font-size="11" fill="#22c55e">6 allowed edges&lt;/text>
&lt;text x="360" y="366" text-anchor="middle" font-family="sans-serif" font-size="11" fill="#ef4444">~50 remaining paths closed&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h3 id="standard-l3l4-l7-and-egress-with-cilium">Standard L3/L4, L7 and egress with Cilium&lt;/h3>
&lt;p>Kubernetes&amp;rsquo; native &lt;code>NetworkPolicy&lt;/code> operates at L3/L4: it selects pods by label and allows/denies by port and protocol. That covers the bulk of the whitelist. But it has limits: it does not understand DNS or HTTP. This is where &lt;a href="https://docs.cilium.io/en/stable/network/kubernetes/policy/">Cilium&lt;/a> comes in, applying policies in the kernel via eBPF and extending them with &lt;code>CiliumNetworkPolicy&lt;/code>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Egress by FQDN/DNS&lt;/strong>: instead of pinning IPs (which change), allow &lt;code>egress&lt;/code> only to &lt;code>huggingface.co&lt;/code> so vLLM can download the model and block the rest. Critical for egress control.&lt;/li>
&lt;li>&lt;strong>L7&lt;/strong>: allow only certain HTTP methods/paths between the gateway and an internal service.&lt;/li>
&lt;li>&lt;strong>Enforcement modes&lt;/strong>: in the default mode, an endpoint with no policy selecting it has everything open; in &lt;em>always&lt;/em> mode, &lt;strong>everything is denied&lt;/strong> until a policy explicitly opens it. For a sovereign system, &lt;em>always&lt;/em> + default-deny is the target posture.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Egress control is the sovereign piece par excellence.&lt;/strong> An internal service being unable to open connections to the internet except to a handful of explicit destinations means that, even if vLLM or the gateway is compromised, &lt;strong>the corpus cannot leave the perimeter&lt;/strong>. The RAG is the asset of value; egress-deny is what stops it leaking out.&lt;/p>
&lt;h3 id="internal-mtls">Internal mTLS&lt;/h3>
&lt;p>The &lt;code>NetworkPolicy&lt;/code> says &lt;em>who&lt;/em> can talk to whom, but it neither encrypts nor authenticates east-west traffic. For that, mTLS (mutual TLS: client and server authenticate each other). Cilium offers native mutual authentication; alternatives such as Linkerd or Istio give you a full service mesh. For a six-service stack, Istio&amp;rsquo;s full mesh is usually over-engineering; Cilium&amp;rsquo;s mutual authentication or Linkerd (lighter) is proportionate. The effect: an attacker on the cluster network cannot impersonate the gateway or eavesdrop on internal traffic in the clear.&lt;/p>
&lt;h2 id="layer-3--pod-security-restricted-and-nothing-privileged">Layer 3 — Pod security: restricted, and nothing privileged&lt;/h2>
&lt;p>The &lt;a href="https://kubernetes.io/docs/concepts/security/pod-security-standards/">Pod Security Standards&lt;/a> define three profiles: &lt;code>privileged&lt;/code> (no restrictions), &lt;code>baseline&lt;/code> (the reasonable minimum) and &lt;code>restricted&lt;/code> (hardened). The target for the whole LLM stack is &lt;code>restricted&lt;/code>, applied via Pod Security Admission with a namespace label in &lt;code>enforce&lt;/code> mode. The &lt;code>restricted&lt;/code> profile requires:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>runAsNonRoot: true&lt;/code>&lt;/strong> and a &lt;code>runAsUser&lt;/code> other than 0. No container runs as root.&lt;/li>
&lt;li>&lt;strong>&lt;code>readOnlyRootFilesystem: true&lt;/code>&lt;/strong>. The root filesystem is read-only; anything that needs to write goes to an &lt;code>emptyDir&lt;/code> or an explicit volume. An attacker cannot leave persistent binaries in the container.&lt;/li>
&lt;li>&lt;strong>&lt;code>drop: [&amp;quot;ALL&amp;quot;]&lt;/code>&lt;/strong> of Linux capabilities. No capabilities that are not needed.&lt;/li>
&lt;li>&lt;strong>seccomp &lt;code>RuntimeDefault&lt;/code>&lt;/strong>, which applies the runtime&amp;rsquo;s profile and blocks dangerous syscalls.&lt;/li>
&lt;li>&lt;strong>&lt;code>allowPrivilegeEscalation: false&lt;/code>&lt;/strong>, nothing &lt;code>privileged&lt;/code>, no host namespaces.&lt;/li>
&lt;/ul>
&lt;p>The nuance with GPUs: pods using an H100 load NVIDIA&amp;rsquo;s device plugin, which historically tempted people to relax the &lt;code>securityContext&lt;/code>. You do not need to run the inference container as privileged in order to use the GPU; device access is managed by the plugin, and the vLLM pod can and should run &lt;code>restricted&lt;/code>. This is a fire partition: if vLLM is compromised, the attacker has a non-root process, with no capabilities, a read-only FS and trimmed syscalls — a terrible starting point for escalation.&lt;/p>
&lt;h2 id="layer-4--image-supply-chain">Layer 4 — Image supply chain&lt;/h2>
&lt;p>The container image is third-party code running with access to your data. Three composed controls:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Pin by digest, not by tag.&lt;/strong> &lt;code>vllm/vllm-openai:latest&lt;/code> is a moving target; &lt;code>@sha256:...&lt;/code> is immutable. Pinning the digest guarantees that what was deployed is exactly what was audited.&lt;/li>
&lt;li>&lt;strong>Scanning with &lt;a href="https://github.com/aquasecurity/trivy">Trivy&lt;/a>.&lt;/strong> Before promoting an image, Trivy enumerates its CVEs and composes the SBOM (component inventory). The pipeline fails if there are unmitigated critical vulnerabilities.&lt;/li>
&lt;li>&lt;strong>Signing with &lt;a href="https://docs.sigstore.dev/cosign/signing/signing_with_containers/">cosign&lt;/a> and verification at admission.&lt;/strong> The Sigstore project (cosign for signing, Fulcio as a CA for ephemeral certificates via OIDC, Rekor as an immutable transparency log) allows the image to be signed. And Sigstore&amp;rsquo;s &lt;a href="https://github.com/sigstore/policy-controller">policy-controller&lt;/a>, or Kyverno, is an &lt;strong>admission controller&lt;/strong> that verifies the signature before admitting the pod: an image without a valid signature does not enter the cluster.&lt;/li>
&lt;/ol>
&lt;p>This is the doorman from the analogy: he checks each image&amp;rsquo;s credential (signature) at the door. A poisoned image uploaded to a registry, or a hijacked tag, stays outside because it carries no signature from the trusted issuer.&lt;/p>
&lt;h2 id="layer-5--authnz-virtual-keys-oidc-and-minimal-rbac">Layer 5 — AuthN/Z: virtual keys, OIDC and minimal RBAC&lt;/h2>
&lt;p>Three identity control points, from the outside in:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Front end (OIDC/LDAP).&lt;/strong> The human user authenticates against the corporate identity provider. The front end does not invent its own user system; it delegates to OIDC. This gives SSO, MFA and centralised revocation.&lt;/li>
&lt;li>&lt;strong>Gateway (LiteLLM virtual keys).&lt;/strong> The gateway issues &lt;strong>virtual keys&lt;/strong>: each team, application or user has its own key with a budget, a &lt;em>rate limit&lt;/em> and permitted models. The gateway&amp;rsquo;s master key, and the real API keys towards the engine, are never seen by the client; clients only handle their virtual key, revocable individually. If a virtual key leaks, that one and only that one is revoked.&lt;/li>
&lt;li>&lt;strong>Kubernetes RBAC with least privilege.&lt;/strong> Each service&amp;rsquo;s &lt;code>ServiceAccount&lt;/code> has exactly the permissions it needs. The vLLM pod does not need to list &lt;code>Secret&lt;/code> objects in other namespaces or create pods. Restrictive RBAC means a stolen service account token opens very little.&lt;/li>
&lt;/ul>
&lt;h2 id="layer-6--runtime-the-cameras-with-tetragon">Layer 6 — Runtime: the cameras with Tetragon&lt;/h2>
&lt;p>The previous layers are preventive. What is missing is detection: what if something, despite everything, runs where it should not? &lt;a href="https://tetragon.io/">Tetragon&lt;/a> is observable security and runtime enforcement over eBPF, &lt;em>Kubernetes-aware&lt;/em>. It hooks kernel events — &lt;code>process_exec&lt;/code>, &lt;code>tcp_connect&lt;/code>, &lt;code>security_file_open&lt;/code> — with typical overhead below 1%, and it can move from observing (recording the event) to enforcement (killing the process or cutting the connection in the kernel, &lt;code>Sigkill&lt;/code>).&lt;/p>
&lt;p>The operational rule, which I set out in detail in the &lt;a href="https://blog.lo0.es/en/posts/runbook-caging-ai-agent-bubblewrap-tetragon/">bubblewrap + Tetragon runbook&lt;/a>: &lt;strong>adopt first, block later&lt;/strong>. First you deploy in observation mode to build a baseline of the stack&amp;rsquo;s normal behaviour — which binaries vLLM executes, what the gateway connects to — without false positives. Only afterwards do you promote the clear rules to enforcement: kill any process that tries to read secret paths, or cut every &lt;code>tcp_connect&lt;/code> to destinations outside the egress whitelist. &lt;a href="https://falco.org/">Falco&lt;/a> is the detection-only alternative over eBPF; Tetragon adds enforcement. These cameras also produce the audit evidence: what each service ran and which attempt was blocked.&lt;/p>
&lt;h2 id="layer-7--data-at-rest">Layer 7 — Data at rest&lt;/h2>
&lt;p>The last thing we protect is data standing still:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Storage encryption.&lt;/strong> The persistent volumes, where the vector database and the state database live, on top of disk encryption (LUKS/dm-crypt) or Ceph-level encryption. A disk stolen from the datacenter does not reveal the corpus.&lt;/li>
&lt;li>&lt;strong>&lt;code>etcd&lt;/code> encryption.&lt;/strong> Remember that Kubernetes &lt;code>Secret&lt;/code> objects, once decrypted by sealed-secrets or ESO, are normal objects in &lt;code>etcd&lt;/code>. You have to enable &lt;code>etcd&lt;/code> &lt;em>encryption at rest&lt;/em>, or the secret sits in the clear in the control plane.&lt;/li>
&lt;li>&lt;strong>Database credentials.&lt;/strong> The vector database&amp;rsquo;s password and the state database&amp;rsquo;s are first-class secrets (layer 1), never embedded in the manifest or in environment variables in the clear in git.&lt;/li>
&lt;/ul>
&lt;h2 id="exposure-table-service--port--external-egress">Exposure table: service × port × external egress?&lt;/h2>
&lt;p>This table is the input for writing the &lt;code>NetworkPolicy&lt;/code> resources. The egress column is the one that decides what leaves the perimeter.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Service&lt;/th>
&lt;th>Internal port&lt;/th>
&lt;th>Permitted callers&lt;/th>
&lt;th>External egress?&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>front end&lt;/td>
&lt;td>3080&lt;/td>
&lt;td>(human ingress via OIDC)&lt;/td>
&lt;td>No&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>gateway (LiteLLM)&lt;/td>
&lt;td>4000&lt;/td>
&lt;td>front end&lt;/td>
&lt;td>No&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>vLLM&lt;/td>
&lt;td>8000&lt;/td>
&lt;td>gateway&lt;/td>
&lt;td>Only &lt;code>huggingface.co&lt;/code> for the initial download; zero in operation&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TEI (embeddings/reranker)&lt;/td>
&lt;td>8080&lt;/td>
&lt;td>gateway&lt;/td>
&lt;td>Only model download; zero in operation&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>vector database&lt;/td>
&lt;td>6333&lt;/td>
&lt;td>gateway&lt;/td>
&lt;td>No&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>state database&lt;/td>
&lt;td>5432&lt;/td>
&lt;td>gateway, front end&lt;/td>
&lt;td>No&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>secrets controller&lt;/td>
&lt;td>—&lt;/td>
&lt;td>(control plane)&lt;/td>
&lt;td>No&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>observability&lt;/td>
&lt;td>9090&lt;/td>
&lt;td>internal scraping&lt;/td>
&lt;td>No&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The reading: &lt;strong>no service needs external egress in normal operation&lt;/strong>. vLLM and TEI need it once to pull the model, and that permission can be temporary or restricted by FQDN to &lt;code>huggingface.co&lt;/code>. Everything else is total egress-deny. If a service starts attempting outbound connections this table does not contemplate, Tetragon records it and, in enforcement, cuts it.&lt;/p>
&lt;h2 id="blast-radius-a-leaked-secret-with-and-without-defence">Blast radius: a leaked secret, with and without defence&lt;/h2>
&lt;p>The blast radius measures how much damage a compromise does. Let us model it for the realistic worst case: the state database&amp;rsquo;s credential leaks.&lt;/p>
&lt;p>&lt;strong>Without hardening&lt;/strong> (flat network, unrotated secret, no egress-deny, no detection):&lt;/p>
&lt;ul>
&lt;li>The secret gives access to the state database from any pod (flat network → 56 open pairs).&lt;/li>
&lt;li>The secret does not rotate → it is valid indefinitely; the exploitation window is $\infty$ until somebody notices.&lt;/li>
&lt;li>Without egress-deny, the attacker dumps the whole database to an external server.&lt;/li>
&lt;li>Without Tetragon, nobody finds out until the public incident.&lt;/li>
&lt;li>Blast radius: &lt;strong>the entire state database, exfiltrated, undetected, for an indefinite time&lt;/strong>.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>With hardening&lt;/strong> (default-deny, ESO + Vault with dynamic credentials, egress-deny, Tetragon in enforcement):&lt;/p>
&lt;ul>
&lt;li>The secret is only usable from the pod that has a network route to the state database (1-2 pairs, not 56).&lt;/li>
&lt;li>With Vault&amp;rsquo;s dynamic credentials, the secret expires — say within a window $T$ of hours; past $T$, it is worthless.&lt;/li>
&lt;li>Egress-deny prevents the dump to the internet: the attacker can read, but cannot take anything out.&lt;/li>
&lt;li>Tetragon records the anomalous access and, in enforcement, kills the process attempting the outbound connection.&lt;/li>
&lt;/ul>
&lt;p>The qualitative reduction is enormous, but let us be quantitative about the window. If a static secret is valid forever and a rotated one with period $T$ is valid for at most $T$, and compromises arrive at rate $\lambda$, the expected number of &lt;em>live and exploitable&lt;/em> secrets at any given instant goes from growing without bound to being capped at $\lambda \cdot T$. With rotation every 24 h ($T = 1$ day) against &amp;ldquo;never&amp;rdquo;, a given secret&amp;rsquo;s window falls from months to a day: a reduction of &lt;strong>one to two orders of magnitude&lt;/strong> in temporal exposure. Combined with the reduction in network pairs ($56 \to \sim 2$, a factor of $\sim 28\times$) and egress-deny (from exfiltration possible to impossible by the direct route), the blast radius is drastically reduced.&lt;/p>
&lt;p>&lt;strong>But let us be honest: it does not reach zero.&lt;/strong> The attacker with the credential &lt;em>can read&lt;/em> the state database during the window $T$ from the compromised pod. Hardening turned &amp;ldquo;indefinite, silent catastrophe&amp;rdquo; into &amp;ldquo;a bounded, detected incident with no exfiltration by the direct route&amp;rdquo;. That is exactly what defence in depth promises: not invulnerability, but that &lt;strong>the cost of a full compromise is high and its radius small&lt;/strong>.&lt;/p>
&lt;h2 id="the-ens--nis2-angle">The ENS / NIS2 angle&lt;/h2>
&lt;p>These layers are not voluntary hygiene: they implement specific controls. The mapping is developed in detail in the &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 post&lt;/a>; here, the actionable summary:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Hardening layer&lt;/th>
&lt;th>ENS measure (RD 311/2022)&lt;/th>
&lt;th>NIS2 / framework link&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Encrypted secrets + rotation&lt;/td>
&lt;td>&lt;code>op.exp.11&lt;/code> (cryptographic keys), &lt;code>mp.info.3&lt;/code> (encryption)&lt;/td>
&lt;td>Credential management; in High Cat., HSM&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Default-deny NetworkPolicy + segmentation&lt;/td>
&lt;td>&lt;code>mp.com.1&lt;/code> (perimeter), &lt;code>mp.com.4&lt;/code> (flow separation)&lt;/td>
&lt;td>NIS2 art. 21: network security measures&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Egress control&lt;/td>
&lt;td>&lt;code>mp.com.1&lt;/code> + &lt;code>op.mon.1&lt;/code>&lt;/td>
&lt;td>Exfiltration prevention&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Internal mTLS&lt;/td>
&lt;td>&lt;code>mp.com.2-3&lt;/code> (confidentiality/integrity in transit)&lt;/td>
&lt;td>Mandatory TLS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Restricted Pod Security&lt;/td>
&lt;td>&lt;code>op.exp.2&lt;/code> (hardened configuration)&lt;/td>
&lt;td>Configuration hardening (CIS)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Trivy + cosign + admission&lt;/td>
&lt;td>&lt;code>op.exp.6&lt;/code> (malicious code), &lt;code>op.ext.3&lt;/code> (supply chain)&lt;/td>
&lt;td>NIS2 supply chain&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>OIDC + virtual keys + RBAC&lt;/td>
&lt;td>&lt;code>op.acc.1-2-5&lt;/code> (identification, access, authentication)&lt;/td>
&lt;td>MFA in High Cat.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tetragon runtime&lt;/td>
&lt;td>&lt;code>op.mon.1&lt;/code> (intrusion detection)&lt;/td>
&lt;td>Monitoring and response&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Encryption at rest + etcd&lt;/td>
&lt;td>&lt;code>mp.info.3&lt;/code> (encryption), &lt;code>mp.si&lt;/code> (media)&lt;/td>
&lt;td>Data at rest&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The honest note for auditing: &lt;strong>hardening reduces risk, it does not eliminate it&lt;/strong>, and maturity is demonstrated by &lt;strong>prioritising by impact&lt;/strong>. A competent auditor does not want to see nine half-finished layers; they want to see that egress-deny on the critical asset (the corpus) and secrets management are solid before mTLS is perfect between low-sensitivity services. For the management and governance context, see also &lt;a href="https://blog.lo0.es/en/posts/iso-42001-aims-on-premise-llm-platform/">ISO/IEC 42001 as an AIMS&lt;/a> and the &lt;a href="https://blog.lo0.es/en/posts/eu-ai-act-technical-dossier-on-premise-llm-architecture/">EU AI Act mapping onto the architecture&lt;/a>.&lt;/p>
&lt;h2 id="applied-to-the-generic-4h100-cluster">Applied to the generic 4×H100 cluster&lt;/h2>
&lt;p>In a real deployment on 4×H100 SXM 80GB, you do not harden the nine layers at once. The order, prioritised by impact:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Egress-deny on vLLM, TEI and the vector database, first.&lt;/strong> It is the barrier that stops the corpus leaving the perimeter, and it goes in as soon as the services start. Allow egress by FQDN to &lt;code>huggingface.co&lt;/code> only during the model&amp;rsquo;s initial download; afterwards, zero. It is the layer with the highest return per hour invested.&lt;/li>
&lt;li>&lt;strong>Secrets for the inference engine and the databases, out of git.&lt;/strong> The Hugging Face token vLLM uses to download the model, the vector database&amp;rsquo;s and the state database&amp;rsquo;s passwords, the gateway&amp;rsquo;s master key: into sealed-secrets or, if there is already a rotation requirement, into ESO + Vault. Never in &lt;code>values.yaml&lt;/code> in the clear.&lt;/li>
&lt;li>&lt;strong>Default-deny NetworkPolicy + the 6-edge whitelist.&lt;/strong> The gateway and the vector database are the most exposed services — the gateway because it receives all the traffic, the vector database because it holds the embedded RAG. Closing everything that is not on the whitelist cuts lateral pivoting from $\sim N^2$ to the 6 real edges.&lt;/li>
&lt;li>&lt;strong>&lt;code>restricted&lt;/code> Pod Security in the inference namespace&lt;/strong>, including the vLLM pod with a GPU (it needs no privileges to use the H100).&lt;/li>
&lt;li>&lt;strong>cosign + admission&lt;/strong> so only signed images get in; &lt;strong>Trivy&lt;/strong> in the GitOps pipeline of &lt;a href="https://blog.lo0.es/en/posts/gitops-inference-stack-flux/">Flux&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Tetragon in observation&lt;/strong>, baseline, and then enforcement over the egress of the sensitive pods.&lt;/li>
&lt;li>&lt;strong>Encryption at rest&lt;/strong> for the databases&amp;rsquo; volumes and for &lt;code>etcd&lt;/code>.&lt;/li>
&lt;/ol>
&lt;p>The gateway and the vector database are the two I harden first inside the whitelist: the gateway for being the exposed face, the vector database for holding the asset that egress-deny protects. The rest is built on top, layer by layer, always assuming the previous one can fail.&lt;/p>
&lt;h2 id="what-hardening-does-not-solve">What hardening does NOT solve&lt;/h2>
&lt;p>To close honestly, what these layers do not cover and what needs other pieces:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Prompt-level attacks&lt;/strong> (jailbreak, indirect injection via the RAG corpus): that is the job of &lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">guardrails&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/llm-guard-anatomy-scanners-langfuse-vllm-litellm/">LLM Guard&lt;/a>, not of NetworkPolicy.&lt;/li>
&lt;li>&lt;strong>Agents with legitimate permissions doing something harmful&lt;/strong>: the runtime isolation from the &lt;a href="https://blog.lo0.es/en/posts/isolating-ai-agents-workstation-to-cluster/">agent isolation post&lt;/a> bounds what an agent can touch, but a granted permission is a usable permission.&lt;/li>
&lt;li>&lt;strong>The human factor&lt;/strong>: a well-managed secret shared over Slack is still leaked. Rotation bounds the window, it does not eliminate the mistake.&lt;/li>
&lt;li>&lt;strong>Zero-day vulnerabilities&lt;/strong> in the components themselves: Trivy detects what is known; the unknown gets through until the CVE is published.&lt;/li>
&lt;/ul>
&lt;p>Hardening is a multiplier on the cost of attacking, not an impassable wall. Its value lies in making an individual compromise &lt;strong>bounded, detected and with no way out&lt;/strong> — and that, for a sovereign system holding sensitive data, is exactly the goal.&lt;/p>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/gitops-inference-stack-flux/">GitOps of the inference stack with Flux&lt;/a> — the sibling piece: GitOps reconciles the state, and shares with this post the chicken-and-egg problem of secrets.&lt;/li>
&lt;li>&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 detailed mapping of each hardening layer to an ENS measure, a 42001 control and an AI Act article.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/iso-42001-aims-on-premise-llm-platform/">ISO/IEC 42001: the AIMS for the on-premise LLM&lt;/a> — the management system that frames hardening as a documented control.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/eu-ai-act-technical-dossier-on-premise-llm-architecture/">EU AI Act: mapping onto the on-premise LLM architecture&lt;/a> — the robustness and cybersecurity articles (Art. 15) these layers satisfy.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-guard-anatomy-scanners-langfuse-vllm-litellm/">LLM Guard: fundamentals&lt;/a> — the prompt/content-level security layer that infrastructure hardening does not cover.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">Guardrails and safety in LLMs&lt;/a> — the semantic WAF that complements the NetworkPolicy.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/isolating-ai-agents-workstation-to-cluster/">Isolating AI agents: from workstation to cluster&lt;/a> — the threat model of runtime isolation.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/runbook-caging-ai-agent-bubblewrap-tetragon/">Runbook: caging the AI agent with bubblewrap and Tetragon&lt;/a> — the operational procedure for Tetragon (observe first, block later) referenced in layer 6.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/model-chain-of-trust-signature-provenance-aibom-why-you-trust-those-bytes/">Model chain of trust (3/4): signature, provenance and AIBOM&lt;/a> — layer 4 of this post (image supply chain with cosign and Trivy) taken to the model artefact: signing the weights, SLSA provenance attestations and an AIBOM inventory.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/model-chain-of-trust-who-serves-the-model-which-machine-you-trust/">Model chain of trust (4/4): who serves the model and which machine you trust&lt;/a> — the step after layer 2&amp;rsquo;s mTLS: cryptographic workload identity with SPIFFE/SPIRE and isolation of the execution environment in a TEE.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/litellm-virtual-keys-budgets-limits/">Virtual keys, budgets and limits in LiteLLM&lt;/a> — key hashing, the &lt;code>LITELLM_SALT_KEY&lt;/code> variable that falls back to the master key by default, and rotation with a grace period.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/keycloak-identity-plane-ai-platform/">Keycloak in an AI platform&lt;/a> — layer 5 of this article developed: who can authenticate against the IdP, what costs a licence and what it does not solve.&lt;/li>
&lt;/ul></description></item><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></channel></rss>