# Trivy — vulnerabilidades de imagen e IaC !!! info "Qué problema resuelve" Trivy es un escáner de seguridad multipropósito. En este pipeline se usa para dos cosas **distintas**, con dos comandos distintos: - `trivy image`: busca CVEs conocidas en los paquetes que terminan dentro de la imagen Docker final (el sistema operativo base, y las librerías de Node.js instaladas). - `trivy config`: busca **misconfiguraciones** en los manifiestos de Kubernetes (YAML) — no vulnerabilidades de código, sino configuración insegura (contenedor como root, sin límites de recursos, etc.). Son preguntas distintas: "¿esta imagen tiene código con bugs de seguridad conocidos?" contra "¿esta manera de desplegar el contenedor es insegura, aunque el código adentro esté perfecto?". ## Dónde corre cada uno en el pipeline ```mermaid flowchart LR A[checkout] --> B["trivy config
(frontend.yaml)"] B --> C[docker login] C --> D["docker build
(push: false, load: true)"] D --> E["trivy image --severity CRITICAL
--exit-code 1"] E -- "CRITICAL con fix" --> X["❌ pipeline detenido
no se sube la imagen"] E -- "sin CRITICAL" --> F["trivy image --severity HIGH
--exit-code 0 (informativo)"] F --> G["docker push"] ``` El escaneo de imagen corre **después de construir la imagen, antes de subirla al registry** — por eso el build ahora usa `push: false, load: true` (la imagen queda en el daemon Docker del runner, pero no sale de ahí hasta pasar el gate de CRITICAL). El escaneo de manifiestos (`trivy config`) no depende de la imagen, así que corre antes, junto a las otras validaciones del código. ## Umbral de severidad (y por qué) | Severidad | Comportamiento | Por qué | |---|---|---| | `CRITICAL` | Bloquea (`--exit-code 1`) | Si existe un fix disponible para una CVE crítica, no tiene sentido publicar la imagen igual | | `HIGH` | Informa, no bloquea (`--exit-code 0`) | Mientras aprendemos a leer los reportes, HIGH se revisa pero no frena el flujo — bloquear de entrada en HIGH hubiera parado el pipeline en el primer intento real (ver ejemplo abajo) | Ambos steps usan `--ignore-unfixed`: si Trivy no tiene un `FixedVersion` para reportar, bloquear o hasta advertir no ayuda en nada — no hay acción posible más que esperar a que el mantenedor del paquete publique un parche. !!! tip "Por qué no escanear todo junto con un solo umbral" Trivy permite pedir `--severity CRITICAL,HIGH` en una sola corrida, pero el `--exit-code` se aplica igual a toda la corrida — no se puede decir "bloqueá en CRITICAL, pero en HIGH solo avisá" en un solo comando. Por eso son dos steps separados, cada uno con su propio umbral y su propio `--exit-code`. ## Ejemplo real: un CRITICAL que sí bloqueaba Antes de integrar este step, construimos la imagen real de `workloads/ecommerce` y corrimos Trivy contra ella para validar el pipeline. Encontró esto: ```text Node.js (node-pkg) Total: 1 (CRITICAL: 1) tar (7.5.15 → 7.5.19) CVE-2026-59873 CRITICAL tar: node-tar: Denial of Service via crafted gzip bomb ``` **El detalle importante no era el CVE en sí, sino dónde vivía:** `/usr/local/lib/node_modules/npm/node_modules/tar/` — ese `tar` no es una dependencia de ARI Shopping, es el que trae **empaquetado el propio `npm`** dentro de la imagen base `node:24.18.0-bookworm-slim`. El `Dockerfile` original hacía `FROM ${NODE_IMAGE} AS runner` para el stage final, heredando el Node.js completo — con `npm`, `npx` y `corepack` incluidos — aunque en producción el contenedor solo ejecuta `node server.js` y **nunca** invoca `npm`. !!! danger "No se arregla solo subiendo la versión de Node" Antes de tocar el Dockerfile probamos si un patch más nuevo de la imagen base ya traía el `tar` corregido: `node:24.19.0-bookworm-slim` trae `npm` con `tar@7.5.16` — sigue por debajo del `7.5.19` con el fix. El problema no es "Node desactualizado", es que el runtime de producción no necesita `npm` para nada. **Fix aplicado** (`workloads/ecommerce/Dockerfile`, stage `runner`): ```diff + RUN rm -rf \ + /usr/local/lib/node_modules/npm \ + /usr/local/lib/node_modules/corepack \ + /usr/local/bin/npm \ + /usr/local/bin/npx \ + /usr/local/bin/corepack ``` Después del fix: **0 CRITICAL**, y de paso las HIGH fixable bajaron de 21 a 15 (varias venían de dependencias de ese mismo `npm` empaquetado, no de la app). Se validó que la imagen sigue arrancando y respondiendo `GET /api/health` con `200` después de sacar `npm`. **Lección:** sacar herramientas que la imagen de producción no necesita en runtime no es solo "buena práctica" en abstracto — reduce directamente la superficie que Trivy (y un atacante) tienen para encontrar algo. ## Las HIGH que quedan (ejemplo real, sin arreglar todavía) Después del fix, `trivy image --severity HIGH` sigue reportando (de forma informativa, no bloqueante) CVEs reales en dependencias que sí son de la app — la mayoría en `next` (15.5.10, con fixes disponibles en 15.5.16+ y 15.5.21+ según el CVE), además de `nanoid`, `postcss` y `sharp`. Esto queda pendiente de revisar como una actualización de dependencias normal, no como una emergencia de seguridad — es exactamente para eso que HIGH no bloquea en esta primera vuelta: da visibilidad sin frenar el flujo mientras se decide cuándo priorizar el bump. ## Cómo priorizar qué arreglar primero 1. **CRITICAL con fix disponible** — ya bloquea el pipeline, así que en la práctica no se acumulan. 2. **HIGH en una dependencia que el runtime realmente carga** (como `next`, que corre en cada request) — más prioridad que una HIGH en una herramienta de build que ni siquiera llega a la imagen final. 3. **HIGH sin ruta de explotación realista** (ej. una librería que solo se usa en un script de generación, no en el server) — se puede posponer con criterio, documentando por qué. 4. **MEDIUM/LOW** — se revisan en lote, no una por una. La pregunta que más ayuda a priorizar no es "¿qué tan grave dice la CVSS que es?", sino "¿este paquete corre en el proceso que atiende tráfico real, o es una herramienta de build que ni siquiera debería estar en la imagen final?" — el propio ejemplo de arriba (`npm` dentro de la imagen de producción) es el caso de manual del segundo. ## Escaneo de manifiestos Kubernetes (`trivy config`) Corre contra `workloads/ecommerce/frontend.yaml` (el `Deployment` + `Service` real del frontend) con `--severity CRITICAL,HIGH,MEDIUM`, en modo informativo (`--exit-code 0`) por ahora. Hallazgos reales de este manifiesto, hoy: | Severidad | Regla | Qué significa | |---|---|---| | HIGH | [KSV-0014](https://avd.aquasec.com/misconfig/ksv-0014) | El filesystem raíz del contenedor no es de solo lectura | | HIGH | [KSV-0118](https://avd.aquasec.com/misconfig/ksv-0118) | No se define `securityContext` — Kubernetes usa el default, que permite privilegios de root | | MEDIUM | [KSV-0012](https://avd.aquasec.com/misconfig/ksv-0012) | El contenedor puede correr como root (aunque la imagen ya defina `USER nextjs` en el Dockerfile, Kubernetes no lo está *forzando* vía `runAsNonRoot`) | | MEDIUM | [KSV-0001](https://avd.aquasec.com/misconfig/ksv-0001) | El contenedor puede escalar sus propios privilegios (falta `allowPrivilegeEscalation: false`) | | MEDIUM | [KSV-0104](https://avd.aquasec.com/misconfig/ksv-0104) | No hay perfil de Seccomp configurado | | MEDIUM | [KSV-0117](https://avd.aquasec.com/misconfig/ksv-0117) | El `containerPort: 80` es un puerto privilegiado (<1024) | | MEDIUM | [KSV-0125](https://avd.aquasec.com/misconfig/ksv-0125) | La imagen viene de un registry que Trivy no reconoce como "de confianza" por defecto (es autoalojado: `gitea.cruzcloud.net`) | !!! warning "Lo que este scan NO detecta todavía" El pedido original incluía "falta de resource limits" y "falta de readiness/liveness probes" como ejemplos de misconfiguración a buscar. En la práctica, `trivy config` sí tiene reglas para límites de recursos (`KSV-0011` CPU, `KSV-0018` memoria) pero las clasifica como **LOW**, por debajo del piso `MEDIUM` que usa este step — y no tiene ninguna regla propia para probes de liveness/readiness (eso lo cubren otras herramientas, como `kube-score` o `kube-linter`, que no forman parte de esta primera integración). Si más adelante se quiere cubrir ese hueco específico, es una herramienta aparte, no una opción de configuración de Trivy. Ninguno de estos hallazgos bloquea el pipeline todavía — son reales, pero corregirlos (agregar `securityContext`, `resources.limits`, etc. a `frontend.yaml`) es un cambio de GitOps que conviene revisar con calma, no como reacción automática a un scan. ## Falsos positivos y excepciones Cuando un hallazgo de Trivy no aplica (por ejemplo, KSV-0125 marcando el registry propio como "no confiable" — que es exactamente lo esperado en un lab self-hosted), se documenta con un archivo `.trivyignore` en la raíz del repo: ```text # KSV-0125: gitea.cruzcloud.net es nuestro registry self-hosted, # no un registry público de terceros. Excepción intencional. KSV-0125 ``` Igual que con Gitleaks, cada línea de `.trivyignore` debe poder justificarse — no es un lugar para silenciar hallazgos incómodos sin revisarlos primero.