Cadena de confianza del modelo (3/4): firma, procedencia y AIBOM — por qué te fías de esos bytes

En el primer artículo de esta serie montamos el plano de control del serving; en el segundo, de dónde salen los bytes. Al final de ese recorrido hay un initContainer que hace oras pull de un artefacto de 140 GB y lo deja en un volumen del que vLLM va a leer. La pregunta incómoda es la que da título a este tercero: ¿por qué te fías de esos bytes?

La respuesta habitual —“porque vienen de nuestro registro”— es la que la disciplina de cadena de suministro lleva una década desmontando: un registro es un almacén, no una autoridad; guarda lo que le suben, y quien puede subir puede envenenar. Aquí cubrimos las cuatro piezas que convierten esa frase en algo que una máquina verifica antes de que arranque el pod —firma (Sigstore), firma de modelos (OpenSSF Model Signing), procedencia (SLSA e in-toto) e inventario de materiales (AIBOM)— y la parte que casi nunca se cuenta: qué demuestra de verdad cada una.

TL;DR

  • El artefacto de modelo es código de terceros con acceso a tus datos. pickle ejecuta código en torch.load; safetensors elimina esa clase de ataque pero no dice nada sobre si los pesos están envenenados.
  • Sigstore es la pieza madura: cosign 3.1.1 (9 de junio de 2026), Rekor v2 en GA desde el 10 de octubre de 2025. En air-gap, PKI propia o Sigstore privado; el bundle nuevo permite verificar offline.
  • Un modelo no es un fichero. OpenSSF Model Signing (OMS) v1 (junio de 2025) firma un manifiesto de hashes de todo el directorio; la librería de referencia, model_signing 1.1.1, está en sandbox de OpenSSF.
  • SLSA v1.2 (24 de noviembre de 2025) añade el Source track; Build L3 es el objetivo realista en fine-tuning. in-toto graduó en la CNCF el 23 de abril de 2025.
  • AIBOM: SPDX 3.0.1 y CycloneDX 1.7 (ECMA-424 2ª ed.) tienen los campos; el tooling que los rellena automáticamente casi no existe. Se emite desde el pipeline, no desde un escáner.
  • Regulación: el art. 53 del AI Act obliga a proveedores de GPAI desde el 2 de agosto de 2025, y el Reglamento (UE) 2026/1744 retrasó el alto riesgo a diciembre de 2027 y agosto de 2028 sin tocar GPAI. La CRA exige SBOM —no AIBOM— con aplicación plena el 11 de diciembre de 2027.
  • Nada de esto dice que el modelo sea bueno. Ultralytics produjo atestaciones válidas de artefactos con un criptominero dentro.

La analogía: la trazabilidad del lote

Un lote de jamón que llega al muelle de un supermercado trae cuatro cosas que es fácil confundir. Un número de lote, que lo identifica y a ningún otro: el sha256. Un precinto del matadero, que si está roto delata manipulación en el transporte: la firma. Un registro sanitario que dice de qué matadero salió, en qué fecha y bajo qué inspección: la atestación de procedencia. Y una etiqueta de ingredientes y alérgenos: el AIBOM. En el muelle, el encargado no acepta el palé si el precinto está roto o falta el registro: eso es el admission control.

La analogía se sostiene hasta el detalle: el precinto viaja pegado por fuera, no dentro del jamón (es una firma separada); el registro lo emite el matadero, no el transportista, y su valor depende de que la inspección sea real; y lo que permite retirar producto cuando aparece un problema es el número de lote, no el precinto.

Pero sirve sobre todo por lo que no promete: un jamón perfectamente trazado, precintado y etiquetado puede estar en mal estado. La trazabilidad responde a “de dónde vino y si alguien lo tocó”; no responde a “está bueno”. Este artículo trata de la primera pregunta; la segunda es trabajo de evaluación y de guardrails, y confundirlas es el error más caro de esta disciplina.

PipelineQLoRA / mergerunner efímeroSelladomanifiesto de hashesfirma (OMS / cosign)provenance SLSAAIBOMRegistro OCIartefacto + referrersinmutable por digestMuelleadmission controlverifica firmaexige provenancefail-closedGPUre-verificaantes de cargarEl precinto se pone una vez, en el origen; se comprueba dos veces: en el muelle y antes de cargar.La trazabilidad no dice que el modelo sea bueno. Dice de dónde vino y si alguien lo tocó.

El modelo de amenaza, sin generalidades

pickle, y qué resuelve safetensors

