Guía9 min

Trivy para tu agente IA: escanear la imagen Docker antes de desplegar

Resumen

Tu imagen del agente arrastra CVEs de la base y de las librerías aunque tu código esté limpio. Esta guía muestra cómo escanearla con Trivy en local y en CI: instalación, severidades, exit-code como puerta de deploy, SARIF en GitHub y qué hacer con cada hallazgo. Distinto de SBOM, de pin digest y de dockerignore.

DockerGitHub
Escáner de seguridad revisando las capas de una imagen Docker antes del despliegue

Qué resuelve

Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.

Tu Dockerfile parte de node:22-slim, instalas dependencias con pnpm y la imagen funciona. Lo que no ves: esa base trae decenas de paquetes del sistema y tus node_modules traen cientos de librerías transitivas. Cualquiera puede tener un CVE publicado con exploit conocido. Escanear la imagen con Trivy antes del deploy es la puerta más barata entre “funciona” y “funciona sin agujeros conocidos”. Hub: seguridad, coste y operación.

No es SBOM + attestations (eso declara qué contiene la imagen; Trivy dice qué está roto). No es pin de digest (eso fija qué SHA tiras; aquí verificas si ese SHA es seguro). No es dockerignore (eso evita filtrar secretos al contexto; Trivy también detecta secretos ya commiteados).

La regla

Cada imagen que despliegas se escanea en CI y un CRITICAL con fix disponible bloquea el deploy. El escaneo local es para iterar rápido; la verdad vive en el pipeline, sobre la imagen exacta que vas a publicar, no sobre una reconstruida parecida.

Trivy tiene targets (dónde busca) y scanners (qué busca). Los targets que importan para un agente: image, fs y repo. Los scanners: vuln (CVEs en paquetes OS y librerías), secret (tokens y keys) y misconfig (Dockerfile y YAML mal configurados).

Instalación en dos minutos

Tres vías oficiales, elige una:

brew install trivy
docker run aquasec/trivy image python:3.12-slim

O descarga el binario desde github.com/aquasecurity/trivy/releases/latest. La vía Docker sirve cuando no quieres instalar nada en el laptop; la vía brew/binario es más rápida para escanear en loop local.

Primer escaneo, contra la imagen de tu agente:

trivy image ghcr.io/org/agente:1.4

Sin flags ya lista vulnerabilidades de paquetes OS y de librerías con su severidad. La primera corrida siempre asusta: una base slim típica reporta decenas de LOW/MEDIUM. Eso es normal y no es razón para bloquear nada todavía.

Filtrar el ruido: severidad y exit-code

La diferencia entre “escanear” y “puerta de deploy” son dos flags. --severity elige qué mirar; --exit-code 1 hace que Trivy falle cuando encuentra algo, y eso es lo que el CI usa para bloquear:

trivy image --severity CRITICAL,HIGH --exit-code 1 --ignore-unfixed ghcr.io/org/agente:1.4

--ignore-unfixed es la pieza que vuelve esto operable: solo falla por vulnerabilidades que ya tienen parche disponible. Un CRITICAL sin fix no lo puedes arreglar actualizando nada; reportarlo como bloqueo solo genera ruido que el equipo aprende a ignorar, y una puerta ignorada es peor que no tener puerta.

Para iteración local, el escáner de secretos sobre el repo vale oro antes de cada commit grande:

trivy fs --scanners vuln,secret,misconfig ./mi-agente/

Esto atrapa el TELEGRAM_TOKEN pegado en un .env.ejemplo real, el Dockerfile que corre como root y las dependencias con CVE, todo en una sola pasada.

Diagrama del flujo de escaneo con Trivy desde la imagen hasta el reporte de severidades

La puerta en CI con trivy-action

El patrón oficial es construir la imagen en el job y escanearla antes de pushearla al registry. Ejemplo verificado de aquasecurity/[email protected]:

- name: Run Trivy vulnerability scanner
  uses: aquasecurity/[email protected]
  with:
    image-ref: "docker.io/my-organization/my-app:${{ github.sha }}"
    format: "table"
    exit-code: "1"
    ignore-unfixed: true
    vuln-type: "os,library"
    severity: "CRITICAL,HIGH"

