Módulo 6: Runtime Security Admission Control And Image Scanning

7. Manos a la obra: `trivy image` sobre `andes-cargo-status-api`

Descripción

Las lecciones 3-6 de este módulo evaluaron la forma de un objeto de Kubernetes: ¿tiene este Pod los campos que la política exige? Esta lección cambia de capa por completo — evalúa el contenido de la imagen Docker que ese Pod referencia: ¿qué software, con qué versión exacta, con qué vulnerabilidades conocidas ya publicadas, está instalado dentro de andes-cargo-status-api:latest? trivy image responde esa pregunta escaneando cada capa de la imagen real, no un archivo de configuración que la describe.

Conexión con el módulo

cloud-security-and-guardrails-guide, en su Módulo 5, ya usó Trivy — pero contra un artefacto completamente distinto. Esa guía corrió trivy config sobre archivos .tf (HCL): busca configuración de infraestructura declarada de forma insegura, antes de que exista (un aws_s3_bucket sin cifrado, por ejemplo), sin ejecutar nada. Esta lección corre trivy image: busca vulnerabilidades reales, ya instaladas dentro de una imagen que ya se construyó y ya corre. Son dos comandos distintos (trivy config frente a trivy image), del mismo binario, apuntando a dos capas completamente distintas de la pila.


Confirma la versión de Trivy

trivy --version

Qué esperar:

Version: 0.74.0
Check Bundle:
  Digest: sha256:1583562f8b90ed2a071b99f0e5ffff6b57e4ceb6ca3e4796577b4e6a339eb74c
  DownloadedAt: 2026-08-14 16:37:24.938201 +0000 UTC

Version: 0.74.0 — la misma versión, exacta, que cloud-security-and-guardrails-guide ya usó. El Digest y el DownloadedAt son variables: dependen de cuándo tu propia máquina descargó la base de datos de vulnerabilidades por última vez.


trivy image frente a trivy config: el mismo binario, dos comandos completamente distintos

   cloud-security-and-guardrails-guide, M5           este módulo (M6.7)

   trivy config andes-cargo-infra/                    trivy image andes-cargo-status-api:latest
              │                                                    │
              ▼                                                    ▼
   ┌──────────────────────┐                          ┌──────────────────────────┐
   │  lee archivos .tf         │                          │  lee capas de la imagen      │
   │  (texto, sin ejecutar        │                          │  (filesystem real, ya          │
   │   nada, sin ninguna           │                          │   construido, con software     │
   │   imagen involucrada)         │                          │   instalado de verdad)          │
   └──────────┬───────────┘                          └──────────┬───────────────┘
              │ compara reglas de la comunidad                    │ compara paquetes instalados
              │ contra CONFIGURACIÓN declarada                    │ contra bases de datos de CVE
              ▼                                                    ▼
   "este bucket S3 no tiene                            "este paquete Debian, en esta
    cifrado activado" (antes                            versión exacta, tiene esta
    de que el bucket exista)                            vulnerabilidad ya publicada"

trivy config responde "¿está esto bien declarado?" — un problema de intención, verificable sin ejecutar nada. trivy image responde "¿tiene esto vulnerabilidades conocidas, ya instaladas?" — un problema de contenido real, que solo existe una vez que la imagen ya se construyó con docker build (el mismo Dockerfile heredado del Módulo 1 de esta guía, sin reescribirse). Son preguntas distintas, sobre objetos distintos, en momentos distintos del ciclo de vida de un artefacto.


El escaneo completo

trivy image andes-cargo-status-api:latest

Qué esperar — el resumen (los conteos por severidad son reales de esta ejecución; van a cambiar en la tuya según la fecha en que corras el comando, porque la base de datos de CVE de Trivy se actualiza constantemente):

Report Summary

┌──────────────────────────────────────────────┬────────────┬─────────────────┬─────────┐
│                    Target                     │    Type    │ Vulnerabilities │ Secrets │
├──────────────────────────────────────────────┼────────────┼─────────────────┼─────────┤
│ andes-cargo-status-api:latest (debian 13.6)   │   debian   │       179       │    -    │
├──────────────────────────────────────────────┼────────────┼─────────────────┼─────────┤
│ Python                                        │ python-pkg │        5        │    -    │
└──────────────────────────────────────────────┴────────────┴─────────────────┴─────────┘