El problema de pickle no es un bug, es su semántica: un objeto puede definir __reduce__ y al deserializar Python ejecuta lo que ese método devuelva. Un checkpoint de torch.save no es un contenedor de números; es un programa que al cargarse puede abrir un shell inverso. No es teórico: en febrero de 2024 JFrog documentó alrededor de cien modelos maliciosos en Hugging Face con payload real —PyTorch con __reduce__ inyectado, Keras abusando de la capa Lambda—, y en febrero de 2025 ReversingLabs describió nullifAI, dos modelos con ficheros pickle deliberadamente corruptos que evadían a Picklescan aprovechando que el payload se ejecuta antes de que el fichero falle. La respuesta llegó tarde: PyTorch 2.6 invirtió el valor por defecto de weights_only a True, restringiendo la deserialización a una lista blanca de tipos. Rompió mucho código —hay issues abiertos en nnUNet, accelerate y media docena de proyectos más— y no es garantía formal: esa lista blanca ha tenido escapes documentados.

safetensors sí resuelve la clase entera: datos puros —cabecera JSON con offsets más un blob de tensores—, sin ejecución posible. La auditoría de Trail of Bits para EleutherAI y Hugging Face, del 23 de mayo de 2023, no encontró ningún fallo crítico que llevara a ejecución arbitraria de código, y de propina el mapeo directo a memoria da cargas del orden de cien veces más rápidas en CPU, algo que ya explotamos en la ruta del disco a la HBM.

El matiz que se olvida sistemáticamente: safetensors garantiza que cargar el fichero no ejecuta código; no garantiza nada sobre los números que hay dentro. Un modelo con puerta trasera entrenada —normal salvo ante un disparador concreto— se distribuye en safetensors, pasa los escáneres y firma sin problema. Es el jamón precintado que está malo.

Adapters LoRA, tags mutables y el modelo del viernes

Un adapter pesa megabytes, se comparte con ligereza y en un stack de multi-LoRA se cargan decenas en caliente sobre el mismo base: es el vector más barato para el atacante y el que menos gobierno tiene. La literatura de 2026 es densa en detección —arXiv 2602.15195, revisado en abril de 2026, reporta ROC-AUC de 1,00 clasificando adapters por estadísticos espectrales de las proyecciones de atención—, pero con la cabeza fría son adapters envenenados por los propios autores con un método conocido: investigación viva, no defensa desplegable. La respuesta operativa realista sigue siendo de dónde vino el adapter y quién lo firmó.

Y dos amenazas sin atacante. Un tag es mutable: mi-registro/llama-70b:produccion puede apuntar hoy a un digest y mañana a otro sin que cambie una línea de tus manifiestos, y basta un oras push bienintencionado; mitigación idéntica a la de las imágenes en el hardening del stack, fijar por digest, nunca por tag, con el detalle de que el digest es lo que la firma cubre. Y el clásico: un ingeniero convierte un checkpoint en su portátil, hace oras push con sus credenciales y el lunes hay un InferenceService sirviéndolo, sin firma, procedencia ni AIBOM. No es malicioso; es ingobernable. Aquí la firma rinde no porque detecte un ataque, sino porque hace inviable el atajo.

Ultralytics: firma válida, artefacto envenenado

El 4 y 5 de diciembre de 2024, las versiones 8.3.41 y 8.3.42 de ultralytics (YOLO) se publicaron en PyPI con un criptominero, por envenenamiento de la caché de GitHub Actions en el workflow de publicación; días después llegó una segunda ronda (8.3.45 y 8.3.46) con tokens de API no rotados al migrar a Trusted Publishing. El detalle que importa: las cuatro versiones maliciosas llevaban atestaciones válidas, porque las generó el propio workflow. La firma era correcta y la procedencia también; lo comprometido era el entorno de build. Como resumió el investigador que lo destapó, una atestación garantiza la relación entre un artefacto y un job de build más un commit — no entre las intenciones del desarrollador y el artefacto final.

Sigstore: quién firmó, y cómo lo compruebas sin llamar a nadie

Sigstore son tres piezas y un cliente. Fulcio es una CA que, a cambio de un token OIDC válido, emite un certificado X.509 de vida muy corta ligado a esa identidad. Rekor es un log de transparencia de solo-anexado donde queda constancia de la firma con su sello temporal. cosign lo orquesta. El resultado es la firma keyless: no hay clave privada de larga duración que custodiar, hay una identidad OIDC —el workflow de CI— y un registro inmutable de que esa identidad firmó ese digest.

Estado a julio de 2026: cosign 3.1.1, del 9 de junio de 2026. La rama 3.x, desde el 8 de octubre de 2025, activó por defecto el formato de bundle nuevo, --trusted-root y --use-signing-config; la consecuencia práctica es que el bundle lleva dentro el material de verificación y permite verificar sin llamar a Rekor ni a Fulcio. Rekor v2 está en GA desde el 10 de octubre de 2025: reimplementación sobre tiles que abarata mucho operar un log propio —se apagan Trillian log server y log signer, las lecturas se cachean en CDN— a cambio de recortar a dos tipos de entrada (hashedrekord y dsse) y quedarse sin índice de búsqueda. Requiere cosign 3.0.1+ o 2.6.0+; v1 sigue en paralelo y su congelación se anunciará con un año de antelación.