Tres decisiones en ese bloque: severity solo CRITICAL,HIGH (los MEDIUM van a reporte, no a bloqueo), ignore-unfixed: true (solo lo arreglable bloquea) y escanear el tag con el SHA del commit (la imagen exacta, no latest).

Para que los hallazgos aparezcan en la pestaña Security de GitHub, cambia el formato a SARIF y súelo como hace la propia documentación de la action:

format: "sarif"
output: "trivy-results.sarif"
severity: "CRITICAL"

seguido del step de upload-sarif. Así el equipo ve cada CVE como alerta trazable en vez de un log enterrado en un job verde.

Si prefieres centralizar la política en el repo en vez de en el YAML del workflow, la action acepta trivy-config: trivy.yaml con el formato, el exit-code y la severidad versionados junto al código. Los campos image-ref, scan-ref y scan-type siempre van en el with porque el archivo de config no puede definirlos.

Qué modo usar y cuándo

ModoComandoCuándo
imagetrivy image org/agente:tagPuerta de deploy, CI, lo que corre en prod
fstrivy fs --scanners vuln,secret,misconfig ./dirPre-commit local, secretos y misconfig
repotrivy repo https://github.com/org/agenteAuditoría rápida de un repo sin clonarlo
k8strivy k8s --report summary clusterCluster vivo, workloads ya desplegados

Para un agente con self-hosting en VPS, image en CI más fs local cubre el 95 %. k8s solo importa si migras a orquestador.

Tabla de decisión de modos de Trivy según la etapa del pipeline de despliegue

Qué hacer con cada hallazgo

El escáner te dice el problema, no la cura. El orden de respuesta:

  1. Actualiza la imagen base (node:22-slim → rebuild con el tag menor más reciente) y reconstruye sin cache.
  2. Actualiza la dependencia afectada (pnpm update <paquete>) si el CVE es de librería.
  3. Si no hay fix, evalúa: ¿el paquete vulnerable está en el runtime o solo en la etapa de build? Un multi-stage limpio elimina familias enteras de hallazgos.
  4. Secretos encontrados: rota el token inmediatamente, sácalo del historial y muévelo a secrets del compose o del CI.
  5. Misconfigs del Dockerfile (USER root, puerto expuesto de más): corrige el Dockerfile, no suprimas la regla.

Nunca silencies por severidad global para “pasar el CI hoy”. Si un hallazgo bloquea y no aplica a tu caso (paquete no cargado en runtime), suprímelo por CVE ID con expiración, no apagando el scanner.

Checklist

  • trivy image corre local contra la imagen que despliegas, no contra latest genérico.
  • CI falla con --severity CRITICAL,HIGH --exit-code 1 --ignore-unfixed.
  • trivy fs --scanners vuln,secret,misconfig corre antes de commits grandes.
  • SARIF subido a GitHub Security para trazabilidad.
  • Base reconstruida regularmente; multi-stage deja fuera tooling de build.
  • Supresiones por CVE ID con fecha, nunca apagando reglas.

FAQ

¿Trivy reemplaza el SBOM? No. El SBOM declara el inventario, Trivy lo evalúa contra bases de CVEs. Se complementan: publica SBOM + provenance y escanea con Trivy en el mismo pipeline.

¿Cada cuánto reescanear? En cada build de CI siempre. Además, reescanea imágenes ya publicadas con cron semanal: un CVE nuevo puede afectar una imagen que ayer estaba limpia.

¿La base de vulnerabilidades necesita update? Trivy descarga su DB automáticamente en la primera corrida y la refresca sola. En runners efímeros de CI eso suma segundos al job; acéptalo, es el costo de datos frescos.

¿Escaneo también los modelos o los prompts? No es su trabajo. Trivy cubre paquetes, secretos y misconfig de infraestructura. Los riesgos del comportamiento del modelo (inyecciones, fugas por prompt) viven en guardrails y evals, no en el scanner de imágenes.

¿Y si el registry es privado? Trivy soporta registries privados con las credenciales del daemon o variables de entorno, incluyendo ECR, GAR y ACR. Autentícate con docker login antes del escaneo y funciona igual que con Docker Hub.