
🌐 También en: English · Deutsch · Français
Deep Dive de la serie en torno al Ansible Tour. Nivel: administración Linux avanzada. El Tour explica los roles; los deep dives explican la tecnología que hay debajo – cómo funciona, por qué está aquí, qué hace exactamente esta instalación con ella, cómo se opera en el día a día y qué otras opciones habría habido.
Cada imagen de esta instalación está fijada a una versión exacta. Eso es bueno para la reproducibilidad y malo para la seguridad en un punto concreto: una imagen fijada no mejora por sí sola. Los paquetes Debian o Alpine que lleva dentro envejecen, la versión de Go con la que se compiló un binario envejece, y cada día se publican vulnerabilidades nuevas para software que no ha cambiado. El host recibe sus actualizaciones de seguridad cada noche con dnf-automatic; las imágenes no recibían nada – hasta que alguien miraba. Ahora Trivy mira cada noche. Este artículo explica cómo encuentra vulnerabilidades, cómo corre en el blog desde el 19 de octubre de 2026, qué encontró el primer escaneo – 241 hallazgos corregibles – y por qué ese número es mucho menos alarmante de lo que parece una vez ordenado.
🧭 Cómo funciona Trivy
Primero un inventario, luego una base de datos
Trivy no ejecuta ni ataca nada. Lee una imagen capa por capa y hace un inventario: qué sistema operativo y versión (Debian 13, Alpine 3.21, Ubuntu 22.04 …), qué paquetes en qué versiones exactas según la base del gestor de paquetes, y qué dependencias están compiladas en binarios o listadas en ficheros de bloqueo – módulos Go leídos de la información de compilación que lleva cada binario Go, npm, pip, Composer, Java, etc. Después compara ese inventario con su base de vulnerabilidades.
El proyecto Trivy construye esa base a partir de muchas fuentes – los avisos de seguridad de las distribuciones, la GitHub Advisory Database, la base de vulnerabilidades de Go y otras – y la distribuye como un único fichero de unos cientos de megabytes. Para cada paquete sabe qué versiones están afectadas por qué CVE y, si existe, en qué versión se corrige.
Gravedad y «versión corregida» – los dos campos que importan
- La gravedad (CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN) viene de la fuente – a menudo la valoración de la propia distribución, si no una puntuación CVSS. Describe la vulnerabilidad en general, no su riesgo en esta imagen. Un fallo CRITICAL en una biblioteca que la imagen nunca usa sigue apareciendo como CRITICAL.
- La versión corregida (Fixed Version) indica si existe un arreglo. Sin él, solo queda aceptar el hallazgo o sustituir el software. Con él, la pregunta pasa a ser: ¿quién tiene que entregarlo – la distribución (ya hecho), el mantenedor de la imagen (hace falta reconstruirla) o el proyecto original (hace falta una versión nueva)?
Esa distinción es la clave de todo lo que sigue. Trivy informa de lo que es conocido; no puede saber si el código vulnerable es alcanzable. Leer los resultados es siempre un segundo paso que hace una persona.
De dónde sale la imagen
Trivy puede leer una imagen desde un registro (remote), desde un demonio Docker o Podman local, desde un fichero tar – o directamente desde containerd, el runtime que usa K3s. Esta última opción significa: sin segunda descarga, el escaneo ve exactamente la imagen que ejecutan los pods. Aquí necesita el socket y el namespace de K3s:
CONTAINERD_ADDRESS=/run/k3s/containerd/containerd.sock \
CONTAINERD_NAMESPACE=k8s.io \
trivy image --image-src containerd docker.io/library/mariadb:10.11.19🛡️ Por qué está aquí
- Las imágenes fijadas envejecen en silencio. El refresco de imágenes de los playbooks de actualización trae imágenes reconstruidas con la misma etiqueta – pero nada nos decía cuándo merecía la pena una reconstrucción, ni cuándo una imagen nunca iba a recibirla.
- El host estaba cubierto, los contenedores no. dnf-automatic parchea el host cada noche; las 14 imágenes que forman el clúster estaban fuera de ese circuito.
- Las decisiones necesitan datos. «¿Cambiamos este exportador?» o «¿Es urgente una actualización de K3s?» se responden mejor con la lista de lo que lleva realmente cada imagen.
🔧 Qué hace este repositorio
Un temporizador en el host, no un operador en el clúster
La forma habitual de usar Trivy en Kubernetes es el Trivy Operator: vigila el clúster, lanza un job de escaneo por carga de trabajo y guarda los resultados como recursos personalizados. En un único nodo pequeño eso supondría CRD, un controlador, pods de escaneo – y para cada uno, excepciones en la política de imágenes y en las NetworkPolicies que aplica esta instalación. El rol common_trivy hace el mismo trabajo con un binario y un script: un temporizador de systemd lanza trivy_scan.py cada noche a las 05:15, después de las actualizaciones de seguridad (01:00–02:00), de la ventana de reinicio (03:30) y de los demás trabajos nocturnos.
El binario: fijado y con suma de comprobación
Trivy se instala desde el archivo de la release de GitHub, con la versión y su SHA-256 fijados en versions.yml. Esa segunda fijación tiene una razón concreta: en marzo de 2026 la propia cadena de publicación de Trivy fue comprometida y distribuyó durante un tiempo una compilación maliciosa (aviso GHSA-69fq-xp46-6×23). Un escáner de seguridad corre como root y lo lee todo – justo la herramienta de la que uno quiere estar seguro. La regla de las notas: actualizar solo a una release con al menos dos semanas de antigüedad y copiar la suma de su fichero de checksums. El get_url de Ansible rechaza un archivo cuyo hash no coincide.
Dos pequeñas cosas con las que tropezó la instalación, ambas intencionadas en otro sitio: /tmp está montado noexec por el endurecimiento CIS, así que el archivo se descomprime desde /root; y tras descomprimirlo, restorecon da al binario su etiqueta SELinux correcta – un fichero escrito a través de una ruta temporal conserva si no la etiqueta de esa ruta, la misma lección que con el binario de k3s.
El script
- Actualizar la base de vulnerabilidades una vez (
--download-db-only); todos los escaneos de imágenes corren después con--skip-db-update. - Listar las imágenes de todos los pods en marcha. Los pods terminados de Jobs y CronJobs quedan fuera – la primera versión los incluía, y un job helm-install que corrió una vez al arrancar el clúster aportaba 32 hallazgos de una imagen que no está en marcha.
- Escanear cada imagen desde containerd. Si falla, segundo intento desde el registro (ver más abajo). Un informe JSON por imagen en
/var/lib/trivy/reports. - Escribir métricas para el colector textfile de node_exporter:
trivy_image_vulnerabilities{image, severity, fixable},trivy_image_scan_ok,trivy_db_update_ok,trivy_last_run_timestamp_seconds. El fichero se escribe con un nombre temporal y se renombra, para que node_exporter nunca lea medio fichero. - Comparar los hallazgos CRITICAL y HIGH corregibles con la noche anterior y enviar por correo solo los nuevos.
Por qué un correo para hallazgos nuevos y no una alerta
El diseño obvio sería una alerta de Grafana «vulnerabilidad crítica con arreglo disponible». El primer escaneo mostró por qué sería un error: la imagen de MariaDB contiene gosu, una pequeña utilidad escrita en Go, compilada con una versión antigua de Go cuya biblioteca estándar tiene una vulnerabilidad CRITICAL. Arreglarla corresponde a los mantenedores de la imagen de MariaDB. Una alerta saltaría, se repetiría cada cuatro horas y seguiría así durante días o semanas – hasta que se ignorara, y con ella todas las demás. El correo lo dice una vez. Un hallazgo que se queda no vuelve a enviar correo; uno que desaparece se cuenta como «resuelto». Grafana solo alerta cuando el propio escaneo está roto: falla la actualización de la base o el escaneo de una imagen, o la última ejecución correcta tiene más de 36 horas.
Aceptar un hallazgo
Algunos hallazgos se aceptarán a propósito. Van a la variable trivy_ignore, cada uno con una justificación obligatoria, y acaban en un fichero .trivyignore que usa el escaneo. La lista está vacía a propósito: cada entrada es una decisión, no un valor por defecto.
SELinux y el colector textfile
node_exporter corre en su propio dominio SELinux desde el mismo día (tras una fase de aprendizaje permisiva, en enforcing desde el 20 y 21 de octubre). Su directorio textfile /var/lib/node_exporter/textfile recibió un tipo propio, homelab_node_exporter_textfile_t, con exactamente dos reglas: el exportador puede listar el directorio y leer los ficheros. El escaneo, como root, los escribe; los ficheros nuevos heredan el tipo del directorio.
🔍 Lo que encontraron las primeras ejecuciones – sobre la propia herramienta
Capas que containerd ya había tirado
Dos de las 15 imágenes fallaron en la primera ejecución: metrics-server y kube-state-metrics, con «content digest … not found». containerd descomprime las capas de la imagen en snapshots y puede descartar después los blobs comprimidos; la imagen sigue funcionando, pero ya no se puede exportar entera. Trivy ofrece --image-src containerd,remote como lista de respaldo – que no sirvió, porque solo recurre a la siguiente fuente cuando la imagen falta, no cuando falta una capa. El script reintenta ahora explícitamente con remote para esas imágenes.
Un AVC, encontrado por las alertas de logs del mismo día
La primera versión de la unidad de escaneo tenía NoNewPrivileges=yes. El script llama a k3s kubectl para listar los pods, y el binario de k3s pasa al dominio SELinux container_runtime_t. Con NoNewPrivileges, SELinux rechaza esa transición si la política no la permite explícitamente, y registra avc: denied { nnp_transition }. La nueva alerta de Loki para denegaciones reales de SELinux saltó en pocos minutos. El ajuste se ha retirado; la unidad conserva las demás restricciones de systemd (/usr y /etc de solo lectura, sin directorios personales, /tmp privado, la prioridad de CPU y E/S más baja, límite de 2 GB de memoria).
📊 Leer 241 hallazgos
Tras las correcciones, el escaneo informaba de 209 hallazgos CRITICAL/HIGH corregibles en 14 imágenes en marcha. Ordenados por quién puede arreglarlos, el panorama cambia por completo:
| Grupo | Hallazgos | Quién puede arreglarlo | Acción |
|---|---|---|---|
| php-fpm_exporter 2.2.0 | 72 (5 de los 6 CRITICAL) | Nadie: última release en 2022, todavía con OpenSSL 1.1 | Sustituir, reconstruir o retirar – una decisión, ver abajo |
| MariaDB 10.11.19 | 26 | Mantenedores de la imagen: ya hay una reconstrucción publicada con la misma etiqueta (libssl3) | Refresco de imagen en el playbook de actualización; gosu se queda hasta que lo reconstruyan |
| WordPress 7.1.3 (php8.4-fpm) | 32 | Debian ha corregido curl, ghostscript, expat; la imagen oficial no se ha reconstruido desde entonces | Esperar – el refresco tomará la próxima reconstrucción automáticamente |
| nginx, Redis (Alpine) | 4 | Igual: arreglos en Alpine, imagen aún sin reconstruir | Esperar |
| Binarios Go: CoreDNS, metrics-server, cert-manager, kube-state-metrics, mysqld_exporter | 75 | Proyectos originales: versiones nuevas con un Go y módulos más recientes | Ya en las últimas versiones; los que vienen con K3s llegarán con el próximo K3s |
Tres lecciones de esa tabla:
- La mayoría de los hallazgos no nos piden nada. Están arreglados en algún punto de origen y llegarán con la próxima reconstrucción o versión, que los playbooks de actualización ya recogen.
- Una imagen es un problema real. Un exportador sin release desde hace cuatro años, que aún lleva OpenSSL 1.1, acumula casi un tercio de todos los hallazgos y casi todos los CRITICAL. Su exposición real es pequeña – solo habla FastCGI con PHP-FPM dentro del mismo pod y sirve métricas dentro del clúster, tras NetworkPolicies –, pero «poca exposición» no es motivo para conservar para siempre software abandonado.
- «Corregido» no significa «relevante». La imagen de WordPress incluye
linux-libc-dev, las cabeceras del kernel – un contenedor usa el kernel del host, las cabeceras solo sirven para compilar. Candidato paratrivy_ignore, con exactamente esa justificación.
Una pregunta llegó enseguida: ¿hay una imagen de WordPress más reciente que arregle sus 32 hallazgos? No. WordPress 7.1.3 es la versión actual, y la imagen 7.1.3-php8.4-fpm del blog tiene el mismo digest que la del registro. Debian ha publicado los paquetes corregidos, pero la imagen oficial no se ha reconstruido desde entonces. Construir una imagen propia con un apt-get upgrade adicional lo arreglaría hoy – al precio de mantener para siempre una compilación de imagen. Esperar unos días a la reconstrucción oficial es el mejor trato.
🧰 Administración en el día a día
# ¿cuándo corrió, cuánto tardó, cuánta memoria usó?
systemctl list-timers trivy-scan.timer
journalctl -u trivy-scan --since today
# lanzarlo ahora (oneshot, vuelve cuando termina)
systemctl start trivy-scan.service
# la línea de resumen de la última ejecución
journalctl -u trivy-scan -n 20 | grep "images scanned"
# qué hallazgos están abiertos (imagen, CVE, paquete, instalado -> corregido)
python3 -c 'import json; [print(v["severity"], v["id"], v["pkg"], v["installed"], "->", v["fixed"], v["image"]) for v in json.load(open("/var/lib/trivy/last-findings.json")).values()]' | sort | less
# una imagen en detalle
trivy image --image-src containerd --skip-db-update docker.io/library/redis:7.4.11-alpineEn Grafana (Explore → Prometheus) se pueden consultar las mismas cifras:
# CRITICAL corregibles por imagen
trivy_image_vulnerabilities{severity="CRITICAL", fixable="true"} > 0
# total de HIGH+CRITICAL corregibles en el tiempo: ¿baja tras una actualización?
sum(trivy_image_vulnerabilities{severity=~"CRITICAL|HIGH", fixable="true"})
# antigüedad del último escaneo en horas
(time() - trivy_last_run_timestamp_seconds) / 3600Qué hacer con un correo
- ¿Se ha reconstruido la imagen en origen con la misma etiqueta? Entonces la arregla la próxima actualización (refresco de imagen) – o se lanza ya el playbook de actualización.
- ¿Hay una versión más reciente? Entonces se sube la versión fijada en
versions.yml, mejor a una que tenga unos días. - ¿Ni una cosa ni otra? Entonces es un hallazgo que aceptar (con justificación en
trivy_ignore) o un motivo para sustituir el software.
Coste
La base de vulnerabilidades y la caché de capas ocupan unos 1,6 GB en /var/cache/trivy. Una ejecución nocturna completa, con la descarga de la base, tarda menos de medio minuto de CPU en el blog; las siguientes reutilizan el análisis de capas en caché y duran unos segundos. El escaneo corre con la prioridad de CPU y E/S más baja y un límite de 2 GB de memoria, así que no puede frenar los sitios.
🆘 Chuleta: síntoma → comando
La versión corta de este artículo para el momento en que algo va mal. Ejecutar en el host como root; <…> son marcadores.
| Síntoma | Comando |
|---|---|
| ¿Se ejecutó el escaneo? | systemctl list-timers trivy-scan.timer |
| Ejecutarlo ahora | systemctl start trivy-scan.service |
| ¿Qué hay abierto ahora? | python3 -c 'import json; [print(v["severity"], v["id"], v["pkg"], v["image"]) for v in json.load(open("/var/lib/trivy/last-findings.json")).values()]' | sort |
| Una imagen en detalle | trivy image --image-src containerd --skip-db-update <image>:<tag> |
| Un correo sobre un hallazgo CRITICAL | # 1. rebuilt upstream under the same tag? -> next update run |
| Resultados del escaneo demasiado antiguos | (time() - trivy_last_run_timestamp_seconds) / 3600 |
| Espacio que ocupa la caché | du -sh /var/cache/trivy |
🔭 Lo que viene
- Una decisión sobre el exporter de PHP-FPM antes del 20 de enero de 2027, cuando caduca su aceptación: hay dos forks activos de una sola persona (uno con actualizaciones automáticas de dependencias); si no, prolongar la aceptación.
- Escaneo de configuración: probado una vez en octubre de 2026 contra los manifiestos en marcha del blog: 21 tipos de hallazgos, casi todos decisiones deliberadas (ingress en la red del host, volúmenes hostPath, imágenes que corren como root). Como job de CI necesitaría plantillas renderizadas y una larga lista de excepciones; de momento no compensa.
- El escaneo de secretos ya está en la CI: gitleaks revisa todo el historial de git en cada push, además del hook de pre-commit.
- Un pequeño panel con la evolución de los hallazgos abiertos por imagen.
⚖️ Alternativas – y por qué Trivy
| Opción | Punto fuerte | Por qué no aquí |
|---|---|---|
| Trivy Operator | Continuo, resultados como recursos de Kubernetes | Controlador, CRD y pods de escaneo en un único nodo pequeño; excepciones en la política de imágenes y las NetworkPolicies. |
| Grype (Anchore) | Rápido, buena integración SBOM con Syft | Comparable; Trivy lee containerd directamente y cubre más ecosistemas en un solo binario. |
| Escaneo en el registro (Harbor/Clair, Docker Scout, GitHub) | Escanea al hacer push | Aquí no hay registro propio – las imágenes llegan tal cual se publican desde Docker Hub, Quay y registry.k8s.io. |
| dnf updateinfo / OpenSCAP | Exacto para el host | Solo cubre los paquetes del host, no el contenido de las imágenes. |
| Trivy como temporizador nocturno en el host | Un binario, lee las imágenes en marcha desde containerd, métricas + correo | Elegido. |
El valor de la herramienta no está en el número que muestra, sino en la ordenación que permite. 241 hallazgos sonaban a emergencia. Ordenados por quién puede arreglarlos, se convirtieron en una decisión real, un refresco de imagen – y mucha espera por el origen, que la rutina de actualización existente ya gestiona.