Firmar y verificar un artefacto de modelo en OCI

Sobre el artefacto del segundo artículo, la operación es idéntica a la de una imagen: para el registro, un artefacto de modelo es un objeto OCI con su digest.

# Resolver el tag a digest UNA vez, en el pipeline. Se firma el digest, nunca el tag.
oras resolve registro.interno/modelos/llama-70b-fin:v7
#  -> sha256:9f2a4c1e77b0d3a8...

# Firma keyless desde CI (el token OIDC lo aporta el runner)
cosign sign --yes registro.interno/modelos/llama-70b-fin@sha256:9f2a4c1e...

# Verificación: identidad exacta del workflow, emisor exacto
cosign verify \
  --certificate-identity-regexp '^https://git.interno/plataforma/modelos/.*@refs/heads/main' \
  --certificate-oidc-issuer 'https://git.interno' \
  registro.interno/modelos/llama-70b-fin@sha256:9f2a4c1e...

El error más frecuente es omitir --certificate-identity y --certificate-oidc-issuer, o ponerlos tan laxos que acepten cualquier identidad del proveedor. Sin esos dos flags acotados, la verificación comprueba que alguien firmó, no que firmara quien debe: la diferencia entre “trae precinto” y “trae el precinto de nuestro matadero”.

El problema del air-gap

Keyless necesita un OIDC alcanzable al firmar y, en el modelo clásico, un Rekor alcanzable al verificar. En un CPD desconectado, tres estrategias:

EstrategiaQué se ganaQué cuesta
Clave / PKI propia (cosign sign --key)Funciona sin red; encaja con HSM y PKI corporativaVuelve la custodia y rotación de claves de larga duración; sin log de transparencia no hay detección de firma no autorizada
Sigstore privado (Fulcio + Rekor v2 + OIDC internos)Keyless completo y transparencia real dentro del perímetroTres servicios más que operar, con su raíz TUF y su ciclo de rotación
Bundle nuevo + verificación offlineVerificación sin red con --offline y --trusted-root localTransportar y mantener actualizada la raíz de confianza por sneakernet
cosign verify --offline=true \
  --trusted-root /etc/sigstore/trusted_root.json \
  --certificate-identity-regexp '^https://git.interno/plataforma/modelos/.*' \
  --certificate-oidc-issuer 'https://git.interno' \
  registro.interno/modelos/llama-70b-fin@sha256:9f2a4c1e...

Dos advertencias. --insecure-ignore-tlog existe y se usa mucho en air-gap, pero su nombre no engaña: desactiva la comprobación de inclusión en el log de transparencia, que es justo lo que distingue a Sigstore de una PKI convencional; si se usa, se está haciendo firma con certificado, no Sigstore. Y hay fricción documentada: el issue 4550 de cosign describe la 3.0.2 intentando alcanzar el CDN de TUF pese a tener clave local y acceso solo a un Nexus interno. El air-gap funciona, pero no es el camino feliz.

Firmar un modelo no es firmar un fichero

Un modelo servible es un directorio: varios shards de safetensors, config.json, tokenizer, quizá un chat_template.jinja. Cambiar el config.json —la longitud de contexto, el rope_scaling— altera el comportamiento sin tocar un peso. Firmar solo los pesos deja la puerta abierta.

La respuesta es la especificación OpenSSF Model Signing (OMS), publicada en junio de 2025 con contribución de Google, HiddenLayer, NVIDIA, Red Hat, Intel, Meta, IBM y Microsoft. Su diseño: una firma separada que no modifica ni reempaqueta el contenido, sobre un manifiesto que lista todos los ficheros por su hash (SHA-256 por defecto, BLAKE2b como alternativa), con una firma que cubre el manifiesto entero. Es deliberadamente agnóstica de PKI: acepta claves desnudas, certificados autofirmados, PKI corporativa o Sigstore keyless. La implementación de referencia es model_signing, de sigstore/model-transparency, versión 1.1.1 del 10 de octubre de 2025; la 1.1.0 añadió PKCS#11 (HSM), instancias privadas de Sigstore, BLAKE3 y trazas OpenTelemetry.

# Firma todo el directorio: pesos, config y tokenizer
model_signing sign /modelos/llama-70b-fin --signature /modelos/llama-70b-fin/model.sig

# Verificación con identidad OIDC acotada
model_signing verify /modelos/llama-70b-fin \
  --signature /modelos/llama-70b-fin/model.sig \
  --identity 'https://git.interno/plataforma/modelos/.github/workflows/publicar.yml@refs/heads/main' \
  --identity-provider 'https://git.interno'

El payload es un envoltorio DSSE con una declaración in-toto cuyos subjects son los pares ruta-digest de cada fichero y cuyo predicateType es https://model_signing/signature/v1.0; el soporte de shards de fichero permite hashear en trozos. Madurez sin adornos: el proyecto está en sandbox de OpenSSF. La especificación es sólida y tiene detrás a los proveedores que importan —NVIDIA firma modelos en NGC con ella—, pero es el primer escalón formal y hay pocos verificadores independientes de la CLI de referencia.

