<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Supply-Chain on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/supply-chain/</link><description>Recent content in Supply-Chain on lo0 — Tech Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Sun, 26 Jul 2026 18:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/en/tags/supply-chain/index.xml" rel="self" type="application/rss+xml"/><item><title>Model chain of trust (3/4): signature, provenance and AIBOM — why you trust those bytes</title><link>https://blog.lo0.es/en/posts/model-chain-of-trust-signature-provenance-aibom-why-you-trust-those-bytes/</link><pubDate>Sun, 26 Jul 2026 18:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/model-chain-of-trust-signature-provenance-aibom-why-you-trust-those-bytes/</guid><description>&lt;p>In the &lt;a href="https://blog.lo0.es/en/posts/kserve-open-inference-protocol-control-plane/">first article&lt;/a> of this series we built the serving control plane; in the &lt;a href="https://blog.lo0.es/en/posts/model-chain-of-trust-where-the-bytes-live-registry-oci-artefacts/">second&lt;/a>, where the bytes come from. At the end of that route there is an &lt;code>initContainer&lt;/code> that runs &lt;code>oras pull&lt;/code> on a 140 GB artefact and leaves it on a volume vLLM is going to read. The uncomfortable question is the one that gives this third article its title: why you trust those bytes.&lt;/p>
&lt;p>The usual answer (&amp;ldquo;because they come from our registry&amp;rdquo;) is the one the supply-chain discipline has spent a decade dismantling: a registry is a warehouse, not an authority; it keeps whatever is pushed to it, and whoever can push can poison. Here we cover the four pieces that turn that sentence into something a machine verifies before the pod starts: signing (Sigstore), model signing (OpenSSF Model Signing), provenance (SLSA and in-toto) and a bill of materials (AIBOM). And the part that almost never gets told: what each one proves.&lt;/p>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Model artefact&lt;/strong>: third-party code with access to your data. &lt;code>pickle&lt;/code> executes code in &lt;code>torch.load&lt;/code>; safetensors removes that class of attack, but says nothing about whether the weights are poisoned.&lt;/li>
&lt;li>&lt;strong>Sigstore&lt;/strong> is the mature piece: &lt;code>cosign&lt;/code> 3.1.1 (9 June 2026), Rekor v2 GA since 10 October 2025. In air-gap, your own PKI or a private Sigstore; the new bundle allows offline verification.&lt;/li>
&lt;li>&lt;strong>OpenSSF Model Signing (OMS)&lt;/strong> v1 (June 2025) signs a manifest of hashes for the whole directory, because a servable model is a directory and not a file; the reference library, &lt;code>model_signing&lt;/code> 1.1.1, is in the OpenSSF &lt;em>sandbox&lt;/em>.&lt;/li>
&lt;li>&lt;strong>SLSA v1.2&lt;/strong> (24 November 2025) adds the &lt;em>Source track&lt;/em>; Build L3 is the realistic target in fine-tuning. in-toto graduated in the CNCF on 23 April 2025.&lt;/li>
&lt;li>&lt;strong>AIBOM&lt;/strong>: SPDX 3.0.1 and CycloneDX 1.7 (ECMA-424 2nd ed.) have the fields; the tooling that fills them in automatically barely exists. It is emitted from the pipeline, not from a scanner.&lt;/li>
&lt;li>&lt;strong>Regulation&lt;/strong>: Art. 53 of the AI Act binds GPAI providers from 2 August 2025, and Regulation (EU) 2026/1744 pushed high risk back to December 2027 and August 2028 without touching GPAI. The CRA requires an SBOM, not an AIBOM, with full application from 11 December 2027.&lt;/li>
&lt;li>&lt;strong>Limit&lt;/strong>: none of this says the model is good. Ultralytics produced valid attestations of artefacts with a cryptominer inside.&lt;/li>
&lt;/ul>
&lt;h2 id="the-analogy-batch-traceability">The analogy: batch traceability&lt;/h2>
&lt;p>A batch of ham arriving at a supermarket&amp;rsquo;s loading bay brings four things that are easy to confuse. A batch number, which identifies it and no other: the &lt;code>sha256&lt;/code>. A seal from the abattoir, which if broken gives away tampering in transit: the signature. A health register saying which abattoir it came from, on what date and under whose inspection: the provenance attestation. And an ingredients and allergens label: the AIBOM. At the bay, the supervisor does not accept the pallet if the seal is broken or the register is missing, and that is admission control.&lt;/p>
&lt;p>The analogy holds down to the detail: the seal travels stuck on the outside and not inside the ham, because it is a detached signature; the register is issued by the abattoir and not by the carrier, so its value depends on the inspection being real. What lets you recall product when a problem shows up is the batch number, not the seal.&lt;/p>
&lt;p>But it serves above all for what it does not promise: a ham with an intact seal and an impeccable label can be off. Traceability answers &amp;ldquo;where did it come from and did anybody touch it&amp;rdquo;; it does not answer &amp;ldquo;is it good&amp;rdquo;. This article is about the first question; the second is the job of &lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">evaluation&lt;/a> and of &lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">guardrails&lt;/a>, and confusing the two is what leads to treating as safe a model you only know the origin of.&lt;/p>
&lt;div class="diagram" style="max-width:820px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 820 280" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Chain of custody of the model artefact: training pipeline, sealing with signature and attestations, OCI registry, verification at admission and loading on the GPU">
&lt;rect x="14" y="70" width="140" height="86" rx="10" fill="none" stroke="currentColor" stroke-width="1.6"/>
&lt;text x="84" y="100" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="700" fill="currentColor">Pipeline&lt;/text>
&lt;text x="84" y="118" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">QLoRA / merge&lt;/text>
&lt;text x="84" y="134" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">ephemeral runner&lt;/text>
&lt;rect x="184" y="52" width="150" height="122" rx="10" fill="none" stroke="currentColor" stroke-width="1.6"/>
&lt;text x="259" y="78" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="700" fill="currentColor">Sealing&lt;/text>
&lt;text x="259" y="98" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">hash manifest&lt;/text>
&lt;text x="259" y="114" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">signature (OMS / cosign)&lt;/text>
&lt;text x="259" y="130" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">SLSA provenance&lt;/text>
&lt;text x="259" y="146" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">AIBOM&lt;/text>
&lt;rect x="364" y="70" width="140" height="86" rx="10" fill="none" stroke="currentColor" stroke-width="1.6"/>
&lt;text x="434" y="100" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="700" fill="currentColor">OCI registry&lt;/text>
&lt;text x="434" y="118" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">artefact + referrers&lt;/text>
&lt;text x="434" y="134" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">immutable by digest&lt;/text>
&lt;rect x="534" y="52" width="146" height="122" rx="10" fill="none" stroke="currentColor" stroke-width="1.6"/>
&lt;text x="607" y="78" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="700" fill="currentColor">Loading bay&lt;/text>
&lt;text x="607" y="98" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">admission control&lt;/text>
&lt;text x="607" y="114" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">verifies signature&lt;/text>
&lt;text x="607" y="130" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">requires provenance&lt;/text>
&lt;text x="607" y="146" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">fail-closed&lt;/text>
&lt;rect x="710" y="70" width="96" height="86" rx="10" fill="none" stroke="currentColor" stroke-width="1.6"/>
&lt;text x="758" y="100" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="700" fill="currentColor">GPU&lt;/text>
&lt;text x="758" y="118" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">re-verifies&lt;/text>
&lt;text x="758" y="134" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="currentColor">before loading&lt;/text>
&lt;path d="M154,113 L182,113" stroke="currentColor" stroke-width="1.6" fill="none"/>
&lt;path d="M334,113 L362,113" stroke="currentColor" stroke-width="1.6" fill="none"/>
&lt;path d="M504,113 L532,113" stroke="currentColor" stroke-width="1.6" fill="none"/>
&lt;path d="M680,113 L708,113" stroke="currentColor" stroke-width="1.6" fill="none"/>
&lt;text x="410" y="222" text-anchor="middle" font-family="sans-serif" font-size="11" fill="currentColor">The seal is applied once, at the source; it is checked twice: at the bay and before loading.&lt;/text>
&lt;text x="410" y="252" text-anchor="middle" font-family="sans-serif" font-size="10.5" font-style="italic" fill="currentColor">Traceability does not say the model is good. It says where it came from and whether anyone touched it.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="the-threat-model-without-generalities">The threat model, without generalities&lt;/h2>
&lt;h3 id="pickle-and-what-safetensors-solves">&lt;code>pickle&lt;/code>, and what safetensors solves&lt;/h3>
&lt;p>The problem with &lt;code>pickle&lt;/code> is not a bug, it is its semantics: an object can define &lt;code>__reduce__&lt;/code> and on deserialisation Python executes whatever that method returns. A &lt;code>torch.save&lt;/code> checkpoint is a program that on loading can open a reverse shell. This is not theoretical: in February 2024 JFrog documented around a hundred malicious models on Hugging Face with a real payload (PyTorch with an injected &lt;code>__reduce__&lt;/code>, Keras abusing the &lt;code>Lambda&lt;/code> layer), and in February 2025 ReversingLabs described &lt;strong>nullifAI&lt;/strong>, two models with deliberately corrupted pickle files that evaded Picklescan by exploiting the fact that the payload runs &lt;em>before&lt;/em> the file fails. The response came late: PyTorch 2.6 flipped the default value of &lt;code>weights_only&lt;/code> to &lt;code>True&lt;/code>, restricting deserialisation to an allowlist of types. It broke a lot of code (there are open issues in &lt;code>nnUNet&lt;/code>, &lt;code>accelerate&lt;/code> and half a dozen more projects) and it is not a formal guarantee: that allowlist has had documented escapes.&lt;/p>
&lt;p>&lt;code>safetensors&lt;/code> does solve the whole class: pure data, a JSON header with offsets plus a blob of tensors, with no execution possible. Trail of Bits&amp;rsquo; audit for EleutherAI and Hugging Face, on 23 May 2023, found no critical flaw leading to arbitrary code execution, and as a bonus the direct memory mapping gives loads on the order of a hundred times faster on CPU, something we already exploited in &lt;a href="https://blog.lo0.es/en/posts/pantry-lift-from-disk-to-hbm-why-the-kitchen-opens-late/">the route from disk to HBM&lt;/a>.&lt;/p>
&lt;p>The nuance that is systematically forgotten: safetensors guarantees that loading the file does not execute code, and guarantees nothing about the numbers inside. A model with a trained backdoor, normal except in the face of a specific trigger, is distributed in safetensors and sails past the scanners and the signature. It is the sealed ham that is off.&lt;/p>
&lt;h3 id="lora-adapters-mutable-tags-and-fridays-model">LoRA adapters, mutable tags and Friday&amp;rsquo;s model&lt;/h3>
&lt;p>An adapter weighs megabytes, gets shared casually and in a &lt;a href="https://blog.lo0.es/en/posts/multi-lora-serving-shared-base-concurrent-adapters-sgmv-kernel/">multi-LoRA&lt;/a> stack dozens are hot-loaded on the same base: it is the cheapest vector for the attacker and the one with the least governance. The 2026 literature is dense on detection: arXiv 2602.15195, revised in April 2026, reports a ROC-AUC of 1.00 classifying adapters by spectral statistics of the attention projections. But with a cool head these are adapters poisoned by the authors themselves with a known method: live research, not deployable defence. The realistic operational answer is still where the adapter came from and who signed it.&lt;/p>
&lt;p>And two threats with no attacker. A tag is mutable: &lt;code>mi-registro/llama-70b:produccion&lt;/code> can point to one digest today and another tomorrow without a line of your manifests changing, and one well-meaning &lt;code>oras push&lt;/code> is enough; the mitigation is identical to the one for images in &lt;a href="https://blog.lo0.es/en/posts/hardening-secrets-sovereign-llm-stack-defence-in-depth/">stack hardening&lt;/a>, pinning by digest and never by tag, with the detail that the digest is what the signature covers. And the classic: an engineer converts a checkpoint on their laptop, runs &lt;code>oras push&lt;/code> with their credentials and on Monday there is an &lt;code>InferenceService&lt;/code> serving it, with no signature, provenance or AIBOM. It is not malicious; it is ungovernable. Here the signature helps because it makes the shortcut unworkable.&lt;/p>
&lt;h3 id="ultralytics-valid-signature-poisoned-artefact">Ultralytics: valid signature, poisoned artefact&lt;/h3>
&lt;p>On 4 and 5 December 2024, versions 8.3.41 and 8.3.42 of &lt;code>ultralytics&lt;/code> (YOLO) were published on PyPI with a cryptominer, through poisoning of the GitHub Actions cache in the publishing workflow; days later a second round arrived (8.3.45 and 8.3.46) with API tokens that had not been rotated when migrating to &lt;em>Trusted Publishing&lt;/em>. All four malicious versions carried valid attestations, because the workflow itself generated them. The signature was correct and so was the provenance; what was compromised was the build environment. As the researcher who uncovered it summed up, an attestation guarantees the relationship between an artefact and a build job plus a commit, not between the developer&amp;rsquo;s intentions and the final artefact.&lt;/p>
&lt;h2 id="sigstore-who-signed-and-how-you-check-without-calling-anybody">Sigstore: who signed, and how you check without calling anybody&lt;/h2>
&lt;p>Sigstore is three pieces and a client. &lt;strong>Fulcio&lt;/strong> is a CA that, in exchange for a valid OIDC token, issues a very short-lived X.509 certificate bound to that identity. &lt;strong>Rekor&lt;/strong> is an append-only transparency log where the signature is recorded with its timestamp. &lt;strong>cosign&lt;/strong> orchestrates it. The result is keyless signing: instead of a long-lived private key to look after, there is an OIDC identity (the CI workflow) and an immutable record that this identity signed that digest.&lt;/p>
&lt;p>Status as of July 2026: cosign 3.1.1, from 9 June 2026. The 3.x branch, since 8 October 2025, enabled the new bundle format by default, along with &lt;code>--trusted-root&lt;/code> and &lt;code>--use-signing-config&lt;/code>; the practical consequence is that the bundle carries the verification material inside and allows verification without calling Rekor or Fulcio. Rekor v2 has been GA since 10 October 2025: a reimplementation over &lt;em>tiles&lt;/em> that makes running your own log far cheaper, because Trillian log server and log signer are switched off and reads are cached in a CDN, at the price of cutting down to two entry types (&lt;code>hashedrekord&lt;/code> and &lt;code>dsse&lt;/code>) and going without a search index. It requires cosign 3.0.1+ or 2.6.0+; v1 continues in parallel and its freeze will be announced a year in advance.&lt;/p>
&lt;h3 id="signing-and-verifying-a-model-artefact-in-oci">Signing and verifying a model artefact in OCI&lt;/h3>
&lt;p>On the artefact from the &lt;a href="https://blog.lo0.es/en/posts/model-chain-of-trust-where-the-bytes-live-registry-oci-artefacts/">second article&lt;/a>, the operation is identical to that for an image: for the registry, a model artefact is an OCI object with its digest.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Resolve the tag to a digest ONCE, in the pipeline. Sign the digest, never the tag.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">oras resolve registro.interno/modelos/llama-70b-fin:v7
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># -&amp;gt; sha256:9f2a4c1e77b0d3a8...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Keyless signature from CI (the OIDC token comes from the runner)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cosign sign --yes registro.interno/modelos/llama-70b-fin@sha256:9f2a4c1e...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verification: exact workflow identity, exact issuer&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cosign verify &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --certificate-identity-regexp &lt;span class="s1">&amp;#39;^https://git.interno/plataforma/modelos/.*@refs/heads/main&amp;#39;&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> --certificate-oidc-issuer &lt;span class="s1">&amp;#39;https://git.interno&amp;#39;&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> registro.interno/modelos/llama-70b-fin@sha256:9f2a4c1e...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The most frequent mistake is omitting &lt;code>--certificate-identity&lt;/code> and &lt;code>--certificate-oidc-issuer&lt;/code>, or setting them so loosely that they accept any identity from the provider. Without those two flags narrowed down, verification checks that somebody signed, not that the right party signed: the difference between &amp;ldquo;it has a seal&amp;rdquo; and &amp;ldquo;it has our abattoir&amp;rsquo;s seal&amp;rdquo;.&lt;/p>
&lt;h3 id="the-air-gap-problem">The air-gap problem&lt;/h3>
&lt;p>Keyless needs a reachable OIDC when signing and, in the classic model, a reachable Rekor when verifying. In a disconnected datacenter, three strategies:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Strategy&lt;/th>
&lt;th>What you gain&lt;/th>
&lt;th>What it costs&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Own key / PKI&lt;/strong> (&lt;code>cosign sign --key&lt;/code>)&lt;/td>
&lt;td>Works without a network; fits HSM and corporate PKI&lt;/td>
&lt;td>Custody and rotation of long-lived keys comes back; without a transparency log there is no detection of unauthorised signing&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Private Sigstore&lt;/strong> (internal Fulcio + Rekor v2 + OIDC)&lt;/td>
&lt;td>Full keyless and real transparency inside the perimeter&lt;/td>
&lt;td>Three more services to operate, with their TUF root and their rotation cycle&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>New bundle + offline verification&lt;/strong>&lt;/td>
&lt;td>Verification without a network using &lt;code>--offline&lt;/code> and a local &lt;code>--trusted-root&lt;/code>&lt;/td>
&lt;td>Transporting and keeping the trust root up to date by sneakernet&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&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">cosign verify --offline&lt;span class="o">=&lt;/span>&lt;span class="nb">true&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> --trusted-root /etc/sigstore/trusted_root.json &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --certificate-identity-regexp &lt;span class="s1">&amp;#39;^https://git.interno/plataforma/modelos/.*&amp;#39;&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> --certificate-oidc-issuer &lt;span class="s1">&amp;#39;https://git.interno&amp;#39;&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> registro.interno/modelos/llama-70b-fin@sha256:9f2a4c1e...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Two warnings. &lt;code>--insecure-ignore-tlog&lt;/code> exists and gets used a lot in air-gap, but its name does not lie: it disables the inclusion check against the transparency log, which is exactly what distinguishes Sigstore from a conventional PKI; if you use it, you are doing certificate signing, not Sigstore. And there is documented friction: cosign issue 4550 describes 3.0.2 trying to reach the TUF CDN despite having a local key and access only to an internal Nexus. Air-gap works, but it is not the happy path.&lt;/p>
&lt;h2 id="signing-a-model-is-not-signing-a-file">Signing a model is not signing a file&lt;/h2>
&lt;p>A servable model is a directory: several safetensors shards, &lt;code>config.json&lt;/code>, tokenizer, perhaps a &lt;code>chat_template.jinja&lt;/code>. Changing &lt;code>config.json&lt;/code> (the context length, the &lt;code>rope_scaling&lt;/code>) alters behaviour without touching a single weight. Signing only the weights leaves the door open.&lt;/p>
&lt;p>The answer is the &lt;strong>OpenSSF Model Signing (OMS)&lt;/strong> specification, published in June 2025 with contributions from Google, HiddenLayer, NVIDIA, Red Hat, Intel, Meta, IBM and Microsoft. Its design is a detached signature, which neither modifies nor repackages the content, over a manifest listing every file by its hash (SHA-256 by default, BLAKE2b as an alternative), with one signature covering the whole manifest. It is deliberately PKI-agnostic: it accepts bare keys, self-signed certificates, corporate PKI or keyless Sigstore. The reference implementation is &lt;code>model_signing&lt;/code>, from sigstore/model-transparency, version 1.1.1 of 10 October 2025; 1.1.0 added PKCS#11 (HSM), private Sigstore instances, BLAKE3 and OpenTelemetry traces.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Sign the whole directory: weights, config and tokenizer&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">model_signing sign /modelos/llama-70b-fin --signature /modelos/llama-70b-fin/model.sig
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verification with a narrowed OIDC identity&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">model_signing verify /modelos/llama-70b-fin &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --signature /modelos/llama-70b-fin/model.sig &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --identity &lt;span class="s1">&amp;#39;https://git.interno/plataforma/modelos/.github/workflows/publicar.yml@refs/heads/main&amp;#39;&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> --identity-provider &lt;span class="s1">&amp;#39;https://git.interno&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The payload is a DSSE envelope with an in-toto statement whose &lt;code>subjects&lt;/code> are the path-digest pairs of each file and whose &lt;code>predicateType&lt;/code> is &lt;code>https://model_signing/signature/v1.0&lt;/code>; support for file shards allows hashing in chunks. Maturity without embellishment: the project is in the OpenSSF sandbox. The specification is solid and has the vendors that matter behind it, with NVIDIA signing models in NGC with it, but it is the first formal step and there are few independent verifiers beyond the reference CLI.&lt;/p>
&lt;h3 id="the-arithmetic-of-hashing-which-turns-out-not-to-be-the-problem">The arithmetic of hashing, which turns out not to be the problem&lt;/h3>
&lt;p>What remains is how long it takes to hash a model of hundreds of gigabytes before every start. Let \( S \) be the total size, \( p \) the threads running in parallel over different shards, \( r_{\text{cpu}} \) the hashing throughput per thread and \( r_{\text{io}} \) that of reading from storage:&lt;/p>
$$t_{\text{hash}} = \frac{S}{\min\left(p \cdot r_{\text{cpu}},\ r_{\text{io}}\right)}$$
&lt;p>With a dense 70B model in bf16, some 140 GB across thirty-odd shards, SHA-256 accelerated by SHA-NI runs at around 1.5-2 GB/s per thread. With eight threads the CPU term is around 14 GB/s, well above what a Gen4 NVMe delivers, on the order of 6 GB/s. The minimum is set by the disk, not by the hash: about 23 seconds. And that is the same throughput the loader is going to consume anyway to move the weights to HBM: if verification happens in the same &lt;code>initContainer&lt;/code> that already reads the artefact, the marginal cost is CPU, not I/O. What is expensive is having put verification in a separate step that reads the disk twice (the rest of the startup budget is in &lt;a href="https://blog.lo0.es/en/posts/speeding-up-model-cold-start-minutes-to-seconds/">the cold start post&lt;/a>). What does have a real cost is incremental verification: if only one shard changes, the manifest allows checking only that one, but only if the distribution layer does incremental pulls. With an OCI artefact and granular layers it works; with a monolithic tarball, it does not.&lt;/p>
&lt;h2 id="provenance-slsa-and-in-toto-over-a-qlora-pipeline">Provenance: SLSA and in-toto over a QLoRA pipeline&lt;/h2>
&lt;p>The signature says &lt;em>who&lt;/em> published; provenance says &lt;em>how it was produced&lt;/em>. The framework is &lt;strong>SLSA&lt;/strong>, whose v1.2 was published on 24 November 2025, backwards compatible with v1.1. Its novelty is the &lt;em>Source track&lt;/em>; the &lt;em>Build Environment track&lt;/em> and the &lt;em>Dependency track&lt;/em> are still in development, and the first is exactly the one that would have helped with Ultralytics.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Level&lt;/th>
&lt;th>What SLSA requires&lt;/th>
&lt;th>What it means in a QLoRA pipeline&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Build L0&lt;/strong>&lt;/td>
&lt;td>Nothing&lt;/td>
&lt;td>The adapter somebody trained on their workstation&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Build L1&lt;/strong>&lt;/td>
&lt;td>Automatically generated provenance: who built it, with what process and with what top-level inputs. It may be unsigned&lt;/td>
&lt;td>The job emits provenance with commit, dataset and hyperparameters. It catches mistakes, not attacks&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Build L2&lt;/strong>&lt;/td>
&lt;td>The above plus a build on a hosted platform that &lt;strong>generates and signs&lt;/strong> the provenance, verifiable by the consumer&lt;/td>
&lt;td>The CI runner signs with its identity. Now there is something an admission controller can verify&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Build L3&lt;/strong>&lt;/td>
&lt;td>The above plus isolation between runs and &lt;strong>signing material inaccessible from user-defined steps&lt;/strong>&lt;/td>
&lt;td>An ephemeral runner per job, with no signing secrets within reach of the training script. This is the realistic target&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>in-toto&lt;/strong> is the format in which all of this is expressed: it graduated in the CNCF on 23 April 2025, which in practice makes it the common substrate of SLSA, of &lt;code>cosign attest&lt;/code> and of OMS itself. Over the flow from the &lt;a href="https://blog.lo0.es/en/posts/qlora-runbook-dataset-to-served-adapter/">QLoRA runbook&lt;/a> and from &lt;a href="https://blog.lo0.es/en/posts/continuous-fine-tuning-production-real-traffic-deployed-adapter/">continuous fine-tuning&lt;/a>, the useful attestation is the one that allows reproduction and auditing:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;_type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;https://in-toto.io/Statement/v1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;subject&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[{&lt;/span> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;adapter-soporte-v7&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;digest&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nt">&amp;#34;sha256&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;e3b0c44298fc1c149afbf4c8...&amp;#34;&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="p">}],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;predicateType&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;https://slsa.dev/provenance/v1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;predicate&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;buildDefinition&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;buildType&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;https://interno/build-types/qlora-finetune@v2&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;externalParameters&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;modeloBase&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;registro.interno/modelos/llama-70b@sha256:9f2a...&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;dataset&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;lakefs://corpus-soporte@commit-4c1f9e&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;hiperparametros&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nt">&amp;#34;rank&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">16&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;alpha&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">32&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;lr&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.0002&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;epochs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;quant&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;nf4&amp;#34;&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;internalParameters&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;gpu&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;4xH100-SXM-80GB&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;cuda&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;12.6&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;torch&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2.8.0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;imagenEntrenamiento&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;registro.interno/ci/qlora@sha256:71ca...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;resolvedDependencies&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span> &lt;span class="nt">&amp;#34;uri&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;git+https://git.interno/plataforma/finetune@a91c3f&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;digest&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nt">&amp;#34;gitCommit&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;a91c3f...&amp;#34;&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;runDetails&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;builder&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;https://git.interno/plataforma/runners/gpu-efimero&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;metadata&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nt">&amp;#34;invocationId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;run-2026-07-19-0041&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;startedOn&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2026-07-19T02:14:33Z&amp;#34;&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The five fields you need and that are almost never complete: hash of the base model, immutable identifier of the dataset (here a lakeFS commit, in line with &lt;a href="https://blog.lo0.es/en/posts/data-versioning-llmops-dvc-lakefs-reproducible-golden-dataset/">data versioning&lt;/a>), hyperparameters, hardware and stack versions, and the code commit. Without the first the chain back to the original model cannot be rebuilt; without the second, the &lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">retrain&lt;/a> loop is left without reproducible evidence. And the obligatory warning: SLSA says nothing about the quality of the dataset; Build L3 guarantees that nobody tampered with the build, not that the corpus was clean.&lt;/p>
&lt;h2 id="aibom-spdx-301-versus-cyclonedx-17">AIBOM: SPDX 3.0.1 versus CycloneDX 1.7&lt;/h2>
&lt;p>If the manifest is the seal and provenance the health register, the AIBOM is the ingredients label. There are two formats, and the honest comparison is less flattering than the vendor literature suggests. &lt;strong>SPDX 3.0&lt;/strong> (April 2024), with patch 3.0.1 on 17 December 2024, reorganised the specification into profiles, two of them relevant here: &lt;code>AI&lt;/code> and &lt;code>Dataset&lt;/code>. The AI profile defines on &lt;code>AIPackage&lt;/code> fields such as &lt;code>typeOfModel&lt;/code>, &lt;code>hyperparameter&lt;/code>, &lt;code>informationAboutTraining&lt;/code>, &lt;code>metric&lt;/code>, &lt;code>safetyRiskAssessment&lt;/code> and &lt;code>limitation&lt;/code>, plus four energy-consumption fields, unique to it, broken down into training, fine-tuning and inference. v3.1 has been in RC since January 2025 without GA, and 3.0 is going through ISO as ISO/IEC DIS 5962 (the standard in force is still 2.2.1). &lt;strong>CycloneDX 1.7&lt;/strong> was published on 21 October 2025 and ratified as ECMA-424, 2nd edition, in December 2025; its ML-BOM rests on the &lt;code>machine-learning-model&lt;/code> type and the &lt;code>modelCard&lt;/code> object.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Criterion&lt;/th>
&lt;th>SPDX 3.0.1&lt;/th>
&lt;th>CycloneDX 1.7&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Formal standardisation&lt;/td>
&lt;td>ISO/IEC DIS 5962 in progress; the standard in force is 2.2.1&lt;/td>
&lt;td>ECMA-424 2nd ed. (December 2025)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AI model&lt;/td>
&lt;td>&lt;code>AI&lt;/code> profile over &lt;code>AIPackage&lt;/code>&lt;/td>
&lt;td>&lt;code>machine-learning-model&lt;/code> + &lt;code>modelCard&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Dataset&lt;/td>
&lt;td>Dedicated &lt;code>Dataset&lt;/code> profile&lt;/td>
&lt;td>&lt;code>data&lt;/code> and &lt;code>modelParameters.datasets&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Energy&lt;/td>
&lt;td>Four broken-down fields (unique)&lt;/td>
&lt;td>Not natively covered&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Intended use and ethics&lt;/td>
&lt;td>&lt;code>limitation&lt;/code>, &lt;code>safetyRiskAssessment&lt;/code>&lt;/td>
&lt;td>&lt;code>considerations&lt;/code> (closer to Model Cards)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Ergonomics&lt;/td>
&lt;td>Rich model, verbose, steep curve&lt;/td>
&lt;td>Compact JSON, more adoption in tooling&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Fit with VEX&lt;/td>
&lt;td>Via the &lt;code>Security&lt;/code> profile&lt;/td>
&lt;td>Native, aligned with CSAF VEX 2.0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;bomFormat&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;CycloneDX&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;specVersion&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;1.7&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;components&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;machine-learning-model&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;bom-ref&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;modelo/adapter-soporte-v7&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;adapter-soporte&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;7.0.0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;hashes&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[{&lt;/span> &lt;span class="nt">&amp;#34;alg&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;SHA-256&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;e3b0c44298fc1c14...&amp;#34;&lt;/span> &lt;span class="p">}],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;licenses&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[{&lt;/span> &lt;span class="nt">&amp;#34;license&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Apache-2.0&amp;#34;&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="p">}],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;modelCard&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;modelParameters&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;task&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;text-generation&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;modelArchitecture&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;llama-70b + LoRA r=16&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;datasets&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[{&lt;/span> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;dataset&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;corpus-soporte&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;contents&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nt">&amp;#34;url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;lakefs://corpus-soporte@commit-4c1f9e&amp;#34;&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="p">}]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;quantitativeAnalysis&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;performanceMetrics&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;exactitud-eval-interna&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;0.83&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;slice&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;soporte-es&amp;#34;&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;considerations&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;useCases&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;asistencia interna a agentes de soporte&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;technicalLimitations&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;no evaluado fuera de castellano&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>What gets filled in on its own is little. Automated tooling gives you name, version, hashes, licences and dependencies of the inference environment: that is a classic SBOM and Trivy or Syft do it unaided. From the pipeline, because no scanner infers it from the bytes, must come dataset, hyperparameters, metrics, base model and energy. And from a person: limitations, intended uses and ethical considerations. The conclusion is uncomfortable: an AIBOM generated after the fact by a tool that looks at the artefact is, for the most part, an empty document with a valid structure. The useful AIBOM is emitted by the training job, the only one that knows those fields, and is signed along with the artefact.&lt;/p>
&lt;h2 id="regulation-what-binds-and-from-exactly-when">Regulation: what binds, and from exactly when&lt;/h2>
&lt;p>The dates call for precision, because they changed in 2026 and much of the documentation in circulation is out of date.&lt;/p>
&lt;h3 id="eu-ai-act">EU AI Act&lt;/h3>
&lt;p>The obligations on providers of general-purpose AI models (GPAI) apply from 2 August 2025. Article 53 requires technical documentation in line with &lt;strong>Annex XI&lt;/strong>, information for downstream providers in line with Annex XII, a copyright policy and a public summary of the training content. Annex XI, section 1, explicitly asks for the specifications of the training process, information on the data used for training, testing and validation, including its type and provenance, the computational resources employed (for example, floating-point operations), the training time and the known or estimated energy consumption. Anyone who had placed a model on the market before that date has until 2 August 2027.&lt;/p>
&lt;p>The 2026 change: &lt;strong>Regulation (EU) 2026/1744&lt;/strong>, the AI &amp;ldquo;Digital Omnibus&amp;rdquo;, was published in the Official Journal on 24 July 2026 and came into force on 27 July 2026. Chapter III for Annex III high risk moves from 2 August 2026 to 2 December 2027; Annex I high risk (AI as a safety component of regulated products), to 2 August 2028; on 2 December 2026 new prohibitions under Article 5 and machine-readable marking for GPAI come into force. The GPAI obligations of Articles 51 to 56 are not delayed.&lt;/p>
&lt;p>The reading for an architect: if you fine-tune a model and place it on the market, Annex XI is enforceable now, and its fields are almost literally those of a SLSA attestation plus an AIBOM. If your case is high risk you have a year and a half more headroom than you thought, but that headroom is for the harmonised standards, not for starting late (&lt;a href="https://blog.lo0.es/en/posts/eu-ai-act-technical-dossier-on-premise-llm-architecture/">detail in the AI Act post&lt;/a>).&lt;/p>
&lt;h3 id="cyber-resilience-act">Cyber Resilience Act&lt;/h3>
&lt;p>The CRA came into force on 10 December 2024; the notification obligations of Article 14 apply from 11 September 2026 and full application from 11 December 2027. What it requires, in Annex I, Part II, point 1, is a software bill of materials in a commonly used, machine-readable format &amp;ldquo;covering at the very least the top-level dependencies&amp;rdquo;.&lt;/p>
&lt;p>Three nuances that get cited badly. &amp;ldquo;At least the top-level dependencies&amp;rdquo; is a very low floor: it does not require the transitive tree. The SBOM does not have to be public, and recital 77 says so expressly: it is internal documentation that the authorities may require. And the CRA names no format; the Commission reserves the right to specify one by implementing act, and the most precise guidance today is BSI TR-03183-2, which accepts CycloneDX 1.6+ or SPDX 3.0.1+. The CRA talks about SBOM and not AIBOM; that the model artefact falls under its umbrella is interpretation, not text.&lt;/p>
&lt;h3 id="fit-with-ens-and-isoiec-42001">Fit with ENS and ISO/IEC 42001&lt;/h3>
&lt;p>None of this is voluntary hygiene. The signature and its verification at admission implement &lt;code>op.exp.6&lt;/code> and &lt;code>op.ext.3&lt;/code> of the ENS (supply chain) and control A.10 of ISO/IEC 42001; the provenance attestation covers &lt;code>op.exp.2&lt;/code> and A.6 (AI system life cycle); the AIBOM with dataset and hyperparameters is &lt;code>op.exp.1&lt;/code> (inventory) and A.7 (data); and the transparency log, &lt;code>op.exp.8&lt;/code>. The evidence an &lt;a href="https://blog.lo0.es/en/posts/iso-42001-aims-on-premise-llm-platform/">ISO/IEC 42001&lt;/a> auditor asks for on the life cycle is, to a large extent, the same JSON the pipeline emits; the control-by-control breakdown is in the &lt;a href="https://blog.lo0.es/en/posts/technical-controls-ens-iso-42001-eu-ai-act-cross-mapping/">technical controls post&lt;/a>.&lt;/p>
&lt;h2 id="verification-in-the-cluster-the-receiving-bay">Verification in the cluster: the receiving bay&lt;/h2>
&lt;p>None of the above is worth anything if nobody checks the seal at the door. &lt;strong>Kyverno&lt;/strong> graduated in the CNCF on 16 March 2026; its 1.17 (February 2026) promoted CEL policies to v1, including &lt;code>ImageValidatingPolicy&lt;/code>, and marked &lt;code>ClusterPolicy&lt;/code> as deprecated, and 1.18 (24 April 2026) polished image verification. The alternative, Sigstore&amp;rsquo;s &lt;strong>policy-controller&lt;/strong>, is at v0.15.1 (26 March 2026), which moved from cosign v2 to v3 and migrated to go-tuf v2, relevant for anyone running a private Sigstore with delegated roles.&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">policies.kyverno.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">ImageValidatingPolicy&lt;/span>&lt;span class="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">modelos-firmados-y-con-provenance&lt;/span>&lt;span class="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">validationActions: [Deny] # fail-closed&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">it does not admit, it does not audit&lt;/span>&lt;span class="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">failurePolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Fail &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># if the webhook fails, it is denied&lt;/span>&lt;span class="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">webhookConfiguration&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">timeoutSeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">20&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># pulling the signature can take a while&lt;/span>&lt;span class="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">matchConstraints&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">resourceRules&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">apiGroups&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;&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">apiVersions&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&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">resources&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;pods&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">operations&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;CREATE&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;UPDATE&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">matchImageReferences&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">glob&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;registro.interno/modelos/*&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">attestors&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">ci-plataforma&lt;/span>&lt;span class="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">cosign&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">keyless&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">identities&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">issuer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;https://git.interno&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">subject&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;https://git.interno/plataforma/modelos/.github/workflows/publicar.yml@refs/heads/main&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ctlog&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">url&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;https://rekor.interno&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">attestations&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">slsaProvenance&lt;/span>&lt;span class="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">intoto&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;https://slsa.dev/provenance/v1&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">validations&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">expression&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> images.containers.map(image,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> verifyImageSignatures(image, [attestors.ci_plataforma])).all(e, e &amp;gt; 0)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">message&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Artefacto de modelo sin firma valida de la CI de plataforma&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">expression&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> images.containers.map(image,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> verifyAttestationSignatures(image, attestations.slsaProvenance,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> [attestors.ci_plataforma])).all(e, e &amp;gt; 0)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">message&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Falta atestacion SLSA de procedencia&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="verify-the-model-not-just-the-image">Verify the model, not just the image&lt;/h3>
&lt;p>That policy covers the serving container&amp;rsquo;s image. The model is downloaded afterwards, in the &lt;code>initContainer&lt;/code>, where the admission controller no longer reaches. The pattern that works is to verify there, before writing to the shared volume:&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">initContainers&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">traer-y-verificar-modelo&lt;/span>&lt;span class="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">registro.interno/plataforma/oras-cosign@sha256:4b7e...&lt;/span>&lt;span class="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">command&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;/bin/sh&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;-ec&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">args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> oras pull registro.interno/modelos/llama-70b-fin@sha256:9f2a... -o /modelos
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> cosign verify --offline=true \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --trusted-root /etc/sigstore/trusted_root.json \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --certificate-oidc-issuer https://git.interno \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --certificate-identity-regexp &amp;#39;^https://git.interno/plataforma/modelos/.*&amp;#39; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> registro.interno/modelos/llama-70b-fin@sha256:9f2a...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> model_signing verify /modelos --signature /modelos/model.sig \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --identity-provider https://git.interno \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --identity &amp;#39;https://git.interno/plataforma/modelos/.github/workflows/publicar.yml@refs/heads/main&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumeMounts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">name: modelos, mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/modelos }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">name: sigstore-root, mountPath: /etc/sigstore, readOnly&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The double verification is not redundant: &lt;code>cosign verify&lt;/code> covers the OCI artefact as it stands in the registry, and &lt;code>model_signing verify&lt;/code> covers the unpacked content as the engine is going to read it. If somebody mounts a &lt;code>ConfigMap&lt;/code> that overwrites &lt;code>config.json&lt;/code> after the pull, the first passes and the second does not.&lt;/p>
&lt;h3 id="fail-closed-versus-fail-open-and-what-it-costs">Fail-closed versus fail-open, and what it costs&lt;/h3>
&lt;p>&lt;code>failurePolicy: Fail&lt;/code> with &lt;code>validationActions: [Deny]&lt;/code> means that if the webhook does not answer nothing starts: the right call from a security standpoint, and exactly what takes a cluster down on a Sunday morning. The defensible posture is fail-closed in the production namespace, with the controller in high availability and its own namespace excluded from its policies so it can be recovered; &lt;code>Audit&lt;/code> during adoption, the same &amp;ldquo;observe first, block later&amp;rdquo; rule we applied with Tetragon in &lt;a href="https://blog.lo0.es/en/posts/hardening-secrets-sovereign-llm-stack-defence-in-depth/">hardening&lt;/a>; and a written exception procedure with an expiry date, because on the day of the 3 a.m. hotfix somebody is going to bypass the policy and it is better that they do it down an audited path.&lt;/p>
&lt;p>On latency, the cost is at startup and not per request: verification happens once per pod. A third-party benchmark from March 2026 on policy-controller v0.15 reports 92 ms at p50 and 184 ms at p99 for internally signed images, with peaks of up to 4 seconds in mass deployments. It is a third-party measurement with no published independent replication, but the order of magnitude is coherent: hundreds of milliseconds against the minutes a pod takes to load 140 GB into HBM.&lt;/p>
&lt;p>As a second net, &lt;strong>Harbor&lt;/strong> stores cosign signatures as referrer artefacts and can prevent the deployment of anything unsigned by project policy. And in &lt;a href="https://blog.lo0.es/en/posts/gitops-inference-stack-flux/">GitOps with Flux&lt;/a>, what closes the circle is always referencing by digest and verifying the OCI artefact&amp;rsquo;s signature at reconciliation: two independent checks at different moments.&lt;/p>
&lt;h2 id="decision-map-what-to-put-in-and-in-what-order">Decision map: what to put in, and in what order&lt;/h2>
&lt;p>By decreasing return on effort:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Pin by digest throughout GitOps.&lt;/strong> An afternoon; without this, the rest is decorative.&lt;/li>
&lt;li>&lt;strong>Image signing with keyless cosign from CI and &lt;code>ImageValidatingPolicy&lt;/code> in &lt;code>Audit&lt;/code>.&lt;/strong> Days. It reveals how much of your cluster is unsigned, which is usually a surprise.&lt;/li>
&lt;li>&lt;strong>Signing the model artefact with &lt;code>model_signing&lt;/code>&lt;/strong> in the job that publishes it. Days. It kills the model uploaded by hand on a Friday.&lt;/li>
&lt;li>&lt;strong>Moving to &lt;code>Deny&lt;/code>&lt;/strong> in the inference namespace, with audited exceptions and the controller in HA.&lt;/li>
&lt;li>&lt;strong>SLSA attestation, targeting Build L2 and then L3.&lt;/strong> Weeks: it requires ephemeral runners and taking the signing material out of the training script&amp;rsquo;s reach.&lt;/li>
&lt;li>&lt;strong>An AIBOM emitted from the pipeline&lt;/strong>, with the five fields only it knows. Weeks, and above all process work.&lt;/li>
&lt;li>&lt;strong>Private Sigstore&lt;/strong>, only with a real air-gap requirement and with the previous six done.&lt;/li>
&lt;/ol>
&lt;p>Between Kyverno and policy-controller: Kyverno if there is or will be a broader policy programme (Pod Security, labels, quotas), for unification and for its graduated status; policy-controller if the organisation is Sigstore-centric and does not want more policy engine than strictly necessary. Between keyless and a key: keyless if CI can reach an OIDC, even an internal one; a key with an HSM in strict air-gap where you are not going to run your own Fulcio.&lt;/p>
&lt;h2 id="operational-traps">Operational traps&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Verifying without narrowing the identity.&lt;/strong> &lt;code>cosign verify&lt;/code> without &lt;code>--certificate-identity&lt;/code> or &lt;code>--certificate-oidc-issuer&lt;/code>, or with a regexp that accepts any repository from the provider, is security theatre. Trap number one and the quietest.&lt;/li>
&lt;li>&lt;strong>Fail-closed bugs in the verifier itself.&lt;/strong> Kyverno issue 16435, opened on 2 July 2026 against 1.18.0, describes two simultaneous regressions in &lt;code>ImageValidatingPolicy&lt;/code> with key and certificate attestors: a null-pointer SIGSEGV and a tlog failure with the message &lt;code>not enough verified log entries from transparency log: 0 &amp;lt; 1&lt;/code>. Both deny legitimate images or bring the controller down. Fixed for 1.19, with the underlying lesson intact: the verifier is a critical component and its regressions are unavailability.&lt;/li>
&lt;li>&lt;strong>The trust root that expires in air-gap.&lt;/strong> It was copied by hand nine months ago, nobody keeps the calendar, and one day verification fails without anyone having touched anything.&lt;/li>
&lt;li>&lt;strong>The AIBOM nobody regenerates.&lt;/strong> It is emitted at the initial deployment, the model is fine-tuned three times and it still describes version one. An out-of-date inventory is worse than none: it creates unjustified confidence.&lt;/li>
&lt;li>&lt;strong>Verifying the image and forgetting the model.&lt;/strong> The most common of all: the vLLM container impeccably signed, and the 140 GB it loads coming from an unverified bucket.&lt;/li>
&lt;/ul>
&lt;h2 id="what-this-does-not-prove">What this does NOT prove&lt;/h2>
&lt;p>&lt;strong>A signature says who published, not that the model is good or safe.&lt;/strong> It is a claim of authorship, not of quality. Ultralytics documents it: four releases with a cryptominer and valid attestations. If the build environment is compromised, the signature faithfully certifies the compromised artefact.&lt;/p>
&lt;p>&lt;strong>A signed model can have a backdoor.&lt;/strong> Nothing in Sigstore, OMS or SLSA examines the weights. A backdoor trained by someone with legitimate access to the pipeline passes through the whole chain without triggering an alarm; the defence is adversarial evaluation and &lt;a href="https://blog.lo0.es/en/posts/canary-blue-green-shadow-llm-models/">canary or shadow&lt;/a>, not cryptography. And SLSA says nothing about dataset quality: a formally perfect pipeline over poisoned data produces a poisoned model with impeccable provenance.&lt;/p>
&lt;p>&lt;strong>The AIBOM is only as good as the process that generates it.&lt;/strong> The fields that matter are not inferred by any tool. And the operational cost is recurring: rotation of keys and TUF roots, verification incidents in the small hours, policy noise during adoption, and the continuous work of maintaining an inventory nobody reads until there is an audit.&lt;/p>
&lt;p>&lt;strong>The maturity split as of July 2026.&lt;/strong> Mature and deployable: Sigstore and cosign over OCI artefacts, Kyverno and policy-controller as admission control, in-toto as a CNCF-graduated format, pinning by digest. Usable with judgement: OMS and &lt;code>model_signing&lt;/code> (a solid specification with strong industry backing, but a sandbox project), SLSA Build L2-L3 in fine-tuning, CycloneDX ML-BOM. Still work in progress: SLSA&amp;rsquo;s &lt;em>Build Environment&lt;/em> and &lt;em>Dependency&lt;/em> tracks (precisely the ones that would cover the Ultralytics scenario), the tooling that fills in an AIBOM automatically, and all backdoor detection in weights, which today is literature and not product.&lt;/p>
&lt;h2 id="for-an-inference-factory">For an inference factory&lt;/h2>
&lt;p>&lt;strong>First: the &lt;code>initContainer&lt;/code> is your receiving bay, and today it probably checks nothing.&lt;/strong> The admission controller looks at the image; the model comes in afterwards through a side door. Adding &lt;code>cosign verify --offline&lt;/code> and &lt;code>model_signing verify&lt;/code> there costs a couple of afternoons, fits inside the cold start budget (23 seconds of hashing over a disk throughput you are already paying for) and closes the largest hole in most deployments.&lt;/p>
&lt;p>&lt;strong>Second: pin by digest today, sign tomorrow.&lt;/strong> If only one thing fits this quarter, it is removing mutable tags from GitOps: it is cheap, it breaks nothing and it turns &amp;ldquo;I trust the registry&amp;rdquo; into &amp;ldquo;I trust these specific bytes&amp;rdquo;. The signature adds the issuer&amp;rsquo;s identity on top, but without a digest it has nothing to hold on to.&lt;/p>
&lt;p>&lt;strong>Third: require the attestation from whoever hands you the model, even if it is the team next door.&lt;/strong> An inference factory consumes what somebody else produces, and that contract should be explicit: a signature from a known CI identity, an attestation with the base model&amp;rsquo;s hash and the dataset&amp;rsquo;s commit, and an AIBOM emitted from the pipeline. Annex XI of the AI Act already requires that, and it is in force, and it is what lets you answer in twenty minutes, rather than in two weeks, when somebody asks what exactly is behind the endpoint.&lt;/p>
&lt;p>The chain now has three links: you know how the model is served, where its bytes come from and why you trust them. The last question remains: what that pod is relative to the rest of the system when it starts with the verified model, and how far you can trust the machine it runs on. The fourth article goes into workload identity with &lt;a href="https://blog.lo0.es/en/posts/model-chain-of-trust-who-serves-the-model-which-machine-you-trust/">SPIFFE/SPIRE and isolation with Confidential Containers&lt;/a>: a verified model, served by an unidentified process on a host you cannot attest, leaves the chain open right at the last link.&lt;/p>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&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> — where the bytes we sign here come from.&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): SPIFFE and Confidential Containers&lt;/a> — who serves the verified model and where it runs.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/hardening-secrets-sovereign-llm-stack-defence-in-depth/">Hardening and secrets in the sovereign LLM stack&lt;/a> — the image supply chain we extend here to the model.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/gitops-inference-stack-flux/">GitOps for the inference stack with Flux&lt;/a> — where pinning by digest and verification at reconciliation take shape.&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 framework that turns these attestations into auditable evidence.&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> — applicable articles and annexes.&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 control-by-control correspondence.&lt;/li>
&lt;/ul>
&lt;h2 id="sources">Sources&lt;/h2>
&lt;ul>
&lt;li>Sigstore, &lt;em>Releases · sigstore/cosign&lt;/em> (v3.1.1, 9-6-2026) — &lt;a href="https://github.com/sigstore/cosign/releases">https://github.com/sigstore/cosign/releases&lt;/a>&lt;/li>
&lt;li>Sigstore Blog, &lt;em>Cosign v3 is now available&lt;/em> (8-10-2025) — &lt;a href="https://blog.sigstore.dev/cosign-3-0-available/">https://blog.sigstore.dev/cosign-3-0-available/&lt;/a>&lt;/li>
&lt;li>Sigstore Blog, &lt;em>Rekor v2 GA — Cheaper to run, simpler to maintain&lt;/em> — &lt;a href="https://blog.sigstore.dev/rekor-v2-ga/">https://blog.sigstore.dev/rekor-v2-ga/&lt;/a>&lt;/li>
&lt;li>Some Natalie, &lt;em>Verifying Cosign signatures offline&lt;/em> — &lt;a href="https://some-natalie.dev/blog/cosign-disconnected/">https://some-natalie.dev/blog/cosign-disconnected/&lt;/a>&lt;/li>
&lt;li>sigstore/cosign, &lt;em>Issue 4550: Cosign 3.0.2 keeps reaching out to the TUF CDN&lt;/em> — &lt;a href="https://github.com/sigstore/cosign/issues/4550">https://github.com/sigstore/cosign/issues/4550&lt;/a>&lt;/li>
&lt;li>OpenSSF, &lt;em>An Introduction to the OpenSSF Model Signing (OMS) Specification&lt;/em> — &lt;a href="https://openssf.org/blog/2025/06/25/an-introduction-to-the-openssf-model-signing-oms-specification/">https://openssf.org/blog/2025/06/25/an-introduction-to-the-openssf-model-signing-oms-specification/&lt;/a>&lt;/li>
&lt;li>Sigstore, &lt;em>model-transparency — README y releases (model_signing 1.1.1)&lt;/em> — &lt;a href="https://github.com/sigstore/model-transparency/blob/main/README.md">https://github.com/sigstore/model-transparency/blob/main/README.md&lt;/a>&lt;/li>
&lt;li>SLSA, &lt;em>Announcing SLSA v1.2&lt;/em> y &lt;em>Build track basics&lt;/em> — &lt;a href="https://slsa.dev/blog/2025/11/announce-slsa-v1.2">https://slsa.dev/blog/2025/11/announce-slsa-v1.2&lt;/a>&lt;/li>
&lt;li>CNCF, &lt;em>Graduation of the in-toto security framework&lt;/em> — &lt;a href="https://www.cncf.io/announcements/2025/04/23/cncf-announces-graduation-of-in-toto-security-framework-enhancing-software-supply-chain-integrity-across-industries/">https://www.cncf.io/announcements/2025/04/23/cncf-announces-graduation-of-in-toto-security-framework-enhancing-software-supply-chain-integrity-across-industries/&lt;/a>&lt;/li>
&lt;li>SPDX, &lt;em>AI profile — SPDX Specification 3.0.1&lt;/em> — &lt;a href="https://spdx.github.io/spdx-spec/v3.0.1/model/AI/AI/">https://spdx.github.io/spdx-spec/v3.0.1/model/AI/AI/&lt;/a>&lt;/li>
&lt;li>CycloneDX, &lt;em>CycloneDX v1.7 released&lt;/em> (y ratificación ECMA-424 2ª ed.) — &lt;a href="https://cyclonedx.org/news/cyclonedx-v1.7-released/">https://cyclonedx.org/news/cyclonedx-v1.7-released/&lt;/a>&lt;/li>
&lt;li>EU Artificial Intelligence Act, &lt;em>Article 53 y Annex XI&lt;/em> — &lt;a href="https://artificialintelligenceact.eu/article/53/">https://artificialintelligenceact.eu/article/53/&lt;/a>&lt;/li>
&lt;li>NicFab, &lt;em>Digital Omnibus on AI: Regulation (EU) 2026/1744 published in the Official Journal&lt;/em> — &lt;a href="https://www.nicfab.eu/en/posts/digital-omnibus-ai-official-journal/">https://www.nicfab.eu/en/posts/digital-omnibus-ai-official-journal/&lt;/a>&lt;/li>
&lt;li>cyberresilienceact.eu, &lt;em>The Cyber Resilience Act Explained: Scope, Classes &amp;amp; Deadlines&lt;/em> — &lt;a href="https://www.cyberresilienceact.eu/explained.html">https://www.cyberresilienceact.eu/explained.html&lt;/a>&lt;/li>
&lt;li>CRA Decoded, &lt;em>CRA SBOM Requirements: What&amp;rsquo;s Mandated, What&amp;rsquo;s Optional&lt;/em> — &lt;a href="https://medium.com/@cra-decoded/cra-sbom-requirements-whats-mandated-what-s-optional-and-what-s-still-unclear-6de9270666de">https://medium.com/@cra-decoded/cra-sbom-requirements-whats-mandated-what-s-optional-and-what-s-still-unclear-6de9270666de&lt;/a>&lt;/li>
&lt;li>Kyverno, &lt;em>Announcing Kyverno Release 1.18&lt;/em> e &lt;em>Issue 16435&lt;/em> — &lt;a href="https://kyverno.io/blog/2026/04/24/announcing-kyverno-release-1.18/">https://kyverno.io/blog/2026/04/24/announcing-kyverno-release-1.18/&lt;/a>&lt;/li>
&lt;li>Safeguard, &lt;em>Sigstore Policy Controller v0.15 Admission Review 2026&lt;/em> — &lt;a href="https://safeguard.sh/resources/blog/sigstore-policy-controller-v0-15-2026">https://safeguard.sh/resources/blog/sigstore-policy-controller-v0-15-2026&lt;/a>&lt;/li>
&lt;li>JFrog, &lt;em>Malicious Hugging Face ML Models with Silent Backdoor&lt;/em> — &lt;a href="https://jfrog.com/blog/data-scientists-targeted-by-malicious-hugging-face-ml-models-with-silent-backdoor/">https://jfrog.com/blog/data-scientists-targeted-by-malicious-hugging-face-ml-models-with-silent-backdoor/&lt;/a>&lt;/li>
&lt;li>ReversingLabs, &lt;em>Novel ML Malware (nullifAI) on Hugging Face&lt;/em> — &lt;a href="https://www.reversinglabs.com/press-releases/reversinglabs-identifies-novel-ml-malware-hosted-on-leading-hugging-face-ai-model-platform">https://www.reversinglabs.com/press-releases/reversinglabs-identifies-novel-ml-malware-hosted-on-leading-hugging-face-ai-model-platform&lt;/a>&lt;/li>
&lt;li>EleutherAI / Hugging Face, &lt;em>Safetensors audited as really safe (Trail of Bits)&lt;/em> — &lt;a href="https://blog.eleuther.ai/safetensors-security-audit/">https://blog.eleuther.ai/safetensors-security-audit/&lt;/a>&lt;/li>
&lt;li>Socket, &lt;em>Ultralytics PyPI Package Compromised Through GitHub Actions Cache Poisoning&lt;/em> — &lt;a href="https://socket.dev/blog/ultralytics-pypi-package-compromised-through-github-actions-cache-poisoning">https://socket.dev/blog/ultralytics-pypi-package-compromised-through-github-actions-cache-poisoning&lt;/a>&lt;/li>
&lt;li>arXiv, &lt;em>Weight Space Detection of Backdoors in LoRA Adapters&lt;/em> — &lt;a href="https://arxiv.org/html/2602.15195v3">https://arxiv.org/html/2602.15195v3&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>The contractor with the master key: isolating AI agents from workstation to cluster</title><link>https://blog.lo0.es/en/posts/isolating-ai-agents-workstation-to-cluster/</link><pubDate>Tue, 09 Jun 2026 16:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/isolating-ai-agents-workstation-to-cluster/</guid><description>&lt;blockquote>
&lt;p>First post of a pair on &lt;strong>AI agent isolation&lt;/strong>. This one fixes the &lt;em>what&lt;/em> and the &lt;em>where&lt;/em>: the complete map of isolation primitives and which domain each belongs to. The &lt;a href="https://blog.lo0.es/en/posts/runbook-caging-ai-agent-bubblewrap-tetragon/">sibling runbook&lt;/a> fixes the &lt;em>how&lt;/em>, with commands: &lt;code>ai-jail&lt;/code> and bubblewrap on the client, Tetragon &lt;code>TracingPolicy&lt;/code> and &lt;code>RuntimeClass&lt;/code> on the cluster. If you are only going to read one, this one gives you the mental model; the other one gives you the files to copy and paste.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>An AI agent that runs code needs access to your filesystem and your tools: compiler, linter, &lt;code>grep&lt;/code>, &lt;code>make&lt;/code>, &lt;code>cargo&lt;/code>, &lt;code>npm&lt;/code>. That is the minimum to be useful. The problem is that alongside that access travels the ability to read &lt;code>~/.aws/credentials&lt;/code>, exfiltrate your SSH keys or fire an &lt;code>rm -rf&lt;/code> outside the project directory. And no malicious model is needed: a compromised dependency in an &lt;code>npm install&lt;/code> is enough, because the well-meaning agent and the poisoned &lt;em>post-install script&lt;/em> &lt;strong>run with the same permissions&lt;/strong>. The answer is not to trust the LLM&amp;rsquo;s good intentions; it is to &lt;strong>isolate in order to bound the blast radius&lt;/strong>. This post walks the five isolation families of 2026, from the process sandbox to the full VM, and sorts them into two columns: what applies on the &lt;strong>client&lt;/strong> (the developer&amp;rsquo;s workstation: bubblewrap, &lt;code>ai-jail&lt;/code>, sandbox-exec, Landlock, the native Claude Code and Codex sandboxes) and what applies on the &lt;strong>cluster&lt;/strong> (where the agent or the inference runs on Kubernetes: namespaces+seccomp, gVisor, Firecracker/Kata microVMs and &lt;strong>eBPF/Tetragon&lt;/strong> as the layer for live observation and enforcement). The thesis: the threat model is the same in both places; the tools are not. Policy extrapolates; the primitive gets rewritten.&lt;/p>
&lt;h2 id="the-analogy-the-contractor-with-the-master-key">The analogy: the contractor with the master key&lt;/h2>
&lt;p>You hire a builder for a renovation. He is competent and acts in good faith. But two things happen that you do not control. The first: he may misread the instruction and knock down the wrong partition wall. The second, worse: his toolbox may have been tampered with before he came through your door — somebody put something inside — and when he opens it in your living room, that something activates.&lt;/p>
&lt;p>Nobody sensible gives him the &lt;strong>master key&lt;/strong> to the whole building. You open the room he works in, you leave him the tools he needs, and you keep the study with the safe and the server room locked. If the renovation goes wrong, by error or by sabotage, the damage stays in that room.&lt;/p>
&lt;p>An AI agent is that contractor. The sandbox is the key policy: &lt;strong>you give it the project room and the tools, not the master key to the system.&lt;/strong> And here is the twist that justifies two posts: the builder works in two different buildings. One is &lt;strong>your flat&lt;/strong>, the developer&amp;rsquo;s workstation, with your credentials, your &lt;code>~/.ssh&lt;/code>, the browser&amp;rsquo;s password vault. The other is &lt;strong>the datacenter&lt;/strong>, the cluster where inference and autonomous agents serve customers, with data from several tenants at once. The key policy is identical in both: least privilege, bound the radius. But the lock on your flat&amp;rsquo;s door is not the same as the one at the datacenter. In the flat you fit a cylinder lock (&lt;code>bubblewrap&lt;/code>). At the datacenter you put a guard watching every door and a separate wing of the building (Tetragon + microVM). Same principle, different hardware. That is &lt;strong>extrapolating the technology&lt;/strong>, not copying it.&lt;/p>
&lt;h2 id="the-threat-model-what-a-runaway-agent-can-do">The threat model: what a runaway agent can do&lt;/h2>
&lt;p>Before choosing a lock it helps to enumerate the burglar. The attack surface of an agent that executes arbitrary bash breaks down into five concrete threats. Not all are defended by the same layer and, an uncomfortable fact that the security documentation itself acknowledges, &lt;strong>no single layer covers them all&lt;/strong>.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Threat&lt;/th>
&lt;th>What the agent does&lt;/th>
&lt;th>Minimum layer that cuts it&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Filesystem out of scope&lt;/strong>&lt;/td>
&lt;td>Reads &lt;code>.env&lt;/code>, &lt;code>~/.ssh/id_rsa&lt;/code>, system secrets; modifies sources outside the project&lt;/td>
&lt;td>Process sandbox (path allowlist)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Arbitrary network egress&lt;/strong>&lt;/td>
&lt;td>Exfiltrates data, receives instructions from a remote C2, calls unauthorised APIs&lt;/td>
&lt;td>Network blocking / NetworkPolicy / microVM&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Kernel syscall surface&lt;/strong>&lt;/td>
&lt;td>A kernel exploit from inside the container escalates to the host (shared kernel)&lt;/td>
&lt;td>gVisor or microVM (dedicated kernel)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Cross-tenant leakage&lt;/strong>&lt;/td>
&lt;td>One customer&amp;rsquo;s workload reads another&amp;rsquo;s data on a multi-tenant platform&lt;/td>
&lt;td>microVM (de facto standard)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Secret exfiltration&lt;/strong>&lt;/td>
&lt;td>Extracts tokens and environment variables via &lt;code>/proc&lt;/code> or the environment&lt;/td>
&lt;td>&lt;code>--clearenv&lt;/code> / tmpfs &lt;code>$HOME&lt;/code> / secrets outside the pod&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>There is a sixth threat that &lt;strong>no sandbox solves&lt;/strong>: &lt;em>prompt injection&lt;/em>. If an attacker manages to slip instructions into the agent&amp;rsquo;s context — a poisoned comment in the code, a malicious file the agent reads, an adversarial response from a tool — the agent will execute those instructions with whatever permissions the sandbox grants it. Isolation &lt;strong>shrinks the impact radius&lt;/strong> of a successful injection; it does not prevent the injection. That is why the sandbox is a layer, not the solution: on top go input validation, tool-call allowlists and output auditing. The sentence to internalise: &lt;em>isolation does not make the agent trustworthy; it bounds what an untrustworthy agent can break.&lt;/em>&lt;/p>
&lt;h2 id="two-domains-one-policy">Two domains, one policy&lt;/h2>
&lt;p>The builder works in two buildings. The distribution of tools reads better in two columns:&lt;/p>
&lt;div class="diagram" style="max-width:820px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 820 430" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Two-column map: client domain (workstation) and cluster domain, with their isolation layers and the shared policy bridge">
&lt;defs>&lt;marker id="am" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="410" y="26" text-anchor="middle" font-family="sans-serif" font-size="14" font-weight="700" fill="currentColor">Same least-privilege policy · different primitive&lt;/text>
&lt;!-- client column -->
&lt;rect x="24" y="46" width="360" height="350" rx="10" fill="#eef4fb" stroke="#1f5fa8" stroke-width="1.6"/>
&lt;text x="204" y="72" text-anchor="middle" font-family="sans-serif" font-size="14" font-weight="700" fill="#0d3a66">CLIENT · dev workstation&lt;/text>
&lt;text x="204" y="90" text-anchor="middle" font-family="sans-serif" font-size="11" fill="#0d3a66">one process, one user, personal data&lt;/text>
&lt;rect x="48" y="106" width="312" height="50" rx="6" fill="#d4ecff" stroke="#1f5fa8" stroke-width="1.2"/>
&lt;text x="204" y="126" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="#0d3a66">Process sandbox&lt;/text>
&lt;text x="204" y="143" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#0d3a66">bubblewrap (Linux) · sandbox-exec (macOS)&lt;/text>
&lt;rect x="48" y="166" width="312" height="42" rx="6" fill="#dff0ff" stroke="#1f5fa8" stroke-width="1.1"/>
&lt;text x="204" y="183" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="#0d3a66">Landlock (LSM, 2nd VFS barrier)&lt;/text>
&lt;text x="204" y="199" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#0d3a66">defence in depth, kernel ≥5.13&lt;/text>
&lt;rect x="48" y="218" width="312" height="42" rx="6" fill="#eaf4ff" stroke="#1f5fa8" stroke-width="1.1"/>
&lt;text x="204" y="235" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="#0d3a66">Dev container (optional)&lt;/text>
&lt;text x="204" y="251" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#0d3a66">reproducibility + reset, shared kernel&lt;/text>
&lt;text x="204" y="284" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="600" fill="#0d3a66">What wraps it:&lt;/text>
&lt;text x="204" y="304" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="#0d3a66">ai-jail · Claude Code /sandbox&lt;/text>
&lt;text x="204" y="320" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="#0d3a66">Codex --sandbox · Cursor /worktree&lt;/text>
&lt;text x="204" y="352" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="#0d3a66">Safety net: git remote with no push&lt;/text>
&lt;text x="204" y="376" text-anchor="middle" font-family="sans-serif" font-size="10.5" font-style="italic" fill="#555">startup ~0 ms · no daemon&lt;/text>
&lt;!-- cluster column -->
&lt;rect x="436" y="46" width="360" height="350" rx="10" fill="#f3eefb" stroke="#5a2db0" stroke-width="1.6"/>
&lt;text x="616" y="72" text-anchor="middle" font-family="sans-serif" font-size="14" font-weight="700" fill="#42208a">CLUSTER · inference / agents in prod&lt;/text>
&lt;text x="616" y="90" text-anchor="middle" font-family="sans-serif" font-size="11" fill="#42208a">multi-pod, multi-tenant, RKE2&lt;/text>
&lt;rect x="460" y="106" width="312" height="50" rx="6" fill="#e6d9f2" stroke="#5a2db0" stroke-width="1.2"/>
&lt;text x="616" y="126" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="#42208a">Pod baseline&lt;/text>
&lt;text x="616" y="143" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#42208a">namespaces + seccomp + cgroups + NetworkPolicy&lt;/text>
&lt;rect x="460" y="166" width="312" height="42" rx="6" fill="#ede1f7" stroke="#5a2db0" stroke-width="1.1"/>
&lt;text x="616" y="183" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="#42208a">gVisor (runsc) — user-space kernel&lt;/text>
&lt;text x="616" y="199" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#42208a">cuts the syscall surface to the host&lt;/text>
&lt;rect x="460" y="218" width="312" height="42" rx="6" fill="#f0e6fb" stroke="#5a2db0" stroke-width="1.1"/>
&lt;text x="616" y="235" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="#42208a">Firecracker / Kata microVM — own kernel&lt;/text>
&lt;text x="616" y="251" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#42208a">isolation by construction for untrusted code&lt;/text>
&lt;rect x="460" y="270" width="312" height="62" rx="6" fill="#fde9d6" stroke="#a85a00" stroke-width="1.6"/>
&lt;text x="616" y="290" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="700" fill="#8a4a00">eBPF / Tetragon (what we already have)&lt;/text>
&lt;text x="616" y="307" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#8a4a00">watches every exec · connect · open in the kernel&lt;/text>
&lt;text x="616" y="322" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#8a4a00">and kills (Sigkill) whatever strays from the script&lt;/text>
&lt;text x="616" y="356" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="#42208a">Safety net: GitOps + PR review&lt;/text>
&lt;text x="616" y="376" text-anchor="middle" font-family="sans-serif" font-size="10.5" font-style="italic" fill="#555">startup &amp;lt;1 s (microVM) · runtime layer always on&lt;/text>
&lt;!-- bridge -->
&lt;path d="M384,221 L436,221" stroke="#666" stroke-width="1.6" fill="none" marker-end="url(#am)"/>
&lt;path d="M436,241 L384,241" stroke="#666" stroke-width="1.6" fill="none" marker-end="url(#am)"/>
&lt;text x="410" y="415" text-anchor="middle" font-family="sans-serif" font-size="11" font-style="italic" fill="#555">extrapolate: each client control has its cluster analogue (equivalence table in the runbook)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="the-client-isolating-the-agent-on-the-workstation">The client: isolating the agent on the workstation&lt;/h2>
&lt;p>Here the agent is a coding assistant — Claude Code, Codex, OpenCode, Cursor — that a developer launches on their machine. One process, one user, and right next to it the juiciest assets that exist: &lt;code>~/.aws/credentials&lt;/code>, &lt;code>~/.ssh&lt;/code>, &lt;code>~/.gnupg&lt;/code>, the browser&amp;rsquo;s password store. The matching tier is the lightest one: the &lt;strong>process sandbox&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>bubblewrap (Linux) and sandbox-exec (macOS).&lt;/strong> &lt;code>bubblewrap&lt;/code> (&lt;code>bwrap&lt;/code>) is the same sandbox Flatpak uses to isolate each desktop app: a ~50 KB binary, ~4,000 lines of C, maintained by the GNOME team and, the key property, it runs &lt;strong>without root&lt;/strong> via &lt;code>CLONE_NEWUSER&lt;/code>, creating namespaces with no elevated privileges. It mounts &lt;code>$HOME&lt;/code> as an ephemeral tmpfs and only exposes the project directory, writable; the rest of the system becomes invisible. On macOS the equivalent is &lt;code>sandbox-exec&lt;/code> with SBPL profiles: a legacy Apple API, officially deprecated and with no public replacement, but working today. Parity between the two is not exact — on macOS the GPU (Metal) and the display (Cocoa) are system services and &lt;code>sandbox-exec&lt;/code> does not restrict them — but both protect what matters: access to the sensitive areas of the filesystem.&lt;/p>
&lt;p>&lt;strong>Landlock as a second barrier.&lt;/strong> &lt;code>bubblewrap&lt;/code> isolates via &lt;em>namespaces and mounts&lt;/em>; Landlock, a Linux Security Module available since kernel 5.13, restricts access at the &lt;strong>VFS&lt;/strong> level, independently of namespaces. It does not replace &lt;code>bwrap&lt;/code>: it complements it. It closes vectors that mount-based isolation does not cover on its own (escape routes via &lt;code>/proc&lt;/code>, symlink tricks inside allowed mounts) and acts as a safety net should the namespace machinery have a bug. It is defence in depth inside the client itself, and it degrades cleanly to a no-op on kernels that do not support it.&lt;/p>
&lt;p>&lt;strong>Dev containers, when reproducibility is needed.&lt;/strong> A dev container (&lt;code>devcontainer.json&lt;/code>, what Codespaces and Cursor use) is a Docker container with a configuration layer on top. It gives reasonable filesystem isolation and easy &lt;em>reset&lt;/em> (destroy and recreate), but it &lt;strong>shares the host kernel&lt;/strong>, the same limitation as any Docker, and it tends to be long-lived, accumulating state. For an agent running your own team&amp;rsquo;s code on your own machine it is a good repeatability story; it is not, on its own, the isolation layer for untrusted code.&lt;/p>
&lt;p>&lt;strong>What wraps all of this.&lt;/strong> A hand-written bash script works, but it does not scale to a team. The tools that package it:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>ai-jail&lt;/code>&lt;/strong> (Rust, GPL-3.0): wraps &lt;code>bwrap&lt;/code>/&lt;code>sandbox-exec&lt;/code> with per-project config in a &lt;code>.ai-jail&lt;/code> file (TOML, committable to the repo, so the whole team inherits the same policy), auto-detection of GPU/Docker/display, a &lt;code>--lockdown&lt;/code> mode (project read-only, network cut with &lt;code>--unshare-net&lt;/code>, &lt;code>--clearenv&lt;/code>), &lt;code>--dry-run&lt;/code> for auditing, and &lt;code>--bootstrap&lt;/code> to generate each agent&amp;rsquo;s permission allowlists. It is tool-agnostic: the same binary serves Claude, Codex, OpenCode or Crush. It also applies Landlock automatically on 5.13+ kernels as defence in depth.&lt;/li>
&lt;li>&lt;strong>The Claude Code &lt;code>/sandbox&lt;/code>&lt;/strong>: since October 2025 Claude Code ships its own sandbox that uses, exactly, &lt;code>bubblewrap&lt;/code> on Linux and &lt;code>sandbox-exec&lt;/code> on macOS. Its &lt;em>Sandboxed Bash&lt;/em> isolates shell commands, but &lt;strong>not&lt;/strong> the file tools, the MCP servers or the hooks, which run with the process&amp;rsquo;s full permissions unless you enable the beta &lt;code>sandbox-runtime&lt;/code> package, which wraps the whole process. There is one nuance worth knowing: if a command fails because of a restriction, the agent can retry with &lt;code>dangerouslyDisableSandbox&lt;/code> — it is &lt;em>opt-out&lt;/em>, not &lt;em>opt-in&lt;/em>.&lt;/li>
&lt;li>&lt;strong>Codex CLI&lt;/strong>: three modes via &lt;code>--sandbox&lt;/code> (&lt;code>read-only&lt;/code>, &lt;code>workspace-write&lt;/code>, &lt;code>danger-full-access&lt;/code>); the recommended default is &lt;code>workspace-write&lt;/code>. The philosophy is deliberate: Codex does not provide the isolation, it delegates it to the environment that wraps it. &lt;code>danger-full-access&lt;/code> only makes sense &lt;strong>inside&lt;/strong> a microVM.&lt;/li>
&lt;li>&lt;strong>Cursor&lt;/strong>: its cloud agents run in isolated VMs; &lt;code>/worktree&lt;/code> creates a single-use isolated worktree per task, and &lt;code>/best-of-n&lt;/code> launches several attempts in parallel in separate worktrees.&lt;/li>
&lt;/ul>
&lt;h2 id="the-cluster-isolating-the-agent-in-production">The cluster: isolating the agent in production&lt;/h2>
&lt;p>The second building is the datacenter. Here the &amp;ldquo;agent&amp;rdquo; can be an autonomous agent running with no human in front of it, or the inference service itself executing generated code, or a multi-tenant workload where one customer&amp;rsquo;s pod must not touch another&amp;rsquo;s data. The process is no longer one: they are pods in a Kubernetes cluster (RKE2/RKE3 on a typical sovereign platform). The primitives change in nature.&lt;/p>
&lt;p>&lt;strong>The pod baseline.&lt;/strong> First of all, the standard kit: Linux namespaces, &lt;code>seccomp&lt;/code> (&lt;code>RuntimeDefault&lt;/code>) to trim the syscall surface, cgroups for resource limits, an unprivileged &lt;code>securityContext&lt;/code> (&lt;code>runAsNonRoot&lt;/code>, &lt;code>readOnlyRootFilesystem&lt;/code>, dropping all capabilities) and &lt;strong>NetworkPolicy&lt;/strong> to cut egress. It is the cluster equivalent of the process sandbox allowlist. Necessary, but it shares the kernel with the host: insufficient for genuinely untrusted code.&lt;/p>
&lt;p>&lt;strong>gVisor (&lt;code>runsc&lt;/code>).&lt;/strong> Google&amp;rsquo;s user-space kernel: it intercepts the workload&amp;rsquo;s syscalls before they reach the host kernel and serves them inside a Linux kernel reimplemented in Go (the &lt;em>Sentry&lt;/em>). The surface exposed to host kernel vulnerabilities drops sharply, while keeping fast startup and a low footprint. It is the middle ground when the risk of a kernel escape is real but the overhead of a microVM is not acceptable.&lt;/p>
&lt;p>&lt;strong>Firecracker / Kata microVMs.&lt;/strong> The de facto standard for untrusted code in 2026. Firecracker (AWS&amp;rsquo;s VMM written in Rust, on top of KVM) gives each sandbox a &lt;strong>dedicated Linux kernel&lt;/strong>: a kernel exploit inside the microVM does not reach the host &lt;em>by construction&lt;/em>. It is what sits underneath Vercel Sandbox (GA January 2026) and E2B. On Kubernetes, &lt;strong>Kata Containers&lt;/strong> brings that model to a &lt;code>RuntimeClass&lt;/code>: you mark the untrusted agent&amp;rsquo;s pod with &lt;code>runtimeClassName: kata&lt;/code> and it runs in its own microVM instead of sharing the node&amp;rsquo;s kernel. For multi-tenant with generated code, this is the baseline, not the luxury.&lt;/p>
&lt;p>&lt;strong>eBPF / Tetragon: the layer we already have.&lt;/strong> Here is the piece that separates a platform with runtime observability from one that only trusts its configuration. The previous layers are &lt;em>static&lt;/em>: they define what the pod can do before it starts. &lt;strong>Tetragon&lt;/strong>, Cilium&amp;rsquo;s runtime security component, based on eBPF, is &lt;em>dynamic&lt;/em>: it observes, in the kernel and at minimal cost, &lt;strong>every&lt;/strong> process execution, &lt;strong>every&lt;/strong> network connection and &lt;strong>every&lt;/strong> file open in every pod, and it can act inline. It does not replace the sandbox; it watches it from inside the kernel. Where &lt;code>bubblewrap&lt;/code> on the client blocks &lt;code>curl&lt;/code> with a command blocklist, Tetragon on the cluster hooks &lt;code>tcp_connect&lt;/code> in the kernel and, if the destination is not allowed, &lt;strong>kills the process with &lt;code>Sigkill&lt;/code>&lt;/strong> before the packet leaves. Where the client hides &lt;code>~/.ssh&lt;/code> behind a tmpfs, Tetragon hooks &lt;code>security_file_open&lt;/code> and reports, or kills, any attempt to read a sensitive mounted path. It is the guard patrolling the corridors while the microVMs are the walls. And it is exactly the kind of control that materialises the ENS monitoring and traceability measures (&lt;code>op.mon&lt;/code>, &lt;code>op.exp&lt;/code>) without instrumenting the application: visibility lives in the kernel, not in the agent&amp;rsquo;s code.&lt;/p>
&lt;h2 id="the-landscape-table">The landscape table&lt;/h2>
&lt;p>The five families, their relative isolation strength, their startup cost and the domain where they live:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tier&lt;/th>
&lt;th>Primitive&lt;/th>
&lt;th style="text-align:center">Isolation&lt;/th>
&lt;th style="text-align:center">Startup&lt;/th>
&lt;th>Natural domain&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Process sandbox&lt;/td>
&lt;td>Seatbelt · bubblewrap&lt;/td>
&lt;td style="text-align:center">Baseline&lt;/td>
&lt;td style="text-align:center">~0 ms&lt;/td>
&lt;td>&lt;strong>Client&lt;/strong> (Claude Code default)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Dev container&lt;/td>
&lt;td>Docker + seccomp&lt;/td>
&lt;td style="text-align:center">Moderate&lt;/td>
&lt;td style="text-align:center">seconds&lt;/td>
&lt;td>Client / cluster (repeatability)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>User-space kernel&lt;/td>
&lt;td>gVisor (&lt;code>runsc&lt;/code>)&lt;/td>
&lt;td style="text-align:center">Strong&lt;/td>
&lt;td style="text-align:center">ms&lt;/td>
&lt;td>&lt;strong>Cluster&lt;/strong> (medium multi-tenant)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>microVM&lt;/td>
&lt;td>Firecracker · Kata&lt;/td>
&lt;td style="text-align:center">Strongest (practical)&lt;/td>
&lt;td style="text-align:center">&amp;lt;1 s&lt;/td>
&lt;td>&lt;strong>Cluster&lt;/strong> (untrusted code)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Full VM&lt;/td>
&lt;td>KVM · EC2&lt;/td>
&lt;td style="text-align:center">Maximum&lt;/td>
&lt;td style="text-align:center">30 s+&lt;/td>
&lt;td>Cluster (external boundary, compliance)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;em>Runtime enforcement&lt;/em>&lt;/td>
&lt;td>&lt;strong>eBPF · Tetragon&lt;/strong>&lt;/td>
&lt;td style="text-align:center">&lt;em>Cross-cutting&lt;/em>&lt;/td>
&lt;td style="text-align:center">&lt;em>always on&lt;/em>&lt;/td>
&lt;td>&lt;strong>Cluster&lt;/strong> (watches+kills on top of any tier)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Tetragon gets its own row on purpose: it is not a rung on the ladder, it is a &lt;strong>cross-cutting layer&lt;/strong> that operates &lt;em>on top of&lt;/em> any of the others. It stacks with all of them.&lt;/p>
&lt;p>A numerical note on why the &amp;ldquo;startup&amp;rdquo; column decides as much as the &amp;ldquo;isolation&amp;rdquo; column. A full VM wins on raw isolation but takes tens of seconds to provision; for an agent that needs a fresh environment &lt;em>per request or per session&lt;/em>, that cost is prohibitive. A Firecracker microVM starts in &lt;strong>under 1 second&lt;/strong> and a process sandbox in &lt;strong>~0 ms&lt;/strong>. That is why the dominant pattern in 2026 is not &amp;ldquo;the most isolated VM&amp;rdquo;, but &lt;strong>a full VM as the external boundary plus a microVM as the per-request execution unit inside&lt;/strong>, the architecture of Vercel, AWS Lambda and E2B. On the client the arithmetic is the opposite: the developer launches the agent dozens of times a day interactively, and a startup measured in seconds would break the flow; hence the process sandbox, with its microsecond overhead, is the correct default.&lt;/p>
&lt;h2 id="extrapolate-do-not-copy">Extrapolate, do not copy&lt;/h2>
&lt;p>The thesis of this pair of posts fits in one sentence: &lt;strong>the threat model is invariant across domains; the primitive that implements it is not.&lt;/strong> The client and the cluster defend exactly the same five vectors — filesystem, network, kernel, multi-tenant, secrets — but with toolboxes that do not overlap. Every control has its twin on the other side:&lt;/p>
&lt;ul>
&lt;li>&lt;code>$HOME&lt;/code> as an ephemeral tmpfs (client) ↔ &lt;code>readOnlyRootFilesystem&lt;/code> + &lt;code>emptyDir&lt;/code> (cluster).&lt;/li>
&lt;li>A &lt;code>curl&lt;/code>/&lt;code>wget&lt;/code> blocklist in &lt;code>bwrap&lt;/code> (client) ↔ a &lt;code>TracingPolicy&lt;/code> on &lt;code>tcp_connect&lt;/code> in Tetragon plus NetworkPolicy (cluster).&lt;/li>
&lt;li>&lt;code>--unshare-net&lt;/code> in lockdown (client) ↔ default-deny NetworkPolicy (cluster).&lt;/li>
&lt;li>No &lt;em>escape hatch&lt;/em>, the process lives inside &lt;code>bwrap&lt;/code> (client) ↔ no &lt;code>privileged&lt;/code>, no &lt;code>hostPath&lt;/code>, &lt;code>RuntimeClass&lt;/code> kata (cluster).&lt;/li>
&lt;li>&lt;code>~/.ssh&lt;/code> and &lt;code>~/.aws&lt;/code> never mounted (client) ↔ secrets outside the pod plus Tetragon watching &lt;code>security_file_open&lt;/code> (cluster).&lt;/li>
&lt;/ul>
&lt;p>The sibling runbook turns each of these equivalences into concrete files. What matters here is the method: when someone shows you an agent sandbox, be it the Claude Code &lt;code>/sandbox&lt;/code> on a laptop or a microVM on a PaaS, the useful question is not &amp;ldquo;which tool does it use?&amp;rdquo;, but &amp;ldquo;which of the five vectors does it close, and which does it leave open?&amp;rdquo;. The tool gets replaced; the threat map stays.&lt;/p>
&lt;h2 id="what-no-sandbox-solves">What no sandbox solves&lt;/h2>
&lt;p>Three limits that Anthropic&amp;rsquo;s own documentation states, and which are worth keeping in view so as not to sell smoke:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Egress remains a risk&lt;/strong> in any sandbox that allows outbound connections. If the agent can open a connection, it can exfiltrate. That is why the client lockdown &lt;em>cuts&lt;/em> the network and the cluster uses default-deny NetworkPolicy plus Tetragon: you do not trust &amp;ldquo;filtering well&amp;rdquo;, you trust &amp;ldquo;not letting anything out&amp;rdquo;.&lt;/li>
&lt;li>&lt;strong>Code modification remains possible&lt;/strong> in any sandbox with the project directory mounted writable. The remedy is not a sandbox-technical one, it is &lt;strong>git&lt;/strong>: with the remote untouched and no &lt;code>push&lt;/code> permission, the worst case is corrupting the local working copy — &lt;code>git checkout .&lt;/code> and start again. The damage never reaches the remote.&lt;/li>
&lt;li>&lt;strong>No sandbox prevents a compromised prompt from reaching the API.&lt;/strong> Isolation bounds the impact of an injection; it does not prevent it. The complementary defences — input validation, tool-call allowlists, output auditing — are mandatory &lt;em>alongside&lt;/em> isolation, not instead of it.&lt;/li>
&lt;/ol>
&lt;p>The operational conclusion: &lt;strong>isolation shrinks the blast radius; defence in depth is what closes the circle.&lt;/strong> A process sandbox for trusted code on a known machine is appropriate and practically free. For an agent acting on user prompts, executing generated code or running multi-tenant, the acceptable minimum in 2026 is a microVM, with Tetragon watching from above. Pick the tier that matches your real threat, check which vector it leaves open, and stack complementary controls on top.&lt;/p>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/hardening-secrets-sovereign-llm-stack-defence-in-depth/">Hardening and secrets in the sovereign LLM stack: defence in depth&lt;/a> — the hardening of the stack that hosts those agents: network, secrets, runtime.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/runbook-caging-ai-agent-bubblewrap-tetragon/">Runbook: caging the AI agent — bubblewrap on the client, Tetragon on the cluster&lt;/a> — the operational companion to this post: the &lt;code>.ai-jail&lt;/code> files, the permissions &lt;code>--bootstrap&lt;/code> and the Tetragon &lt;code>TracingPolicy&lt;/code> files to copy and paste. With commands.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/network-numa-cilium-ebpf-dranet-fourth-leg-pinning/">The kitchen door the maître never checked: network NUMA, Cilium eBPF and DRANET&lt;/a> — the Cilium eBPF datapath Tetragon leans on in the cluster; the same kernel layer, a different use.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">Guardrails and safety in LLMs&lt;/a> — mitigation on the &lt;em>content&lt;/em> plane (what the model says and what is said to it); this post is mitigation on the &lt;em>execution&lt;/em> plane (what the agent&amp;rsquo;s process can do).&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/technical-controls-ens-iso-42001-eu-ai-act-cross-mapping/">Technical controls: ENS × ISO 42001 × EU AI Act&lt;/a> — the compliance framework that runtime isolation materialises: Tetragon as technical evidence for &lt;code>op.mon&lt;/code>/&lt;code>op.exp&lt;/code>.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/oss-catalogue-llmops-six-stages-tool-by-tool/">Catalogue of OSS tools for LLMOps&lt;/a> — where the runtime security layer fits in the complete open stack.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>bubblewrap: &lt;a href="https://github.com/containers/bubblewrap">https://github.com/containers/bubblewrap&lt;/a>&lt;/li>
&lt;li>Landlock LSM: &lt;a href="https://landlock.io">https://landlock.io&lt;/a> · &lt;a href="https://docs.rs/landlock">https://docs.rs/landlock&lt;/a>&lt;/li>
&lt;li>ai-jail (Fabio Akita): &lt;a href="https://github.com/akitaonrails/ai-jail">https://github.com/akitaonrails/ai-jail&lt;/a>&lt;/li>
&lt;li>gVisor: &lt;a href="https://gvisor.dev">https://gvisor.dev&lt;/a>&lt;/li>
&lt;li>Firecracker: &lt;a href="https://firecracker-microvm.github.io">https://firecracker-microvm.github.io&lt;/a>&lt;/li>
&lt;li>Kata Containers: &lt;a href="https://katacontainers.io">https://katacontainers.io&lt;/a>&lt;/li>
&lt;li>Tetragon (Cilium): &lt;a href="https://tetragon.io">https://tetragon.io&lt;/a>&lt;/li>
&lt;li>Vercel Sandbox — concepts: &lt;a href="https://vercel.com/docs/vercel-sandbox/concepts">https://vercel.com/docs/vercel-sandbox/concepts&lt;/a>&lt;/li>
&lt;li>E2B: &lt;a href="https://github.com/e2b-dev/E2B">https://github.com/e2b-dev/E2B&lt;/a>&lt;/li>
&lt;li>Claude Code sandboxing: &lt;a href="https://docs.claude.com/en/docs/claude-code">https://docs.claude.com/en/docs/claude-code&lt;/a>&lt;/li>
&lt;li>Codex CLI: &lt;a href="https://github.com/openai/codex">https://github.com/openai/codex&lt;/a>&lt;/li>
&lt;/ul></description></item></channel></rss>