<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Mtls on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/mtls/</link><description>Recent content in Mtls on lo0 — Tech Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Sun, 26 Jul 2026 17:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/en/tags/mtls/index.xml" rel="self" type="application/rss+xml"/><item><title>Model chain of trust (4/4): who serves the model and which machine you trust</title><link>https://blog.lo0.es/en/posts/model-chain-of-trust-who-serves-the-model-which-machine-you-trust/</link><pubDate>Sun, 26 Jul 2026 17:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/model-chain-of-trust-who-serves-the-model-which-machine-you-trust/</guid><description>&lt;p>The three previous articles closed questions about the artefact: what contract the served model exposes, where its bytes come from and why you trust them. What remains is the one almost nobody asks until an auditor asks it: nothing proves that the process currently returning tokens on port 8000 is the inference engine and not something that slipped into the cluster and inherited its token. And a second one, worse: whether the machine it runs on is trustworthy, and against whom.&lt;/p>
&lt;p>These are different problems with different projects. &lt;strong>Workload identity&lt;/strong> is solved by SPIFFE/SPIRE, a CNCF graduated project since 2022. &lt;strong>Environment isolation&lt;/strong> is covered by Kata Containers and Confidential Containers, the latter promoted to CNCF &lt;em>incubating&lt;/em> on 22 July 2026, four days before this article was published. The first is cheap and almost always worth it. The second is expensive, more than the vendor&amp;rsquo;s press release suggests, and is only justified against a specific threat model.&lt;/p>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>User identity ≠ workload identity.&lt;/strong> OIDC says which person is behind a request; it does not say which process serves it. &lt;code>ServiceAccount&lt;/code> tokens, static keys and gateway API keys are &lt;em>bearer&lt;/em> credentials: whoever holds them, is them.&lt;/li>
&lt;li>&lt;strong>SPIFFE&lt;/strong> defines a URI identifier (&lt;code>spiffe://domain/path&lt;/code>) and a verifiable document (&lt;strong>SVID&lt;/strong>, X.509 or JWT) delivered by a Workload API that does not require the workload to hold any prior secret. SPIRE implements it with node and workload attestation; defaults: &lt;code>default_x509_svid_ttl&lt;/code> 1 h, &lt;code>default_jwt_svid_ttl&lt;/code> 5 min, &lt;code>ca_ttl&lt;/code> 24 h.&lt;/li>
&lt;li>Identity is only worth something if something enforces it: Istio/Envoy via SDS, Cilium with mutual authentication (with serious reservations) or an &lt;code>ext_authz&lt;/code> with OPA in front of the gateway.&lt;/li>
&lt;li>&lt;strong>Three rungs of isolation&lt;/strong>: container with a shared kernel → Kata Containers (its own kernel and VM; v3.32.0 from June 2026) → Confidential Containers (hardware TEE, hypervisor outside the trust base, remote attestation with Trustee per RFC 9334).&lt;/li>
&lt;li>&lt;strong>The overhead of confidential mode runs from ~0 % to ~28 %&lt;/strong> depending on what you measure: GPU-only in CC with large batches sits around 4-8 %, while the full stack (a CVM with Intel TDX plus an H100 in CC), measured independently in 2026, gives +21.8 % to +27.8 % on TTFT and −17.7 % to −21.1 % on throughput. Honest heuristic: reserve 15-25 % extra capacity.&lt;/li>
&lt;li>&lt;strong>MIG and vGPU are forbidden in CC mode&lt;/strong> according to NVIDIA&amp;rsquo;s reference architecture, and every GPU on a node goes in the same mode.&lt;/li>
&lt;li>&lt;strong>Closing criterion&lt;/strong>: the order is contract → digest → signature verified at admission → workload identity → TEE.&lt;/li>
&lt;/ul>
&lt;h2 id="the-analogy-the-biosafety-laboratory">The analogy: the biosafety laboratory&lt;/h2>
&lt;p>A laboratory handling pathogens solves two problems much like ours, separately.&lt;/p>
&lt;p>The first is who gets in. The company badge is not enough: at the airlock there is a specific credential, short-lived and automatically renewed (if lost, it expires on its own within an hour), issued only after independently checking that the applicant is where they say they are and is who they say they are. That credential is the &lt;strong>SVID&lt;/strong>; the prior check is &lt;strong>attestation&lt;/strong>.&lt;/p>
&lt;p>The second is which room you work in, and here there are containment levels. On the open bench the air is shared and anything aerosolised affects everyone: the normal container, with a kernel shared by all the neighbours. One rung up, the biological safety cabinet, with a physical barrier and its own airflow: Kata Containers. At the very top, the maximum containment laboratory, where maintenance staff change filters without ever seeing the sample: the TEE, where the operator maintains the machine but cannot read its memory. And before opening the airlock somebody verifies that the integrity of the suit and of the room is as expected: remote attestation with conditional secret release.&lt;/p>
&lt;p>The uncomfortable lesson: the maximum containment level is a cost decision before it is one more degree of security. Nobody builds a BSL-4 to grow yeast. The useful question is who it isolates from, not how much.&lt;/p>
&lt;h2 id="part-a--workload-identity">Part A — Workload identity&lt;/h2>
&lt;h3 id="six-services-talking-to-each-other-and-none-of-them-knows-who-the-other-is">Six services talking to each other and none of them knows who the other is&lt;/h3>
&lt;p>In a serious inference cluster, like the &lt;a href="https://blog.lo0.es/en/posts/anatomy-on-premise-llm-inference-stack-seven-layers/">seven layers&lt;/a> one, the gateway talks to the serving engine, the engine queries the vector database, the trace collector receives &lt;em>spans&lt;/em> from everyone and the agents with MCP invoke tools that in turn call the gateway. Many east-west connections and, in most deployments, none of them authenticated.&lt;/p>
&lt;p>User identity says which person is behind the request and is resolved with OIDC, as when &lt;a href="https://blog.lo0.es/en/posts/mcp-grows-up-authentication-keycloak/">putting Keycloak in front of MCP&lt;/a>. Workload identity says which process issues it: there is no human on the other side, no browser, no consent, and the process is born and dies in seconds. Its three usual substitutes fail for different reasons:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Mechanism&lt;/th>
&lt;th>Where it fails&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Projected &lt;code>ServiceAccount&lt;/code> token&lt;/td>
&lt;td>SA granularity, not workload granularity: two pods with the same SA are indistinguishable. It does not cross the cluster boundary and does not federate. It is a &lt;em>bearer token&lt;/em> on the filesystem&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Static key&lt;/td>
&lt;td>It does not rotate, it gets shared over Slack, it does not distinguish issuer from bearer; once leaked, it is valid until somebody notices&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Gateway API key&lt;/td>
&lt;td>It authenticates the &lt;em>client&lt;/em>, not the process. LiteLLM&amp;rsquo;s virtual keys are for quota and budget, not for proving identity&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>There is no cryptographic link between credential and process: copy the credential, copy the identity. In a multi-tenant cluster like the &lt;a href="https://blog.lo0.es/en/posts/gpu-cluster-as-multi-tenant-platform/">H100 cluster&lt;/a> one, real isolation then depends on network topology and not on identity.&lt;/p>
&lt;h3 id="spiffe-the-standard">SPIFFE: the standard&lt;/h3>
&lt;p>&lt;a href="https://spiffe.io/docs/latest/spiffe-about/spiffe-concepts/">SPIFFE&lt;/a> and its implementation &lt;a href="https://github.com/spiffe/spire">SPIRE&lt;/a> graduated together in the CNCF on 20 September 2022. As of July 2026 they are settled infrastructure, not a bet.&lt;/p>
&lt;p>&lt;strong>The SPIFFE ID&lt;/strong> is a URI of the form &lt;code>spiffe://&amp;lt;trust-domain&amp;gt;/&amp;lt;workload-identifier&amp;gt;&lt;/code>, for example &lt;code>spiffe://inferencia.example/ns/inferencia/sa/vllm&lt;/code>: a name, not a credential. &lt;strong>The trust domain&lt;/strong> is the root of trust (organisation, environment or site), and the guidance recommends separating workloads from different sites or security environments into different domains.&lt;/p>
&lt;p>The SVID is the credential that proves that ID. The &lt;strong>X509-SVID&lt;/strong> carries the SPIFFE ID in the URI-type SAN, not in the &lt;code>CN&lt;/code>, which the specification discourages as a source of identity, and it is the preferred format. The &lt;strong>JWT-SVID&lt;/strong> exists for when there are L7 proxies terminating TLS in the middle, with the replay risk the documentation warns about:&lt;/p>
&lt;pre tabindex="0">&lt;code>Certificate:
Issuer: C=ES, O=SPIFFE
Validity
Not Before: Jul 26 08:00:00 2026 GMT
Not After : Jul 26 09:00:00 2026 GMT
Subject: C=ES, O=SPIRE, CN=vllm.inferencia
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication, TLS Web Client Authentication
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Subject Alternative Name:
URI:spiffe://inferencia.example/ns/inferencia/sa/vllm
&lt;/code>&lt;/pre>&lt;p>One hour of validity, and what authenticates is the SAN URI: the &lt;code>CN&lt;/code> is decorative.&lt;/p>
&lt;p>&lt;strong>The Workload API&lt;/strong> is the elegant piece: a local UNIX socket from which the workload obtains its SVID, its private key and the trust bundle, without presenting any secret. The documentation is explicit: &amp;ldquo;the Workload API does not require that a calling workload have any knowledge of its own identity, or possess any authentication token&amp;rdquo;. That solves the cold start of all identity cryptography: the secret you would need in order to obtain the first secret.&lt;/p>
&lt;h3 id="spire-how-that-document-is-issued">SPIRE: how that document is issued&lt;/h3>
&lt;p>SPIRE has a server (the authority that signs) and one agent per node (which exposes the Workload API), and it chains two checks.&lt;/p>
&lt;p>&lt;strong>Node attestation&lt;/strong> verifies that the agent runs where it says it does. In Kubernetes the reference attestor is &lt;code>k8s_psat&lt;/code>, which validates a projected token with an audience against the API server; on bare metal there are TPM-based attestors. A note of honesty: SPIRE v1.15.1, from 28 May 2026, is a security patch over an incorrect PKCS7 validation in the &lt;code>azure_imds&lt;/code> attestor that allowed attested documents to be forged and a virtual machine to be impersonated.&lt;/p>
&lt;p>&lt;strong>Workload attestation&lt;/strong> answers the opening question. The &lt;a href="https://github.com/spiffe/spire/blob/main/doc/plugin_agent_workloadattestor_k8s.md">&lt;code>k8s&lt;/code> attestor&lt;/a> receives the PID of the process that opens the socket, infers the pod from its cgroup membership and queries the kubelet for the metadata. From there come the &lt;strong>selectors&lt;/strong>: namespace, service account, pod name and UID, labels, owner, node and, above all, container and image by tag or by digest. From SPIRE v1.15.0 (19 May 2026) Sigstore support stopped being experimental, enabling selectors by signature verification status, certificate subject and issuer, and transparency log. That closes the series&amp;rsquo; circle: you can require that the engine&amp;rsquo;s SVID go only to the container whose image carries a valid cosign signature from the expected issuer, which is what was verified in &lt;a href="https://blog.lo0.es/en/posts/model-chain-of-trust-signature-provenance-aibom-why-you-trust-those-bytes/">article 3/4&lt;/a>. Provenance goes from being a one-off admission check to a precondition of identity at runtime.&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">spire-server entry create &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -parentID spiffe://inferencia.example/spire/agent/k8s_psat/prod/nodo-gpu-a &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -spiffeID spiffe://inferencia.example/ns/inferencia/sa/vllm &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -selector k8s:ns:inferencia &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -selector k8s:sa:vllm &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -selector k8s:container-name:vllm &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -selector k8s:container-image:vllm/vllm-openai@sha256:aa11bb22cc33 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -x509SVIDTTL &lt;span class="m">3600&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -federatesWith spiffe://sede-b.example
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The selectors are &lt;strong>conjunctive&lt;/strong>: a pod in another namespace, with another SA or with another image does not obtain that SVID even if it shares the node.&lt;/p>
&lt;p>The default TTLs have three consequences. Rotation is continuous, not a one-off event: code that loads the certificate once at startup will break exactly one hour later. Server downtime has a clock: twenty minutes are invisible, two hours bring down the cluster&amp;rsquo;s authenticated communications, and the &lt;a href="https://spiffe.io/docs/latest/planning/scaling_spire/">scaling guide&lt;/a> admits that &amp;ldquo;a single SPIRE Server instance represents a single point of failure&amp;rdquo;, with the datastore as the bottleneck and indicative figures running from two replicas of 1 CPU and 1 GB for 10 agents to eight of 16 CPU and 16 GB for 5,000 agents and 10,000 workloads. And tag-based selectors are fragile: the runtime may report one tag or another depending on the node and the moment, so the correct selector is the digest, as in &lt;a href="https://blog.lo0.es/en/posts/model-chain-of-trust-where-the-bytes-live-registry-oci-artefacts/">article 2/4&lt;/a>.&lt;/p>
&lt;h3 id="federation-between-trust-domains">Federation between trust domains&lt;/h3>
&lt;p>Two sites with independent clusters, or an external partner exposing a &lt;em>reranker&lt;/em>, should not share an authority. &lt;a href="https://spiffe.io/docs/latest/spiffe-specs/spiffe_federation/">SPIFFE federation&lt;/a> makes each domain publish a &lt;strong>bundle endpoint&lt;/strong> with its public trust material and the other poll it periodically: the &lt;code>https_web&lt;/code> profile authenticates it with web PKI and &lt;code>https_spiffe&lt;/code> with an X509-SVID from the domain itself, enabling automatic rotation and revocation of the root. The relationship is one-directional, bundles from different domains must never be merged and refresh is by polling with a default suggestion of five minutes. In practice, site A&amp;rsquo;s gateway accepts requests from site B&amp;rsquo;s engine without sharing a CA or an identity database, and cutting the relationship means deleting a bundle, not revoking certificates.&lt;/p>
&lt;h3 id="from-identity-to-effective-authorisation">From identity to effective authorisation&lt;/h3>
&lt;p>An SVID blocks nothing by itself; somebody has to compare the presented ID against a policy.&lt;/p>
&lt;p>&lt;strong>Istio + SPIRE&lt;/strong> is the best-worn integration: Istio detects a UNIX socket implementing Envoy&amp;rsquo;s SDS API and the proxy obtains its identities from there instead of from &lt;code>istiod&lt;/code>, mounted with the &lt;strong>SPIFFE CSI Driver&lt;/strong> (recommended over &lt;code>hostMount&lt;/code>). Two conditions break deployments: SPIRE&amp;rsquo;s trust domain and Istio&amp;rsquo;s must match exactly, and SPIRE only issues to previously registered workloads, including Istio&amp;rsquo;s own components. With that, policy is written in terms of identity and not IP:&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">security.istio.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">AuthorizationPolicy&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">vllm-solo-desde-gateway&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">inferencia&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">selector&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">vllm&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">ALLOW&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">rules&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">from&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">source&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">principals&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;inferencia.example/ns/inferencia/sa/gateway&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">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">operation&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">methods&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;POST&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">paths&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;/v1/chat/completions&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;/v1/completions&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>It is a layer on top of the &lt;code>NetworkPolicy&lt;/code> from &lt;a href="https://blog.lo0.es/en/posts/hardening-secrets-sovereign-llm-stack-defence-in-depth/">layered hardening&lt;/a>: there we closed communication pairs by topology; here we also require who calls and on what path.&lt;/p>
&lt;p>&lt;strong>Cilium&lt;/strong> offers mutual authentication backed by SPIRE, tempting if you already use eBPF for the &lt;a href="https://blog.lo0.es/en/posts/ebpf-zero-to-cilium-kernel-tcp-ip-bypass/">data network&lt;/a>. It deserves criticism: it is still marked &lt;strong>beta&lt;/strong> in the stable documentation, and The New Stack published a reasoned critique arguing that the design does not preserve mTLS properties over the life of the connection (it uses the &lt;em>handshake&lt;/em> only to authenticate and discards the session keys) and that the identity model rests on eventually consistent per-node IP-identity caches, which can allow traffic that policy should deny.&lt;/p>
&lt;p>&lt;strong>The inference gateway and LiteLLM&lt;/strong> do not speak SPIFFE natively as of this article, and saying so is better than suggesting an integration that does not exist. The pattern that does work is the one the project itself documents: an Envoy in front that terminates mTLS with an X509-SVID or validates a JWT-SVID and delegates to OPA via &lt;code>ext_authz&lt;/code>. The gateway carries on with its own job (quota, budget, routing), as we saw when &lt;a href="https://blog.lo0.es/en/posts/choosing-oss-gateway-llm-inference/">choosing an OSS gateway&lt;/a> and in the &lt;a href="https://blog.lo0.es/en/posts/llm-inference-router-l7-switchboard/">L7 router&lt;/a>; workload identity is resolved one hop earlier.&lt;/p>
&lt;h3 id="ai-agents-and-mcp-the-missing-piece">AI agents and MCP: the missing piece&lt;/h3>
&lt;p>An agent invoking tools over MCP is a shared secret with legs: it is handed a static credential, the MCP server cannot know whether the caller is the legitimate agent or any process holding the same string, and the blast radius is the entire set of exposed tools. The &lt;a href="https://blog.lo0.es/en/posts/isolating-ai-agents-workstation-to-cluster/">threat model for agent isolation&lt;/a> warned that a granted permission is a usable permission; with bearer credentials it is also transferable.&lt;/p>
&lt;p>SPIFFE provides what is missing: the agent presents a short-lived SVID obtained through attestation and the MCP server authorises against the SPIFFE ID. In 2026 this stopped being theory: there is work at the IETF on dynamic OAuth client registration based on trusted SPIFFE issuers, and in June 2026 Google published a SPIFFE-based &lt;em>Agent Identity&lt;/em> in its IAM. None of it is fully standardised.&lt;/p>
&lt;p>Two honest caveats. SPIFFE answers &amp;ldquo;who&amp;rdquo;, not &amp;ldquo;why&amp;rdquo;: to know whether the action is the one the user asked for you need traceability (&lt;a href="https://blog.lo0.es/en/posts/mcp-deep-observability-opentelemetry/">MCP with OTel&lt;/a>) and runtime detection with &lt;a href="https://blog.lo0.es/en/posts/tetragon-cilium-security-syscalls-kernel/">Tetragon&lt;/a>. And both identities must be composed: the agent proves its own with an SVID and propagates the delegating user&amp;rsquo;s in the token, authorising on the pair. Collapsing them into one is how you end up with agents able to do, in the system&amp;rsquo;s name, things no user could.&lt;/p>
&lt;h2 id="part-b--isolation-of-the-execution-environment">Part B — Isolation of the execution environment&lt;/h2>
&lt;div class="diagram" style="max-width:820px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 820 300" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Three rungs of isolation: container with a shared kernel, virtual machine sandbox with Kata Containers, and trusted execution environment with Confidential Containers, showing which components stay inside the trusted computing base in each case">
&lt;text x="410" y="22" text-anchor="middle" font-family="sans-serif" font-size="13" font-weight="700" fill="currentColor">What stays INSIDE the trusted computing base (TCB)&lt;/text>
&lt;rect x="20" y="44" width="240" height="200" rx="10" fill="none" stroke="currentColor" stroke-width="1.6"/>
&lt;text x="140" y="68" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="700" fill="currentColor">1. Container (runc)&lt;/text>
&lt;text x="140" y="92" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">namespaces + cgroups + seccomp&lt;/text>
&lt;line x1="40" y1="106" x2="240" y2="106" stroke="currentColor" stroke-width="1"/>
&lt;text x="140" y="126" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">host kernel (shared)&lt;/text>
&lt;text x="140" y="146" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">container runtime&lt;/text>
&lt;text x="140" y="166" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">hypervisor / firmware&lt;/text>
&lt;text x="140" y="186" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">platform operator&lt;/text>
&lt;text x="140" y="216" text-anchor="middle" font-family="sans-serif" font-size="10.5" font-style="italic" fill="currentColor">startup: ~1-6 s&lt;/text>
&lt;text x="140" y="234" text-anchor="middle" font-family="sans-serif" font-size="10.5" font-style="italic" fill="currentColor">cost: zero&lt;/text>
&lt;rect x="290" y="44" width="240" height="200" rx="10" fill="none" stroke="currentColor" stroke-width="1.6"/>
&lt;text x="410" y="68" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="700" fill="currentColor">2. Kata Containers&lt;/text>
&lt;text x="410" y="92" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">own kernel + lightweight VM&lt;/text>
&lt;line x1="310" y1="106" x2="510" y2="106" stroke="currentColor" stroke-width="1"/>
&lt;text x="410" y="126" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">host kernel: OUT&lt;/text>
&lt;text x="410" y="146" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">container runtime: OUT&lt;/text>
&lt;text x="410" y="166" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">hypervisor / firmware: in&lt;/text>
&lt;text x="410" y="186" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">platform operator: in&lt;/text>
&lt;text x="410" y="216" text-anchor="middle" font-family="sans-serif" font-size="10.5" font-style="italic" fill="currentColor">startup: +1-2 s over runc&lt;/text>
&lt;text x="410" y="234" text-anchor="middle" font-family="sans-serif" font-size="10.5" font-style="italic" fill="currentColor">cost: VM memory&lt;/text>
&lt;rect x="560" y="44" width="240" height="200" rx="10" fill="none" stroke="currentColor" stroke-width="2.2"/>
&lt;text x="680" y="68" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="700" fill="currentColor">3. Confidential Containers&lt;/text>
&lt;text x="680" y="92" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">TEE (SEV-SNP / TDX) + GPU in CC&lt;/text>
&lt;line x1="580" y1="106" x2="780" y2="106" stroke="currentColor" stroke-width="1"/>
&lt;text x="680" y="126" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">host kernel: OUT&lt;/text>
&lt;text x="680" y="146" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">hypervisor: OUT&lt;/text>
&lt;text x="680" y="166" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">platform operator: OUT&lt;/text>
&lt;text x="680" y="186" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">CPU/GPU and their firmware: in&lt;/text>
&lt;text x="680" y="216" text-anchor="middle" font-family="sans-serif" font-size="10.5" font-style="italic" fill="currentColor">startup: +10 s or more&lt;/text>
&lt;text x="680" y="234" text-anchor="middle" font-family="sans-serif" font-size="10.5" font-style="italic" fill="currentColor">cost: 0-25 % of performance&lt;/text>
&lt;text x="410" y="272" text-anchor="middle" font-family="sans-serif" font-size="11" fill="currentColor">Each rung takes components out of the TCB. What leaves the TCB can no longer read your memory.&lt;/text>
&lt;text x="410" y="290" text-anchor="middle" font-family="sans-serif" font-size="11" font-style="italic" fill="currentColor">The right question is not "how much does it isolate", but "from whom".&lt;/text>
&lt;/svg>
&lt;/div>
### Kata Containers: the virtual machine sandbox
&lt;p>Kata replaces &lt;code>runc&lt;/code> with a runtime that boots a lightweight virtual machine per pod, with its own kernel and a &lt;code>kata-agent&lt;/code> inside; for Kubernetes it is transparent via &lt;code>RuntimeClass&lt;/code>. As of July 2026 the stable branch is 3.32.x (3.32.0 is from June 2026, with Rust 1.94, Go 1.25.11, QEMU 11.0.1, guest kernel 6.18.35 and containerd 2.3), and in April 2026 the 4.0.0 preview was published, making the Rust runtime (&lt;code>runtime-rs&lt;/code>) the default and leaving the Go one deprecated until 5.0.0, because of memory footprint.&lt;/p>
&lt;p>For GPUs the route is &lt;strong>VFIO passthrough&lt;/strong>. The &lt;a href="https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/deploy-kata-containers.html">NVIDIA GPU Operator&lt;/a> automates it with &lt;code>sandboxWorkloads.enabled=true&lt;/code> and &lt;code>sandboxWorkloads.mode=kata&lt;/code>, installing the VFIO Manager, the Sandbox Device Plugin, the Confidential Computing Manager and the Kata Manager. It requires virtualisation and ACS in the BIOS, IOMMU, kata-deploy 3.29.0 or later, containerd (not CRI-O), the &lt;code>KubeletPodResourcesGet&lt;/code> &lt;em>feature gate&lt;/em> (on by default from Kubernetes 1.34) and removing the NVIDIA drivers from the host: the GPU is handed over whole to the VM and the guest manages it.&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">vllm-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">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">inferencia&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">runtimeClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kata-qemu-nvidia-gpu&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">vllm&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">vllm/vllm-openai@sha256:aa11bb22cc33&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">resources&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">limits&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">nvidia.com/pgpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;1&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Two measurable costs. Memory: the VM reserves its own and you have to declare it as &lt;code>overhead&lt;/code> in the &lt;code>RuntimeClass&lt;/code> so the scheduler does not overcommit the node. And startup: according to the SESAME'24 study on serverless confidential containers, cold start goes from around 6 s with &lt;code>runc&lt;/code> to around 7 s with Kata, and warm start from 1 s to 2 s. Acceptable for an engine that takes minutes to load weights; unacceptable for ephemeral functions.&lt;/p>
&lt;h3 id="confidential-containers-the-tee">Confidential Containers: the TEE&lt;/h3>
&lt;p>&lt;a href="https://confidentialcontainers.org/">Confidential Containers&lt;/a> (CoCo) takes the hypervisor and the platform operator out of the trust base. The CNCF promoted it to &lt;em>incubating&lt;/em> on 22 July 2026, with more than 150 active contributors, 26 repositories and over 1,200 merged &lt;em>pull requests&lt;/em>, and with Microsoft Azure, Intel, AMD, IBM, NVIDIA, Alibaba and Red Hat behind it. A serious project, but &lt;em>incubating&lt;/em> means it is not graduated and that its integration surface changes between versions.&lt;/p>
&lt;p>The components are the CoCo pods (unmodified containers run in a TEE via Kata) and &lt;strong>Trustee&lt;/strong>: &lt;strong>KBS&lt;/strong> (&lt;em>Key Broker Service&lt;/em>, which releases secrets conditionally), &lt;strong>AS&lt;/strong> (&lt;em>Attestation Service&lt;/em>, which validates the hardware evidence) and &lt;strong>RVPS&lt;/strong> (&lt;em>Reference Value Provider Service&lt;/em>, which holds the reference values). Inside the guest, the Attestation Agent collects the evidence and the Confidential Data Hub consumes the secrets.&lt;/p>
&lt;p>The flow is a clean instance of the RATS architecture from RFC 9334: the agent inside the TEE is the &lt;em>attester&lt;/em>, the Attestation Service the &lt;em>verifier&lt;/em> and the Confidential Data Hub the &lt;em>relying party&lt;/em>. The evidence is a hardware-signed report with the boot measurements; the AS validates it against the RVPS values; and only if it matches does the KBS release the key that decrypts the image layers or the weights. Without valid attestation there is no key, and without a key there is no model: that is what stops the operator extracting it.&lt;/p>
&lt;p>A mapping warning: the &lt;code>confidential-containers/operator&lt;/code> repository was archived in February 2026 and deployment moved to the Helm charts and the &lt;code>trustee-operator&lt;/code>. As of July 2026 the latest chart release is v0.21.0, aligned with kata-deploy 3.31.0, and Trustee v0.20.0, with TLS 1.3 and post-quantum cryptography, external plugins for the KBS and multi-GPU support via Intel Trust Authority and NVIDIA NVSwitch.&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">confidentialcontainers.org/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">TrusteeConfig&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">trusteeconfig&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">operators&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">profileType&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Restrictive&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">httpsSpec&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">tlsSecretName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">trustee-tls-cert&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>Permissive&lt;/code> profile exists for development and should not leave it: it is the mode in which attestation does not block.&lt;/p>
&lt;h3 id="the-hardware-cpu-and-gpu">The hardware: CPU and GPU&lt;/h3>
&lt;p>On the CPU side the two relevant TEEs are &lt;strong>AMD SEV-SNP&lt;/strong> and &lt;strong>Intel TDX&lt;/strong>; NVIDIA&amp;rsquo;s reference architecture sets EPYC Milan/Genoa and Xeon Emerald/Granite Rapids as the baseline. Host support is no longer the hard part.&lt;/p>
&lt;p>On the GPU side, the &lt;a href="https://cacm.acm.org/practice/creating-the-first-confidential-gpus/">technical description of the design of the first confidential GPUs&lt;/a> explains what CC mode on an H100 buys you. Memory is split into a &lt;strong>Compute Protected Region&lt;/strong> that hardware firewalls stop both the CPU over PCIe and other GPUs over NVLink from accessing. Everything crossing the CPU-GPU boundary passes through &lt;strong>bounce buffers&lt;/strong> encrypted with AES-GCM-256, and command buffers and CUDA kernels are encrypted and signed before crossing the bus. There is a chain of trust from GPU boot with a signed attestation report, and only NVIDIA-signed firmware runs in CC mode, validated against NRAS or locally in isolated environments. And the performance counters are disabled in hardware as a side-channel mitigation: that is the telemetry much of &lt;a href="https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/">observability with DCGM&lt;/a> depends on.&lt;/p>
&lt;p>What it does not protect: the package&amp;rsquo;s HBM memory is not encrypted (it is considered resistant to interposers, which is a claim about physical difficulty and not a cryptographic guarantee); nothing against denial of service, because the operator who cannot read you can still switch you off; nothing against timing or access-pattern side channels; and nothing against a bug in your own code inside the enclave or prompt injection, which remains the job of &lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">guardrails&lt;/a>.&lt;/p>
&lt;p>On Blackwell, NVIDIA announces (2 July 2026) NVLink encryption that extends confidential computing to up to 8 GPUs, non-existent on Hopper and a condition for serving a large model with tensor parallelism. The reference architecture lists H100, H200, RTX Pro 6000 Blackwell Server Edition and B200 in single-GPU passthrough, and H100/H200 in PPCIe mode and B200 for multi-GPU.&lt;/p>
&lt;h3 id="the-overhead-numbers-with-source-and-with-judgement">The overhead numbers, with source and with judgement&lt;/h3>
&lt;p>Here it pays to distrust everybody, papers included. The published spread runs from 0 % to 28 % because the studies do not measure the same thing.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Source (date)&lt;/th>
&lt;th>What it measures&lt;/th>
&lt;th>Result&lt;/th>
&lt;th>Nature&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>NVIDIA, Blackwell blog (Jul. 2026)&lt;/td>
&lt;td>B200 with CC enabled&lt;/td>
&lt;td>−1.0 % to −7.5 % throughput; per-token latency under 8 %; &amp;ldquo;up to 98 % of native performance&amp;rdquo;&lt;/td>
&lt;td>&lt;strong>Vendor claim&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Corvex, HGX B200 with TDX (2026)&lt;/td>
&lt;td>&amp;ldquo;Verified&amp;rdquo; deployment, encrypted NVSwitch&lt;/td>
&lt;td>&amp;ldquo;Near-native performance&amp;rdquo;, with no figures of its own&lt;/td>
&lt;td>&lt;strong>Commercial claim&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>arXiv 2409.03992 (2024)&lt;/td>
&lt;td>H100 GPU only in CC, Llama-3.1 8B/70B&lt;/td>
&lt;td>−0.36 % to 6.85 % throughput; TTFT up to +19 %; overhead → 0 as the model grows&lt;/td>
&lt;td>Independent, partial&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>arXiv 2509.18886 (Sep. 2025)&lt;/td>
&lt;td>CPU TEE (TDX/SGX) and GPU TEE (H100), Llama2 7B/13B/70B&lt;/td>
&lt;td>CPU TEE: under 10 % throughput and 20 % latency. GPU TEE: 4-8 %&lt;/td>
&lt;td>Independent&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>arXiv 2607.19353 (May 2026)&lt;/td>
&lt;td>&lt;strong>Full stack&lt;/strong>: H100 in CC inside a CVM with TDX, Mistral-7B and Qwen3-30B-A3B under load&lt;/td>
&lt;td>TTFT +21.8 % and +27.8 %; throughput −17.7 % and −21.1 %; closed-loop, 11.5-20.2 %&lt;/td>
&lt;td>Independent, &lt;strong>complete&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The reconciliation: the penalty is in the data path, not in the GPU&amp;rsquo;s compute. NVIDIA itself quantifies the bottleneck: the effective bandwidth of the CPU-GPU interconnect in CC mode is limited by the CPU&amp;rsquo;s encryption throughput, &amp;ldquo;around 4 GB/s&amp;rdquo;. From there, three rules. The higher the compute/I-O ratio, the lower the overhead: a 70B with large batches and long sequences amortises the toll almost entirely, and a 7B with small batches and short prompts pays it in full. Adding the CPU TEE brings its own cost, and that is where most of the difference between 4-8 % and 20 % lies. And model loading and cold start are the worst cases, with tens of gigabytes crossing an encrypted bus at 4 GB/s, which makes everything discussed in &lt;a href="https://blog.lo0.es/en/posts/pantry-lift-from-disk-to-hbm-why-the-kitchen-opens-late/">from disk to HBM&lt;/a> and in &lt;a href="https://blog.lo0.es/en/posts/speeding-up-model-cold-start-minutes-to-seconds/">speeding up cold start&lt;/a> more important.&lt;/p>
&lt;p>The recommendation from the authors of the 2026 study, reserving between 15 % and 25 % extra capacity, is the figure I would take to a &lt;a href="https://blog.lo0.es/en/posts/capacity-planning-on-premise-llm-inference/">capacity planning&lt;/a> exercise, and not the vendor&amp;rsquo;s &amp;ldquo;98 % of native performance&amp;rdquo;. Both can be true at once; only one is prudent for sizing.&lt;/p>
&lt;h3 id="when-each-rung-is-worth-it">When each rung is worth it&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Scenario&lt;/th>
&lt;th>Kata&lt;/th>
&lt;th>CoCo (TEE)&lt;/th>
&lt;th>Reason&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Tenants that do not trust each other&lt;/td>
&lt;td>&lt;strong>Yes&lt;/strong>&lt;/td>
&lt;td>Depends&lt;/td>
&lt;td>Kata removes the shared kernel, a real escape vector&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Unaudited third-party code or models&lt;/td>
&lt;td>&lt;strong>Yes&lt;/strong>&lt;/td>
&lt;td>Optional&lt;/td>
&lt;td>A shared kernel is too much surface for someone else&amp;rsquo;s code&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Model intellectual property against the &lt;strong>infrastructure operator&lt;/strong>&lt;/td>
&lt;td>Not enough&lt;/td>
&lt;td>&lt;strong>Yes&lt;/strong>&lt;/td>
&lt;td>The canonical case: third-party hosting, cloud, a partner providing the iron&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Regulated third-party data on infrastructure you do not control&lt;/td>
&lt;td>Not enough&lt;/td>
&lt;td>&lt;strong>Yes&lt;/strong>&lt;/td>
&lt;td>See &lt;a href="https://blog.lo0.es/en/posts/infrastructure-defence-ai-compliance/">defence&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/infrastructure-healthcare-ai-compliance/">healthcare&lt;/a>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Sovereign on-premise, one tenant, trusted operator&lt;/td>
&lt;td>Useful&lt;/td>
&lt;td>&lt;strong>Expensive over-engineering&lt;/strong>&lt;/td>
&lt;td>It protects you from an adversary you do not have&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Low latency with small batches&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>&lt;strong>Bad idea&lt;/strong>&lt;/td>
&lt;td>The worst point on the bus-encryption cost/benefit curve&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The nuance that saves the most money: the two axes are independent. Kata without a TEE is cheap and delivers most of the isolation between tenants; CoCo only adds something if your threat model includes the operator. If you build a sovereign factory with your own iron and your own staff and you answer &amp;ldquo;no&amp;rdquo; to that question, &lt;strong>the TEE is a cost with no return&lt;/strong>.&lt;/p>
&lt;h3 id="operational-traps">Operational traps&lt;/h3>
&lt;p>&lt;strong>MIG and vGPU against confidential mode.&lt;/strong> NVIDIA&amp;rsquo;s GA 1.0.0 reference architecture is blunt: MIG and vGPU are forbidden in CC mode, and mixed-mode nodes are not supported; every GPU on a host goes in CC or none does. The forums show the typical contradiction of a moving area: in February 2026 a moderator states that MIG+CC is not supported and in April a user cites the MIG page suggesting that Hopper and Blackwell already allow it. As of this article the reference architecture overrides the product page: if your multi-tenancy rested on &lt;a href="https://blog.lo0.es/en/posts/sharing-one-gpu-time-slicing-mps-mig/">partitioning the GPU with MIG&lt;/a>, enabling CC sends you back to &amp;ldquo;one whole GPU per tenant&amp;rdquo;.&lt;/p>
&lt;p>&lt;strong>Startup grows non-linearly.&lt;/strong> SESAME'24 breaks down where: SEV memory provisioning adds around one second per 2 GB of guest (SEV pins every page in advance, and an inference VM allocates a lot), the OVMF firmware about three seconds and layer decryption another three to five. Scaling from 0 to 16 instances goes from 16 s with &lt;code>runc&lt;/code> to 190 s: irrelevant for a stable &lt;code>Deployment&lt;/code>, a design change for aggressive autoscaling with &lt;a href="https://blog.lo0.es/en/posts/llm-inference-autoscaling-kubernetes-hpa-keda/">KEDA&lt;/a>.&lt;/p>
&lt;p>&lt;strong>Version coupling is brutal.&lt;/strong> Kata, kata-deploy, GPU Operator, containerd, QEMU with its own patches, guest kernel, NVIDIA driver and Trustee are a matrix that has to be treated as a unit: the reference architecture fixes Kubernetes 1.32 or later, Kata 3.29, GPU Operator 26.3.1 or later, containerd 2.2.2 or later and QEMU 10.1. Add it to the weight the &lt;a href="https://blog.lo0.es/en/posts/llm-inference-operators-kubernetes/">stack&amp;rsquo;s operators&lt;/a> already carry.&lt;/p>
&lt;p>&lt;strong>Attestation fails after a firmware update, and that is normal.&lt;/strong> The RVPS reference values describe a specific boot state. When you patch the SEV-SNP firmware, the microcode or the GPU firmware, the TCB version changes, the evidence stops matching and the KBS does not release keys, so the pods do not start. Since these updates usually respond to a security bulletin, the typical scenario is &amp;ldquo;we applied the critical patch on a Friday and on Saturday inference would not start&amp;rdquo;. The correct procedure reverses the order: update the reference values first with an overlap window, and patch the iron afterwards.&lt;/p>
&lt;p>&lt;strong>The attestation service is a single point of failure by design.&lt;/strong> If the KBS does not answer, no new confidential pod gets its key; those already started survive, but any rescheduling or scaling does not. The same dependency as the SPIRE server and the same treatment: real high availability, its own alert and rehearsed degradation.&lt;/p>
&lt;h2 id="closing-the-series-the-complete-chain">Closing the series: the complete chain&lt;/h2>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/kserve-open-inference-protocol-control-plane/">Article 1/4&lt;/a> set the contract: what the endpoint exposes and what control plane governs it reproducibly. In &lt;a href="https://blog.lo0.es/en/posts/model-chain-of-trust-where-the-bytes-live-registry-oci-artefacts/">2/4&lt;/a> the subject was the provenance of the bytes: which registry they come from, by immutable digest and not by a moving tag. &lt;a href="https://blog.lo0.es/en/posts/model-chain-of-trust-signature-provenance-aibom-why-you-trust-those-bytes/">3/4&lt;/a> added the cryptographic proof of what they are, with signatures, build attestations and an AIBOM verified at admission. This 4/4 closes with who runs them and where: an SVID that proves the serving process&amp;rsquo;s identity and an isolation rung chosen for the adversary you actually have. If one of the four fails, the other three are worth less than they look: an impeccably signed model served by an unidentified process on a machine a third party can dump is still a problem.&lt;/p>
&lt;p>With a limited budget, the order follows cost per unit of risk removed. First, digests everywhere: almost free, and it removes a whole family of substitution attacks. Second, signature verified at admission, low cost and the first thing an auditor asks to see. Third, a declarative contract and control plane, which make everything else auditable. Fourth, workload identity with SPIFFE/SPIRE: expensive to operate, but it is the only thing that turns east-west traffic into something authorisable, and it becomes mandatory as soon as agents with MCP arrive. And last, the TEE, only if the infrastructure operator is in your threat model; Kata without a TEE can jump ahead to fourth place if there are tenants that do not trust each other.&lt;/p>
&lt;h3 id="mapping-the-series-to-ens-isoiec-42001-and-the-eu-ai-act">Mapping the series to ENS, ISO/IEC 42001 and the EU AI Act&lt;/h3>
&lt;p>The detail is in &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>; this table is the summary by link.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Link&lt;/th>
&lt;th>ENS measure (RD 311/2022)&lt;/th>
&lt;th>ISO/IEC 42001 (Annex A)&lt;/th>
&lt;th>EU AI Act&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1/4 Contract and control plane&lt;/td>
&lt;td>&lt;code>op.exp.2&lt;/code>; &lt;code>op.mon.1&lt;/code>&lt;/td>
&lt;td>A.6 life cycle&lt;/td>
&lt;td>Art. 12 (record-keeping); Art. 13 (transparency)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2/4 Registry and distribution by digest&lt;/td>
&lt;td>&lt;code>op.ext.3&lt;/code>&lt;/td>
&lt;td>A.10 third parties&lt;/td>
&lt;td>Art. 11 and Annex IV (technical documentation)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3/4 Signature, attestations and AIBOM&lt;/td>
&lt;td>&lt;code>op.exp.6&lt;/code>; &lt;code>op.ext.3&lt;/code>&lt;/td>
&lt;td>A.6.2 and A.7 data and traceability&lt;/td>
&lt;td>Art. 15(5), data and model poisoning&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4/4-A Workload identity&lt;/td>
&lt;td>&lt;code>op.acc.1/2/5&lt;/code>; &lt;code>op.exp.11&lt;/code>; &lt;code>mp.com.2-3&lt;/code>&lt;/td>
&lt;td>A.9 use of the AI system&lt;/td>
&lt;td>Art. 15(5), unauthorised third parties&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4/4-B Isolation and TEE&lt;/td>
&lt;td>&lt;code>mp.info.3&lt;/code>; &lt;code>op.exp.2&lt;/code>; &lt;code>mp.com.4&lt;/code>&lt;/td>
&lt;td>A.6 life cycle controls&lt;/td>
&lt;td>Art. 15(4) robustness; Art. 15(5)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Art. 15(5) carries the most weight: it requires high-risk systems to be &amp;ldquo;resilient against attempts by unauthorised third parties to alter their use, outputs or performance&amp;rdquo;, and it enumerates data and model poisoning and confidentiality attacks. Links 3 and 4 are its technical implementation. For the management framework, see &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/">AI Act mapping&lt;/a>.&lt;/p>
&lt;h2 id="for-an-inference-factory">For an inference factory&lt;/h2>
&lt;p>&lt;strong>Start workload identity with the gateway↔engine pair and with the agents, not with the whole cluster.&lt;/strong> Registering all forty workloads on day one is the most reliable way to abandon the project: register two entries, set up the Istio policy that requires the gateway&amp;rsquo;s principal and live with it for a couple of weeks to learn what breaks when the certificate rotates. Then the agents with MCP, where static credentials are the entire threat model.&lt;/p>
&lt;p>&lt;strong>Separate the Kata decision from the TEE decision, and take them in that order.&lt;/strong> Kata is a &lt;code>RuntimeClass&lt;/code> change with a second of startup and some memory. CoCo is a redesign: you lose MIG and the GPU&amp;rsquo;s performance counters, you gain ten seconds or more of startup, you pay 15-25 % of capacity and you add two dependencies whose failure prevents workloads from starting. Evaluate the second with a written threat model, not with a slide.&lt;/p>
&lt;p>&lt;strong>If you go to CoCo, rehearse patch day before you need it.&lt;/strong> The failure that will take your inference down will not be an attack, it will be a firmware update that misaligns the RVPS reference values. Write the runbook (update the RVPS with overlap, patch, retire the old value), rehearse it on one node and monitor the attestation success rate as a first-class metric, just as for SPIRE: alert on attestation failures before they turn into 401s at the gateway.&lt;/p>
&lt;p>That closes the series: from the contract to the artefact, from the artefact to its proof, and from the proof to the process and the machine. What lies below is no longer a chain of trust but daily operation: seeing what each process does with &lt;a href="https://blog.lo0.es/en/posts/tetragon-cilium-security-syscalls-kernel/">Tetragon&lt;/a> and measuring whether what is served is still good with &lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">evals&lt;/a>. Trust is established once; vigilance is continuous.&lt;/p>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/kserve-open-inference-protocol-control-plane/">Model chain of trust (1/4): KServe and the Open Inference Protocol&lt;/a> — the API contract that opens the series.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/model-chain-of-trust-where-the-bytes-live-registry-oci-artefacts/">Model chain of trust (2/4): registry and distribution with OCI and ORAS&lt;/a> — why the digest is everything.&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> — the proof that Sigstore selectors turn into a precondition of identity.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/mcp-grows-up-authentication-keycloak/">MCP grows up: authentication with Keycloak&lt;/a> — the user identity to compose with the workload one.&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 that SPIFFE gives a non-transferable credential.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/tetragon-cilium-security-syscalls-kernel/">Tetragon: runtime security&lt;/a> — the vigilance that starts where the chain of trust ends.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/infrastructure-defence-ai-compliance/">Infrastructure and regulatory compliance in defence&lt;/a> — where the TEE stops being over-engineering.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/gpu-cluster-as-multi-tenant-platform/">H100 cluster: multi-tenant platform&lt;/a> — where MIG and confidential mode collide head-on.&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> — the other identity plane, the one for people, and where it overlaps with SPIFFE.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/completing-keycloak-for-mcp-protected-resource/">Completing Keycloak for MCP&lt;/a> — the authorisation of the tools an agent invokes, which neither SPIFFE nor the IdP solves.&lt;/li>
&lt;/ul>
&lt;h2 id="sources">Sources&lt;/h2>
&lt;ul>
&lt;li>CNCF, &lt;em>SPIFFE and SPIRE Projects Graduate from CNCF Incubator&lt;/em> (20 sep. 2022) — &lt;a href="https://www.cncf.io/announcements/2022/09/20/spiffe-and-spire-projects-graduate-from-cloud-native-computing-foundation-incubator/">https://www.cncf.io/announcements/2022/09/20/spiffe-and-spire-projects-graduate-from-cloud-native-computing-foundation-incubator/&lt;/a>&lt;/li>
&lt;li>SPIFFE, &lt;em>SPIFFE Concepts&lt;/em> (SPIFFE ID, trust domain, SVID, Workload API) — &lt;a href="https://spiffe.io/docs/latest/spiffe-about/spiffe-concepts/">https://spiffe.io/docs/latest/spiffe-about/spiffe-concepts/&lt;/a>&lt;/li>
&lt;li>SPIFFE, &lt;em>SPIFFE Federation specification&lt;/em> — &lt;a href="https://spiffe.io/docs/latest/spiffe-specs/spiffe_federation/">https://spiffe.io/docs/latest/spiffe-specs/spiffe_federation/&lt;/a>&lt;/li>
&lt;li>SPIFFE, &lt;em>Scaling SPIRE&lt;/em> (dimensionamiento y punto único de fallo) — &lt;a href="https://spiffe.io/docs/latest/planning/scaling_spire/">https://spiffe.io/docs/latest/planning/scaling_spire/&lt;/a>&lt;/li>
&lt;li>spiffe/spire, &lt;em>Releases&lt;/em> (v1.15.0 de 19 may. 2026; v1.15.1 de 28 may. 2026, parche de &lt;code>azure_imds&lt;/code>) — &lt;a href="https://github.com/spiffe/spire/releases">https://github.com/spiffe/spire/releases&lt;/a>&lt;/li>
&lt;li>spiffe/spire, &lt;em>Kubernetes Workload Attestor plugin&lt;/em> (selectores, kubelet, cgroups, Sigstore) — &lt;a href="https://github.com/spiffe/spire/blob/main/doc/plugin_agent_workloadattestor_k8s.md">https://github.com/spiffe/spire/blob/main/doc/plugin_agent_workloadattestor_k8s.md&lt;/a>&lt;/li>
&lt;li>spiffe/spire, &lt;em>SPIRE Server configuration reference&lt;/em> (&lt;code>default_x509_svid_ttl&lt;/code>, &lt;code>default_jwt_svid_ttl&lt;/code>, &lt;code>ca_ttl&lt;/code>) — &lt;a href="https://github.com/spiffe/spire/blob/main/doc/spire_server.md">https://github.com/spiffe/spire/blob/main/doc/spire_server.md&lt;/a>&lt;/li>
&lt;li>Istio, &lt;em>SPIRE integration&lt;/em> (Envoy SDS, SPIFFE CSI Driver, trust domain) — &lt;a href="https://istio.io/latest/docs/ops/integrations/spire/">https://istio.io/latest/docs/ops/integrations/spire/&lt;/a>&lt;/li>
&lt;li>SPIFFE, &lt;em>OPA Authorization with Envoy and JWT-SVIDs&lt;/em> — &lt;a href="https://spiffe.io/docs/latest/microservices/envoy-jwt-opa/readme/">https://spiffe.io/docs/latest/microservices/envoy-jwt-opa/readme/&lt;/a>&lt;/li>
&lt;li>The New Stack, &lt;em>How Cilium&amp;rsquo;s Mutual Authentication Can Compromise Security&lt;/em> — &lt;a href="https://thenewstack.io/how-ciliums-mutual-authentication-can-compromise-security/">https://thenewstack.io/how-ciliums-mutual-authentication-can-compromise-security/&lt;/a>&lt;/li>
&lt;li>Riptides, &lt;em>Bringing SPIFFE to OAuth for MCP&lt;/em> — &lt;a href="https://riptides.io/blog/bringing-spiffe-to-oauth-for-mcp-secure-identity-for-agentic-workloads/">https://riptides.io/blog/bringing-spiffe-to-oauth-for-mcp-secure-identity-for-agentic-workloads/&lt;/a>&lt;/li>
&lt;li>CNCF, &lt;em>Confidential Containers becomes a CNCF incubating project&lt;/em> (22 jul. 2026) — &lt;a href="https://www.cncf.io/blog/2026/07/22/confidential-containers-becomes-a-cncf-incubating-project/">https://www.cncf.io/blog/2026/07/22/confidential-containers-becomes-a-cncf-incubating-project/&lt;/a>&lt;/li>
&lt;li>Confidential Containers, &lt;em>Attestation with Trustee&lt;/em> (KBS, AS, RVPS, CDH) — &lt;a href="https://confidentialcontainers.org/docs/attestation/">https://confidentialcontainers.org/docs/attestation/&lt;/a>&lt;/li>
&lt;li>Confidential Containers, &lt;em>Deploy Trustee in Kubernetes&lt;/em> (11 feb. 2026) — &lt;a href="https://confidentialcontainers.org/blog/2026/02/11/deploy-trustee-in-kubernetes/">https://confidentialcontainers.org/blog/2026/02/11/deploy-trustee-in-kubernetes/&lt;/a>&lt;/li>
&lt;li>confidential-containers/trustee, &lt;em>Releases&lt;/em> (v0.20.0: TLS 1.3, PQC, multi-GPU ITA, NVSwitch) — &lt;a href="https://github.com/confidential-containers/trustee/releases">https://github.com/confidential-containers/trustee/releases&lt;/a>&lt;/li>
&lt;li>IETF, &lt;em>RFC 9334 — Remote ATtestation procedureS (RATS) Architecture&lt;/em> — &lt;a href="https://www.rfc-editor.org/info/rfc9334/">https://www.rfc-editor.org/info/rfc9334/&lt;/a>&lt;/li>
&lt;li>Kata Containers, &lt;em>Kata Containers 4.0.0 Preview&lt;/em> (28 abr. 2026, &lt;code>runtime-rs&lt;/code> por defecto) — &lt;a href="https://katacontainers.io/blog/release-4-0-0-preview/">https://katacontainers.io/blog/release-4-0-0-preview/&lt;/a>&lt;/li>
&lt;li>kata-containers, &lt;em>Release 3.32.0&lt;/em> — &lt;a href="https://github.com/kata-containers/kata-containers/releases/tag/3.32.0">https://github.com/kata-containers/kata-containers/releases/tag/3.32.0&lt;/a>&lt;/li>
&lt;li>NVIDIA, &lt;em>Deploy with Kata Containers — GPU Operator&lt;/em> — &lt;a href="https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/deploy-kata-containers.html">https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/deploy-kata-containers.html&lt;/a>&lt;/li>
&lt;li>NVIDIA, &lt;em>Deploying Proprietary Models Securely with Confidential Computing on Self-Hosted Kubernetes&lt;/em> (GA 1.0.0; MIG y vGPU prohibidos en CC) — &lt;a href="https://docs.nvidia.com/enterprise-reference-architectures/deploying-proprietary-models-confidential-compute-self-hosted-kubernetes/latest/reference-implementations.html">https://docs.nvidia.com/enterprise-reference-architectures/deploying-proprietary-models-confidential-compute-self-hosted-kubernetes/latest/reference-implementations.html&lt;/a>&lt;/li>
&lt;li>CACM, &lt;em>Creating the First Confidential GPUs&lt;/em> (CPR, bounce buffers, contadores deshabilitados, HBM no cifrada) — &lt;a href="https://cacm.acm.org/practice/creating-the-first-confidential-gpus/">https://cacm.acm.org/practice/creating-the-first-confidential-gpus/&lt;/a>&lt;/li>
&lt;li>NVIDIA, &lt;em>Confidential Computing on H100 GPUs for Secure and Trustworthy AI&lt;/em> (límite de ~4 GB/s, NRAS) — &lt;a href="https://developer.nvidia.com/blog/confidential-computing-on-h100-gpus-for-secure-and-trustworthy-ai/">https://developer.nvidia.com/blog/confidential-computing-on-h100-gpus-for-secure-and-trustworthy-ai/&lt;/a>&lt;/li>
&lt;li>NVIDIA, &lt;em>Hardware-Rooted AI Security That Won&amp;rsquo;t Slow You Down&lt;/em> (2 jul. 2026; Blackwell, NVLink cifrado hasta 8 GPU) — &lt;a href="https://developer.nvidia.com/blog/hardware-rooted-ai-security-that-wont-slow-you-down">https://developer.nvidia.com/blog/hardware-rooted-ai-security-that-wont-slow-you-down&lt;/a>&lt;/li>
&lt;li>Corvex, &lt;em>Confidential Computing Meets NVIDIA HGX B200&lt;/em> (claim comercial) — &lt;a href="https://www.corvex.ai/blog/confidential-computing-meets-nvidia-hgxtm-b200-secure-ai-without-the-performance-trade-off">https://www.corvex.ai/blog/confidential-computing-meets-nvidia-hgxtm-b200-secure-ai-without-the-performance-trade-off&lt;/a>&lt;/li>
&lt;li>arXiv 2409.03992, &lt;em>Confidential Computing on NVIDIA Hopper GPUs: A Performance Benchmark Study&lt;/em> — &lt;a href="https://arxiv.org/html/2409.03992v1">https://arxiv.org/html/2409.03992v1&lt;/a>&lt;/li>
&lt;li>arXiv 2509.18886, &lt;em>Confidential LLM Inference: Performance and Cost Across CPU and GPU TEEs&lt;/em> (23 sep. 2025) — &lt;a href="https://arxiv.org/abs/2509.18886">https://arxiv.org/abs/2509.18886&lt;/a>&lt;/li>
&lt;li>arXiv 2607.19353, &lt;em>Benchmarking Confidential GPU Inference on NVIDIA H100 under Intel TDX&lt;/em> (may. 2026; heurística del 15-25 %) — &lt;a href="https://arxiv.org/html/2607.19353">https://arxiv.org/html/2607.19353&lt;/a>&lt;/li>
&lt;li>Segarra et al., &lt;em>Serverless Confidential Containers: Challenges and Opportunities&lt;/em> (SESAME'24) — &lt;a href="https://carlossegarra.com/assets/papers/sesame24-serverlesscoco.pdf">https://carlossegarra.com/assets/papers/sesame24-serverlesscoco.pdf&lt;/a>&lt;/li>
&lt;li>EU Artificial Intelligence Act, &lt;em>Article 15: Accuracy, Robustness and Cybersecurity&lt;/em> — &lt;a href="https://artificialintelligenceact.eu/article/15/">https://artificialintelligenceact.eu/article/15/&lt;/a>&lt;/li>
&lt;li>EU Artificial Intelligence Act, &lt;em>Article 12: Record-Keeping&lt;/em> — &lt;a href="https://artificialintelligenceact.eu/article/12/">https://artificialintelligenceact.eu/article/12/&lt;/a>&lt;/li>
&lt;/ul></description></item><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></channel></rss>