La aritmética del hashing, que resulta no ser el problema

¿Cuánto tarda en hashearse un modelo de cientos de gigas antes de cada arranque? Sea ( S ) el tamaño total, ( p ) los hilos en paralelo sobre shards distintos, ( r_{\text{cpu}} ) el caudal de hashing por hilo y ( r_{\text{io}} ) el de lectura del almacenamiento:

$$t_{\text{hash}} = \frac{S}{\min\left(p \cdot r_{\text{cpu}},\ r_{\text{io}}\right)}$$

Con un modelo denso de 70B en bf16 —unos 140 GB en una treintena de shards—, SHA-256 acelerado por SHA-NI ronda 1,5-2 GB/s por hilo. Con ocho hilos el término de CPU está en torno a 14 GB/s, muy por encima de lo que entrega un NVMe Gen4, del orden de 6 GB/s. El mínimo lo fija el disco, no el hash: unos 23 segundos. Y ese es el mismo caudal que el loader va a consumir de todas formas para llevar los pesos a HBM: si la verificación ocurre en el mismo initContainer que ya lee el artefacto, el coste marginal es de CPU, no de I/O. Lo caro no es verificar un modelo grande, sino haber puesto la verificación en un paso separado que lee el disco dos veces (el resto del presupuesto de arranque, en el post de cold start). Lo que sí tiene coste real es la verificación incremental: si solo cambia un shard, el manifiesto permite comprobar únicamente ese, pero solo si la capa de distribución hace pull incremental. Con artefacto OCI y capas granuladas funciona; con un tarball monolítico, no.

Procedencia: SLSA e in-toto sobre un pipeline de QLoRA

La firma dice quién publicó; la procedencia dice cómo se produjo. El marco es SLSA, cuya v1.2 se publicó el 24 de noviembre de 2025, compatible hacia atrás con la v1.1. Su novedad es el Source track; el Build Environment track y el Dependency track siguen en desarrollo — y el primero es exactamente el que habría ayudado en Ultralytics.

NivelQué exige SLSAQué significa en un pipeline QLoRA
Build L0NadaEl adapter que alguien entrenó en su workstation
Build L1Procedencia generada automáticamente: quién construyó, con qué proceso y con qué entradas de primer nivel. Puede ir sin firmarEl job emite provenance con commit, dataset e hiperparámetros. Detecta errores, no ataques
Build L2Lo anterior más build en plataforma alojada que genera y firma la procedencia, validable por el consumidorEl runner de CI firma con su identidad. Ya hay algo que un admission controller puede verificar
Build L3Lo anterior más aislamiento entre ejecuciones y material de firma inaccesible desde los pasos definidos por el usuarioRunner efímero por job, sin secretos de firma al alcance del script de entrenamiento. Es el objetivo realista

in-toto es el formato en el que se expresa todo esto: graduó en la CNCF el 23 de abril de 2025, lo que en la práctica lo convierte en sustrato común de SLSA, de cosign attest y del propio OMS. Sobre el flujo del runbook de QLoRA y del fine-tuning continuo, la atestación útil es la que permite reproducir y auditar:

{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [{ "name": "adapter-soporte-v7",
                "digest": { "sha256": "e3b0c44298fc1c149afbf4c8..." } }],
  "predicateType": "https://slsa.dev/provenance/v1",
  "predicate": {
    "buildDefinition": {
      "buildType": "https://interno/build-types/qlora-finetune@v2",
      "externalParameters": {
        "modeloBase": "registro.interno/modelos/llama-70b@sha256:9f2a...",
        "dataset": "lakefs://corpus-soporte@commit-4c1f9e",
        "hiperparametros": { "rank": 16, "alpha": 32, "lr": 0.0002,
                             "epochs": 3, "quant": "nf4" }
      },
      "internalParameters": {
        "gpu": "4xH100-SXM-80GB", "cuda": "12.6", "torch": "2.8.0",
        "imagenEntrenamiento": "registro.interno/ci/qlora@sha256:71ca..."
      },
      "resolvedDependencies": [
        { "uri": "git+https://git.interno/plataforma/finetune@a91c3f",
          "digest": { "gitCommit": "a91c3f..." } }
      ]
    },
    "runDetails": {
      "builder": { "id": "https://git.interno/plataforma/runners/gpu-efimero" },
      "metadata": { "invocationId": "run-2026-07-19-0041",
                    "startedOn": "2026-07-19T02:14:33Z" }
    }
  }
}