andes-cargo-status-api:latest (debian 13.6)
===========================================
Total: 179 (UNKNOWN: 30, LOW: 66, MEDIUM: 60, HIGH: 19, CRITICAL: 4)

184 hallazgos en total (179 en el sistema operativo base + 5 en dependencias de Python), repartidos en dos categorías completamente distintas:

  • andes-cargo-status-api:latest (debian 13.6) — 179 hallazgos. Estos no vienen de ningún código que Andes Cargo haya escrito — vienen del sistema operativo base de la imagen (Debian 13.6, la distribución sobre la que se construye el Dockerfile de andes-cargo-status-api). Cada paquete del sistema (apt, bash, coreutils, bsdutils, etc.) trae su propio historial de CVE, acumulado a lo largo de los años, independientemente de qué aplicación corra encima.
  • Python — 5 hallazgos. Estos sí son específicos de las dependencias que andes-cargo-status-api declara —Flask, boto3, y sus dependencias transitivas—, instaladas por pip dentro de la imagen.

Los 4 hallazgos CRITICAL

┌───────────┬────────────────┬──────────┬──────────────┬───────────────────┬───────────────┐
│  Library  │ Vulnerability  │ Severity │    Status    │ Installed Version │ Fixed Version │
├───────────┼────────────────┼──────────┼──────────────┼───────────────────┼───────────────┤
│ perl-base │ CVE-2026-13221 │ CRITICAL │ affected     │ 5.40.1-6          │               │
│           │ CVE-2026-42496 │          │ fix_deferred │                   │               │
│           │ CVE-2026-57433 │          │ affected     │                   │               │
│           │ CVE-2026-8376  │          │              │                   │               │
└───────────┴────────────────┴──────────┴──────────────┴───────────────────┴───────────────┘
Total: 4 (CRITICAL: 4)

Los cuatro hallazgos CRITICAL de esta ejecución caen, los cuatro, sobre el mismo paquete: perl-base 5.40.1-6, la instalación mínima de Perl que trae Debian como parte de su sistema base (usada internamente por herramientas del propio sistema operativo, no por el código de Andes Cargo). Fíjate en la columna Fixed Version: está vacía en los cuatro casos — no hay todavía una versión corregida disponible para ninguno de los cuatro, en el repositorio de paquetes de Debian, al momento de este escaneo. Status: affected/fix_deferred confirma lo mismo desde otro ángulo: Debian sabe de estas vulnerabilidades, y todavía no publicó (o decidió postergar) el parche. Esto no es un error del Dockerfile de Andes Cargo — es el estado real, público, de ese paquete específico en ese momento exacto. La lección 8 de este módulo retoma esta tabla para tomar una decisión de gate real sobre ella.


Los hallazgos en las dependencias de Python

trivy image --format json andes-cargo-status-api:latest | \
  python3 -c "
import json, sys
data = json.load(sys.stdin)
for r in data.get('Results', []):
    if r.get('Target') == 'Python':
        for v in r.get('Vulnerabilities', []) or []:
            print(v['Severity'], v['PkgName'], v['VulnerabilityID'], v['InstalledVersion'], '->', v.get('FixedVersion'))
"

Qué esperar (real de esta ejecución — CVE y severidad exactas varían con la fecha del escaneo, la lista de paquetes de andes-cargo-status-api no):

LOW    Flask       CVE-2025-47278 3.1.0   -> 3.1.1
LOW    Flask       CVE-2026-27205 3.1.0   -> 3.1.3
HIGH   msgpack     GHSA-6v7p-g79w-8964 1.1.2 -> 1.2.1
HIGH   setuptools  CVE-2025-47273 70.3.0  -> 78.1.1
MEDIUM setuptools  CVE-2026-59890 70.3.0  -> 83.0.0