Los cinco campos que hacen falta y casi nunca están completos: hash del modelo base, identificador inmutable del dataset (aquí un commit de lakeFS, en la línea del versionado de datos), hiperparámetros, hardware y versiones del stack, y commit del código. Sin el primero no se reconstruye la cadena hasta el modelo original; sin el segundo, el bucle de retrain queda sin evidencia reproducible. Y la advertencia obligatoria: SLSA no dice nada sobre la calidad del dataset; Build L3 garantiza que nadie manipuló el build, no que el corpus estuviera limpio.

AIBOM: SPDX 3.0.1 frente a CycloneDX 1.7

Si el manifiesto es el precinto y la procedencia el registro sanitario, el AIBOM es la etiqueta de ingredientes. Hay dos formatos, y la comparativa honesta es menos halagüeña de lo que sugiere la literatura de proveedor. SPDX 3.0 (abril de 2024), con parche 3.0.1 el 17 de diciembre de 2024, reorganizó la especificación en perfiles, dos relevantes aquí: AI y Dataset. El perfil AI define sobre AIPackage campos como typeOfModel, hyperparameter, informationAboutTraining, metric, safetyRiskAssessment y limitation, más —singularidad suya— cuatro campos de consumo energético desglosados en entrenamiento, fine-tuning e inferencia. La v3.1 lleva en RC desde enero de 2025 sin GA, y la 3.0 está en trámite ISO como ISO/IEC DIS 5962 (la norma vigente sigue siendo la 2.2.1). CycloneDX 1.7 se publicó el 21 de octubre de 2025 y fue ratificado como ECMA-424, 2ª edición, en diciembre de 2025; su ML-BOM se apoya en el tipo machine-learning-model y el objeto modelCard.

CriterioSPDX 3.0.1CycloneDX 1.7
Estandarización formalISO/IEC DIS 5962 en curso; norma vigente es la 2.2.1ECMA-424 2ª ed. (diciembre de 2025)
Modelo de IAPerfil AI sobre AIPackagemachine-learning-model + modelCard
DatasetPerfil Dataset dedicadodata y modelParameters.datasets
EnergíaCuatro campos desglosados (único)No cubierto de forma nativa
Uso previsto y éticalimitation, safetyRiskAssessmentconsiderations (más cercano a Model Cards)
ErgonomíaModelo rico, verboso, curva altaJSON compacto, más adopción en tooling
Encaje con VEXVía perfil SecurityNativo, alineado con CSAF VEX 2.0
{
  "bomFormat": "CycloneDX", "specVersion": "1.7", "version": 1,
  "components": [{
    "type": "machine-learning-model",
    "bom-ref": "modelo/adapter-soporte-v7",
    "name": "adapter-soporte", "version": "7.0.0",
    "hashes": [{ "alg": "SHA-256", "content": "e3b0c44298fc1c14..." }],
    "licenses": [{ "license": { "id": "Apache-2.0" } }],
    "modelCard": {
      "modelParameters": {
        "task": "text-generation",
        "modelArchitecture": "llama-70b + LoRA r=16",
        "datasets": [{ "type": "dataset", "name": "corpus-soporte",
                       "contents": { "url": "lakefs://corpus-soporte@commit-4c1f9e" } }]
      },
      "quantitativeAnalysis": {
        "performanceMetrics": [
          { "type": "exactitud-eval-interna", "value": "0.83", "slice": "soporte-es" }
        ]
      },
      "considerations": {
        "useCases": ["asistencia interna a agentes de soporte"],
        "technicalLimitations": ["no evaluado fuera de castellano"]
      }
    }
  }]
}

Qué se genera de verdad hoy. Un tooling automático rellena bien nombre, versión, hashes, licencias y dependencias del entorno de inferencia: eso es un SBOM clásico y Trivy o Syft lo hacen solos. Lo que solo puede venir del pipeline, porque ningún escáner lo infiere de los bytes: dataset, hiperparámetros, métricas, modelo base, energía. Y lo que es puramente humano: limitaciones, usos previstos, consideraciones éticas. La conclusión es incómoda: un AIBOM generado a posteriori por una herramienta que mira el artefacto es, en su mayor parte, un documento vacío con estructura válida. El AIBOM útil lo emite el job de entrenamiento —el único que conoce esos campos— y se firma junto con el artefacto.

Regulación: qué obliga, y desde cuándo exactamente

Conviene ser quirúrgico con las fechas, porque han cambiado en 2026 y mucha documentación en circulación está desactualizada.

EU AI Act

Las obligaciones de los proveedores de modelos de propósito general (GPAI) aplican desde el 2 de agosto de 2025. El artículo 53 exige documentación técnica conforme al Anexo XI, información para proveedores aguas abajo conforme al Anexo XII, una política de derechos de autor y un resumen público del contenido de entrenamiento. El Anexo XI, sección 1, pide explícitamente las especificaciones del proceso de entrenamiento, información sobre los datos usados para entrenamiento, prueba y validación, incluyendo su tipo y procedencia, los recursos computacionales empleados (por ejemplo, operaciones en coma flotante), el tiempo de entrenamiento y el consumo energético conocido o estimado. Quien hubiera puesto un modelo en el mercado antes de esa fecha tiene hasta el 2 de agosto de 2027.

El cambio de 2026: el Reglamento (UE) 2026/1744, el “Digital Omnibus” de IA, se publicó en el DOUE el 24 de julio de 2026 y entró en vigor el 27 de julio de 2026. El Capítulo III para alto riesgo del Anexo III pasa del 2 de agosto de 2026 al 2 de diciembre de 2027; el alto riesgo del Anexo I (IA como componente de seguridad de productos regulados), al 2 de agosto de 2028; el 2 de diciembre de 2026 entran nuevas prohibiciones del artículo 5 y el marcado legible por máquina para GPAI. Las obligaciones de GPAI de los artículos 51 a 56 no se retrasan.

La lectura para un arquitecto: si afinas un modelo y lo pones en el mercado, el Anexo XI es exigible ya, y sus campos son casi literalmente los de una atestación SLSA más un AIBOM. Si tu caso es de alto riesgo tienes año y medio más de margen del que creías, pero ese margen es para los estándares armonizados, no para empezar tarde (detalle en el post del AI Act).

Cyber Resilience Act

La CRA entró en vigor el 10 de diciembre de 2024; las obligaciones de notificación del artículo 14 aplican desde el 11 de septiembre de 2026 y la aplicación plena desde el 11 de diciembre de 2027. Lo que exige, en el Anexo I, Parte II, punto 1, es una lista de materiales de software en un formato comúnmente utilizado y legible por máquina “que cubra al menos las dependencias de primer nivel”.

Tres matices que se citan mal. “Al menos las dependencias de primer nivel” es un suelo bajísimo: no exige el árbol transitivo. El SBOM no tiene que ser público —el considerando 77 lo dice expresamente—: es documentación interna que las autoridades pueden requerir. Y la CRA no nombra ningún formato; la Comisión se reserva concretarlo por acto de ejecución, y la guía más precisa hoy es la BSI TR-03183-2, que acepta CycloneDX 1.6+ o SPDX 3.0.1+. El punto clave: la CRA habla de SBOM, no de AIBOM; que el artefacto de modelo caiga bajo su paraguas es interpretación, no texto.

Encaje con ENS e ISO/IEC 42001

Nada de esto es higiene voluntaria. La firma y su verificación en admission materializan op.exp.6 y op.ext.3 del ENS (cadena de suministro) y el control A.10 de ISO/IEC 42001; la atestación de procedencia cubre op.exp.2 y el A.6 (ciclo de vida del sistema de IA); el AIBOM con dataset e hiperparámetros es op.exp.1 (inventario) y A.7 (datos); y el log de transparencia, op.exp.8. La evidencia que pide un auditor de ISO/IEC 42001 sobre el ciclo de vida es, en buena medida, el mismo JSON que emite el pipeline; el desglose control a control, en el post de controles técnicos.

Verificación en el clúster: el muelle de recepción

Nada de lo anterior vale si nadie comprueba el precinto en la puerta. Kyverno graduó en la CNCF el 16 de marzo de 2026; su 1.17 (febrero de 2026) promovió a v1 las políticas CEL —incluida ImageValidatingPolicy— y marcó ClusterPolicy como obsoleta, y la 1.18 (24 de abril de 2026) pulió la verificación de imágenes. La alternativa, el policy-controller de Sigstore, va por v0.15.1 (26 de marzo de 2026), que subió de cosign v2 a v3 y migró a go-tuf v2 —relevante para quien opera un Sigstore privado con roles delegados—.

apiVersion: policies.kyverno.io/v1
kind: ImageValidatingPolicy
metadata:
  name: modelos-firmados-y-con-provenance
spec:
  validationActions: [Deny]        # fail-closed: no admite, no audita
  failurePolicy: Fail              # si el webhook falla, se deniega
  webhookConfiguration:
    timeoutSeconds: 20             # el pull de la firma puede tardar
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        resources: ["pods"]
        operations: ["CREATE", "UPDATE"]
  matchImageReferences:
    - glob: "registro.interno/modelos/*"
  attestors:
    - name: ci-plataforma
      cosign:
        keyless:
          identities:
            - issuer: "https://git.interno"
              subject: "https://git.interno/plataforma/modelos/.github/workflows/publicar.yml@refs/heads/main"
        ctlog:
          url: "https://rekor.interno"
  attestations:
    - name: slsaProvenance
      intoto:
        type: "https://slsa.dev/provenance/v1"
  validations:
    - expression: >-
        images.containers.map(image,
          verifyImageSignatures(image, [attestors.ci_plataforma])).all(e, e > 0)        
      message: "Artefacto de modelo sin firma valida de la CI de plataforma"
    - expression: >-
        images.containers.map(image,
          verifyAttestationSignatures(image, attestations.slsaProvenance,
            [attestors.ci_plataforma])).all(e, e > 0)        
      message: "Falta atestacion SLSA de procedencia"