A diferencia de los cuatro CRITICAL de perl-base, estos tienen Fixed Version disponible en cada fila — actualizar Flask a 3.1.3, msgpack a 1.2.1, y setuptools a 83.0.0 (en requirements.txt, sin tocar el Dockerfile) resolvería los cinco de una sola vez. Esta es, en la práctica de un equipo real, la categoría de hallazgo más accionable: dependencias de aplicación, con parche ya publicado, que solo esperan que alguien actualice una versión y reconstruya la imagen.


Errores comunes

Confundir los 179 hallazgos de Debian con "vulnerabilidades del código de Andes Cargo" (de atribución equivocada). Qué pasa: alguien ve Total: 179 bajo el nombre de la imagen y concluye que el equipo de Andes Cargo escribió código inseguro. Cómo detectarlo: si tu reacción es "¿qué hicimos mal en 179 lugares?". Cómo corregirlo: mira la columna Library de la tabla completa —apt, bash, coreutils, bsdutils, perl-base— ninguno de esos paquetes es código de Andes Cargo; son parte del sistema operativo base (Debian 13.6) que cualquier imagen construida sobre esa base hereda. Los únicos 5 hallazgos que se relacionan directamente con decisiones del equipo (qué versión de Flask, boto3, etc. declarar) están en la sección Python, separada explícitamente en el resumen.

Esperar que trivy image reconstruya o modifique la imagen (de confusión con otro tipo de herramienta). Qué pasa: alguien corre trivy image andes-cargo-status-api:latest y espera que, al terminar, la imagen local tenga los parches aplicados automáticamente. Cómo detectarlo: si buscas, después del escaneo, algún cambio en docker images o en el tamaño de la imagen. Cómo corregirlo: trivy image es, estrictamente, de solo lectura — inspecciona capas, compara versiones instaladas contra bases de datos de CVE, e imprime un reporte. Nunca modifica la imagen escaneada. Corregir un hallazgo real (por ejemplo, actualizar Flask en requirements.txt) es un paso separado, manual, que el equipo decide hacer después de leer el reporte — exactamente el mismo patrón que cloud-security-and-guardrails-guide ya estableció con trivy config en su Módulo 5, lección 6 ("Fixing, suppressing, o accepting a finding").

Asumir que Fixed Version vacío significa "Trivy no pudo determinarlo" (de lectura incorrecta del campo). Qué pasa: alguien ve Fixed Version vacío en la tabla de perl-base y asume que es un límite de la herramienta, no del estado real del paquete. Cómo detectarlo: si tu conclusión es "hay que usar otra herramienta para ver la versión corregida". Cómo corregirlo: un Fixed Version vacío, junto con Status: affected o fix_deferred, significa que Debian mismo todavía no publicó una versión corregida para ese paquete específico en ese repositorio — no es un límite de Trivy, es información real y verificable sobre el estado del ecosistema del que la imagen depende. Trivy reporta el estado tal cual existe; no lo inventa ni lo completa.


Ejercicios

Ejercicio 1 — Clasifica cinco hallazgos hipotéticos por si son accionables de inmediato. Sin correr ningún comando nuevo, para cada uno de estos hallazgos hipotéticos, decide si un equipo podría resolverlo hoy mismo actualizando una versión: (a) Fixed Version: 2.4.1, Status: fixed; (b) Fixed Version: (vacío), Status: affected; (c) Fixed Version: 1.9.0, Status: fixed; (d) Fixed Version: (vacío), Status: fix_deferred.

Ver solución

(a) y (c) sí son accionables de inmediato — ambos tienen una versión corregida publicada (Status: fixed con Fixed Version no vacío); un equipo puede actualizar esa dependencia hoy mismo y resolver el hallazgo. (b) y (d) no son accionables de inmediato — ninguno tiene una versión corregida disponible todavía (Fixed Version vacío), sin importar si el estado es affected o fix_deferred; la única opción real, hasta que el mantenedor del paquete publique un parche, es documentar el riesgo como aceptado (el mismo patrón de cloud-security-and-guardrails-guide, Módulo 5, lección 6) o buscar una mitigación distinta (una imagen base diferente, por ejemplo). Si clasificaste los cuatro correctamente, tienes el criterio central de esta lección: Fixed Version vacío no es un problema del equipo, es un límite del ecosistema.