Verificar el modelo, no solo la imagen

Esa política cubre la imagen del contenedor de serving. El modelo se descarga después, en el initContainer, donde el admission controller ya no llega. El patrón que funciona es verificar ahí, antes de escribir en el volumen compartido:

initContainers:
  - name: traer-y-verificar-modelo
    image: registro.interno/plataforma/oras-cosign@sha256:4b7e...
    command: ["/bin/sh", "-ec"]
    args:
      - |
        oras pull registro.interno/modelos/llama-70b-fin@sha256:9f2a... -o /modelos
        cosign verify --offline=true \
          --trusted-root /etc/sigstore/trusted_root.json \
          --certificate-oidc-issuer https://git.interno \
          --certificate-identity-regexp '^https://git.interno/plataforma/modelos/.*' \
          registro.interno/modelos/llama-70b-fin@sha256:9f2a...
        model_signing verify /modelos --signature /modelos/model.sig \
          --identity-provider https://git.interno \
          --identity 'https://git.interno/plataforma/modelos/.github/workflows/publicar.yml@refs/heads/main'        
    volumeMounts:
      - { name: modelos, mountPath: /modelos }
      - { name: sigstore-root, mountPath: /etc/sigstore, readOnly: true }

La doble verificación no es redundante: cosign verify cubre el artefacto OCI tal como está en el registro, y model_signing verify cubre el contenido desempaquetado tal como lo va a leer el motor. Si alguien monta un ConfigMap que sobreescribe el config.json después del pull, la primera pasa y la segunda no.

Fail-closed frente a fail-open, y lo que cuesta

failurePolicy: Fail con validationActions: [Deny] significa que si el webhook no responde no arranca nada: lo correcto desde seguridad, y exactamente lo que tumba un clúster un domingo por la mañana. La postura defendible: fail-closed en el namespace de producción, con el controlador en alta disponibilidad y su propio namespace excluido de sus políticas para poder recuperarlo; Audit durante la adopción, la misma regla de “observar primero, bloquear después” que aplicamos con Tetragon en el hardening; y un procedimiento de excepción escrito y con caducidad, porque el día del hotfix a las tres de la mañana alguien se va a saltar la política y es mejor que lo haga por un camino auditado.

Sobre latencia, lo importante es que es coste de arranque, no de request: se verifica una vez por pod. Un benchmark de terceros de marzo de 2026 sobre policy-controller v0.15 reporta 92 ms en p50 y 184 ms en p99 para imágenes firmadas internamente, con picos de hasta 4 segundos en despliegues masivos. Conviene tomarlo por lo que es —medición de un tercero, sin réplica independiente publicada—, pero el orden de magnitud es coherente: centenares de milisegundos frente a los minutos que tarda un pod en cargar 140 GB a HBM.

Como segunda red, Harbor guarda las firmas de cosign como artefactos referrer y puede impedir el despliegue de lo no firmado por política de proyecto. Y en el GitOps con Flux, lo que cierra el círculo es referenciar siempre por digest y verificar la firma del artefacto OCI en la reconciliación: dos comprobaciones independientes en momentos distintos.

Mapa de decisión: qué poner, en qué orden

Por retorno decreciente sobre esfuerzo:

  1. Pin por digest en todo el GitOps. Una tarde; sin esto, lo demás es decorativo.
  2. Firma de imágenes con cosign keyless desde CI y ImageValidatingPolicy en Audit. Días. Revela cuánto de tu clúster no está firmado, que suele ser una sorpresa.
  3. Firma del artefacto de modelo con model_signing en el job que lo publica. Días. Mata el modelo subido a mano un viernes.
  4. Paso a Deny en el namespace de inferencia, con excepciones auditadas y controlador en HA.
  5. Atestación SLSA, objetivo Build L2 y luego L3. Semanas: exige runners efímeros y sacar el material de firma del alcance del script de entrenamiento.
  6. AIBOM emitido desde el pipeline, con los cinco campos que solo él conoce. Semanas, y sobre todo trabajo de proceso.
  7. Sigstore privado, solo con requisito real de air-gap y con los seis anteriores hechos.

Entre Kyverno y policy-controller: Kyverno si hay o va a haber un programa de política más amplio —Pod Security, etiquetas, cuotas—, por unificación y por su estado graduado; policy-controller si la organización es Sigstore-céntrica y no quiere más motor de políticas que el imprescindible. Entre keyless y clave: keyless si CI alcanza un OIDC, aunque sea interno; clave con HSM en air-gap estricto donde no se vaya a operar un Fulcio propio.