Ejercicio 2 — Explica por qué trivy config de cloud-security-and-guardrails-guide no habría encontrado ninguno de los 184 hallazgos de esta lección. En una frase, explica por qué correr trivy config sobre el repositorio andes-cargo-k8s (los manifiestos YAML de esta guía) nunca habría mostrado ninguno de los hallazgos de perl-base o Flask que viste en esta lección.

Ver solución

Porque trivy config lee texto de configuración —los propios archivos .yaml/.tf en disco— y busca patrones de configuración insegura declarada ahí mismo; nunca abre, descarga, ni inspecciona el contenido real de ninguna imagen Docker referenciada por un campo image: de un manifiesto. Los 184 hallazgos de esta lección viven dentro de las capas del filesystem de andes-cargo-status-api:latest —paquetes de Debian instalados, dependencias de Python instaladas—, un artefacto completamente distinto de cualquier archivo YAML que lo referencie por nombre.

Ejercicio 3 — Diseña el comando que resolvería, de una sola vez, los 5 hallazgos de Python. Basándote en la tabla de hallazgos de Python de esta lección, ¿qué cambio exacto (sin ejecutar nada todavía) resolvería los cinco hallazgos de una sola vez, sin tocar el Dockerfile?

Ver solución

Actualizar requirements.txt (o el archivo de dependencias equivalente) fijando Flask>=3.1.3, msgpack>=1.2.1 y setuptools>=83.0.0 —o simplemente relajando los pines de versión para que pip install tome la última disponible de cada uno—, y después reconstruir la imagen con docker build (el mismo Dockerfile de la lección 7 del Módulo 1, sin ningún cambio en su contenido, solo en las versiones que requirements.txt fija). Ninguno de los tres requiere cambiar la imagen base de Debian ni el código de la aplicación — los tres son actualizaciones de dependencias de terceros, con parche ya publicado por sus respectivos mantenedores.


Resumen y siguiente paso

Esta lección corrió trivy image andes-cargo-status-api:latest de verdad, contra la imagen que corre en andes-cargo-cluster desde el Módulo 1, y encontró 184 hallazgos reales: 179 heredados del sistema operativo base (Debian 13.6, ninguno atribuible al código de Andes Cargo), y 5 en dependencias de Python declaradas por el equipo, cada uno de estos últimos con una versión corregida ya publicada. Viste, con el diagrama de esta lección, por qué trivy image es un comando completamente distinto de trivy config (el que cloud-security-and-guardrails-guide usó): uno inspecciona el contenido real de una imagen ya construida; el otro lee texto de configuración, sin tocar ninguna imagen.

Antes de avanzar deberías poder: explicar la diferencia entre trivy config y trivy image en una frase; leer una tabla de hallazgos de Trivy e identificar cuáles son accionables hoy (columna Fixed Version no vacía) frente a cuáles no; y explicar por qué la mayoría de los hallazgos de una imagen real no son culpa del código propio, sino del sistema operativo base heredado.

Siguiente lección: proyecto — los guardrails de runtime de Andes Cargo. Ahí vas a activar Gatekeeper y Kyverno a la vez sobre andes-cargo, documentar honestamente qué pasa cuando los dos evalúan el mismo objeto, y usar el hallazgo CRITICAL de esta lección como la base real de un gate de trivy image que corre antes de cargar cualquier imagen nueva en el clúster.

Recursos

  1. Trivy — Container Image — documentación oficial de trivy image, el comando central de esta lección.
  2. Trivy — Config Scanning — documentación oficial de trivy config, contrastado aquí con trivy image.
  3. cloud-security-and-guardrails-guide (NIEVA), Módulo 5, lección 6 — el patrón de "fix, suppress, o accept" para un hallazgo sin parche disponible, aplicable a los cuatro CRITICAL de perl-base de esta lección.
  4. Debian Security Tracker — la fuente pública de Status: affected/fix_deferred para los paquetes del sistema operativo base.