Trampas operativas

  • Verificar sin acotar la identidad. cosign verify sin --certificate-identity ni --certificate-oidc-issuer, o con un regexp que acepta cualquier repositorio del proveedor, es teatro de seguridad. La trampa número uno y la más silenciosa.
  • Bugs fail-closed en el propio verificador. El issue 16435 de Kyverno, abierto el 2 de julio de 2026 contra la 1.18.0, describe dos regresiones simultáneas en ImageValidatingPolicy con atestadores de clave y certificado: un SIGSEGV por puntero nulo y un fallo de tlog con el mensaje not enough verified log entries from transparency log: 0 < 1. Ambas deniegan imágenes legítimas o tumban el controlador. Corregido para la 1.19, pero la lección es permanente: el verificador es un componente crítico y sus regresiones son indisponibilidad.
  • La raíz de confianza que caduca en air-gap. Se copió a mano hace nueve meses, nadie lleva el calendario, y un día la verificación falla sin que nadie haya tocado nada.
  • El AIBOM que nadie regenera. Se emite en el despliegue inicial, se afina el modelo tres veces y sigue describiendo la versión uno. Un inventario desactualizado es peor que ninguno: genera confianza injustificada.
  • Verificar la imagen y olvidar el modelo. Lo más habitual: el contenedor de vLLM impecablemente firmado, y los 140 GB que carga vienen de un bucket sin verificar.

Lo que esto NO demuestra

Una firma dice quién publicó, no que el modelo sea bueno ni seguro. Es una afirmación de autoría, no de calidad. Ultralytics lo documenta: cuatro releases con criptominero y atestaciones válidas. Si el entorno de build está comprometido, la firma certifica fielmente el artefacto comprometido.

Un modelo firmado puede tener puerta trasera. Nada en Sigstore, OMS o SLSA examina los pesos. Un backdoor entrenado por quien tiene acceso legítimo al pipeline atraviesa toda la cadena sin encender una alarma; la defensa es evaluación adversarial y canary o shadow, no criptografía. Y SLSA no dice nada de la calidad del dataset: un pipeline formalmente perfecto sobre datos envenenados produce un modelo envenenado con procedencia impecable.

El AIBOM es tan bueno como el proceso que lo genera. Los campos que importan no los infiere ninguna herramienta. Y el coste operativo es real y recurrente: rotación de claves y de raíces TUF, incidentes de verificación de madrugada, ruido de política durante la adopción, y el trabajo continuo de mantener un inventario que nadie lee hasta que hay auditoría.

El reparto de madurez a julio de 2026. Maduro y desplegable: Sigstore y cosign sobre artefactos OCI, Kyverno y policy-controller como admission control, in-toto como formato graduado en CNCF, el pin por digest. Utilizable con criterio: OMS y model_signing (especificación sólida y respaldo industrial fuerte, pero proyecto en sandbox), SLSA Build L2-L3 en fine-tuning, CycloneDX ML-BOM. Todavía trabajo en curso: los tracks de Build Environment y Dependency de SLSA —los que cubrirían justamente el escenario Ultralytics—, el tooling que rellena un AIBOM automáticamente, y toda la detección de backdoors en pesos, que hoy es literatura y no producto.

Para una factoría de inferencia

Primera: el initContainer es tu muelle de recepción, y hoy probablemente no comprueba nada. El admission controller mira la imagen; el modelo entra después por una puerta lateral. Añadir ahí cosign verify --offline y model_signing verify cuesta un par de tardes, cabe en el presupuesto de arranque en frío —23 segundos de hashing sobre un caudal de disco que ya estás pagando— y cierra el agujero más grande de la mayoría de despliegues.

Segunda: fija por digest hoy, firma mañana. Si solo cabe una cosa este trimestre, es eliminar los tags mutables del GitOps: es barato, no rompe nada y convierte “confío en el registro” en “confío en estos bytes concretos”. La firma añade la identidad del emisor encima, pero sin digest no tiene de qué agarrarse.

Tercera: exige la atestación a quien te entrega el modelo, aunque sea el equipo de al lado. Una factoría de inferencia consume lo que produce otro, y ese contrato debería ser explícito: firma de una identidad de CI conocida, atestación con hash del modelo base y commit del dataset, y AIBOM emitido desde el pipeline. No es burocracia: es lo que ya exige el Anexo XI del AI Act, en vigor, y lo que permite responder en veinte minutos —y no en dos semanas— cuando alguien pregunta qué hay exactamente detrás del endpoint.

La cadena tiene ya tres eslabones: sabes cómo se sirve el modelo, de dónde salen sus bytes y por qué te fías de ellos. Queda la última pregunta: cuando ese pod arranca con el modelo verificado, ¿quién es exactamente ese pod frente al resto del sistema, y hasta qué punto puedes confiar en la máquina donde se ejecuta? El cuarto artículo entra en identidad de carga de trabajo con SPIFFE/SPIRE y aislamiento con Confidential Containers — porque un modelo verificado, servido por un proceso no identificado sobre un host que no puedes atestiguar, deja la cadena abierta justo en el último eslabón.

Ver también

Fuentes