Escaneo de vulnerabilidades en Docker: qué confiar y qué es ruido

¿Te fue útil?

Ejecutaste un escaneo de contenedor y gritó: “327 vulnerabilidades, 14 críticas.”
Mientras tanto, el servicio es un binario Go pequeño, sin shell, sin gestor de paquetes y ni siquiera abre un puerto entrante.
El escáner entra en pánico de todos modos. Tu equipo de seguridad quiere un ticket. El de guardia quiere dormir.

El escaneo de vulnerabilidades de contenedores es valioso—cuando lo tratas como un panel de instrumentos, no como una profecía.
Así es como separas señal de ruido, decides qué arreglar y construyes un flujo de trabajo que sobreviva al contacto con producción.

Lo que los escáneres realmente saben (y lo que no)

La mayoría de los escáneres de vulnerabilidades para Docker funcionan inspeccionando el contenido de la imagen y comparando lo que encuentran con bases de datos de vulnerabilidades.
Suena sencillo hasta que recuerdas qué es una imagen de contenedor: un montón de archivos más metadatos por capas, construida desde un ecosistema que adora la abstracción.
Los escáneres no “entienden tu aplicación.” Inferen, comparan y suponen.

Para qué son buenos los escáneres

  • Detectar versiones de paquetes vulnerables conocidas en gestores de paquetes del SO (dpkg, rpm, apk) y en ecosistemas de lenguajes comunes.
  • Resaltar imágenes base obsoletas donde el upstream ya envió correcciones pero tú no has reconstruido.
  • Inventariar componentes (comportamiento similar a SBOM) para que dejes de discutir sobre qué “hay en la imagen”.
  • Encontrar errores evidentes: claves privadas embebidas, archivos con permisos globales de escritura, o un servidor SSH sorpresa—según la herramienta.

Donde los escáneres rutinariamente mienten (o al menos inducen a error)

  • Alcanzabilidad: una vulnerabilidad en una biblioteca que está presente pero nunca se carga igual será reportada.
  • Backports: las distribuciones empresariales parchean vulnerabilidades sin cambiar versiones de la forma que espera una comparación ingenua.
  • Ambigüedad de “arreglado en”: el escáner dice “arreglado en 1.2.3” pero tu distro distribuye “1.2.2-ubuntu0.4” con el parche backporteado.
  • Severidad sin contexto: CVSS asume un despliegue genérico. Tu contenedor puede ser non-root, sin red, con sistema de archivos de solo lectura, seccomp restringido. A CVSS no le importa.
  • Paquetes fantasma: dependencias de tiempo de compilación dejadas en la capa final porque alguien olvidó builds multietapa.

El escaneo de vulnerabilidades es un generador de señales necesario, no una calculadora de riesgo. El cálculo de riesgo es tu trabajo.
O, más precisamente: tu trabajo es crear un sistema que calcule el riesgo de manera consistente para que los humanos no improvisen a las 2 a. m.

Una cita que mantengo pegada en el tablero mental, de John Allspaw: “La automatización debe ser un multiplicador de fuerza, no una fuente de misterio.”

Hechos y contexto histórico (por qué existe este lío)

Si el escaneo de vulnerabilidades se siente como discutir con una hoja de cálculo muy segura de sí misma, es porque lo es. Algo de contexto te ayuda a predecir modos de fallo.

  1. CVE empezó en 1999 como un sistema de nombrado para que los vendedores pudieran hablar del mismo bug sin traducciones tribales.
  2. CVSS v2 (2005) y v3 (2015) estandarizaron la puntuación de severidad, pero sigue siendo un modelo abstracto que no ve tus mitigaciones en tiempo de ejecución.
  3. Los contenedores popularizaron la “infraestructura inmutable”, pero muchos equipos aún tratan las imágenes como VMs mutables: parchean dentro, envían, olvidan.
  4. Las distros backportean parches por política (familias Debian, Ubuntu, RHEL), lo que rompe la lógica simple “versión < versión_arreglada”.
  5. El ecosistema musl libc de Alpine cambió el panorama de dependencias; imágenes más pequeñas, pero diferente compatibilidad y a veces narrativas de vulnerabilidad distintas.
  6. Heartbleed (2014) enseñó a todos la lección: “simplemente actualiza OpenSSL” no es estrategia cuando ni siquiera sabes dónde está OpenSSL.
  7. Log4Shell (2021) metió a los SBOM en el mainstream corporativo porque la gente escaneaba todo para encontrar dónde vivía el jar.
  8. La Executive Order 14028 (2021) aceleró requisitos de SBOM y expectativas de “demuestra lo que hay adentro” en las compras públicas.
  9. Emergió VEX porque los SBOM sin contexto de “¿es explotable?” generan fatiga de alertas a escala industrial.

Hecho #10, no oficial pero dolorosamente real: los vendedores de herramientas de seguridad descubrieron que “más hallazgos” suele vender mejor que “hallazgos más precisos.”
Es una realidad de mercado, no una conspiración.

Un modelo práctico de confianza: tres capas de verdad

Cuando estás triageando hallazgos, necesitas una jerarquía. No por filosofía—para que dejes de debatir sin fin.

Capa 1: Qué hay en la imagen (verdad de inventario)

Esto es lo que los escáneres suelen hacer bien: enumerar paquetes, bibliotecas y a veces archivos.
La primera pregunta no es “¿es crítico?” La primera pregunta es “¿está realmente ahí?”

Señales de confianza:

  • Resultados derivados de metadatos del gestor de paquetes del SO (bases dpkg/rpm/apk) en lugar de adivinar por nombre de archivo.
  • Generación de SBOM con sumas de verificación e identificadores de paquetes (purl, CPE, identificadores SPDX).
  • Builds reproducibles o al menos Dockerfiles deterministas para que confirmes que el escaneo es sobre lo que crees.

Capa 2: ¿Es vulnerable en esa distro/ecosistema? (verdad de asesoría)

Un CVE no es una nota de parche. Es una etiqueta.
“Vulnerable” depende del estado del parche en la distro, backports, flags de compilación, características deshabilitadas y decisiones del proveedor.

Señales de confianza:

  • Rastreadores de seguridad de la distro y avisos de los proveedores referenciados por el escáner, no solo NVD.
  • Conciencia de backporting: mapeo por strings de release del paquete de la distro, no solo semver upstream.
  • Herramientas que distinguen “no arreglado”, “arreglado”, “afectado pero mitigado” y “no afectado”.

Capa 3: ¿Es explotable en tu runtime? (verdad de la realidad)

La explotabilidad es donde el escaneo para y empieza la operación:
exposición de red, privilegios, acceso de escritura al sistema de archivos, secretos en variables de entorno, endurecimiento del kernel, controles de egreso y qué hace realmente la ruta de código.

Señales de confianza:

  • Configuración de runtime conocida: IDs de usuario, capacidades de Linux, seccomp/apparmor, rootfs de solo lectura, no-new-privileges.
  • Exposición conocida: qué puertos son alcanzables, reglas de entrada, política de service mesh, restricciones de egreso.
  • Análisis de alcanzabilidad basado en evidencia (SCA con grafo de llamadas) para dependencias de lenguaje cuando sea viable.

Si tu pipeline trata la Capa 1 como si fuera la Capa 3, te ahogarás en alertas y aun así te explotarán por algo aburrido.

Categorías comunes de ruido (y cuándo dejan de ser ruido)

1) CVE “no arreglados” en paquetes de la imagen base que no usas

Ejemplo: tu imagen incluye libxml2 como dependencia transitiva, el escáner marca CVE, pero tu app nunca parsea XML.
Eso suele ser “riesgo tolerable”, no “ignorar para siempre.”

Cuando se vuelve real:

  • La biblioteca vulnerable la usan componentes del sistema que sí expones (nginx, curl, git, llamadas al gestor de paquetes).
  • El contenedor se ejecuta con privilegios o comparte namespaces del host.
  • La biblioteca se usa comúnmente por rutas inesperadas (p. ej., bibliotecas de procesamiento de imágenes vía carga de usuario).

2) Falsos positivos por backport

Debian y Ubuntu frecuentemente entregan parches sin incrementar la versión que coincida con la versión de corrección upstream.
Un escáner que solo compara versiones upstream dirá que eres vulnerable cuando no lo eres.

Cuando se vuelve real:

  • Estás en una distro sin cultura de backport, o usas binarios upstream.
  • El proveedor del paquete marca explícitamente que está afectado y sin arreglar.

3) CVE en dependencias de lenguaje sin ruta de código alcanzable

JavaScript, Java, Python—ecosistemas donde el grafo de dependencias puede ser un pequeño incendio forestal.
Los escáneres marcarán bibliotecas presentes en node_modules incluso si el código nunca se importa en runtime.

Cuando se vuelve real:

  • Envías todo el árbol de dependencias a producción (común en imágenes Node).
  • Hay carga dinámica/reflectiva que complica el análisis de alcanzabilidad.
  • Tienes entradas controladas por el usuario que podrían activar parsers poco comunes.

4) CVE que requieren acceso local de usuario, en un contenedor que se ejecuta como non-root

Muchos CVE de “escalada de privilegios local” siguen siendo relevantes si un atacante puede ejecutar código en tu contenedor.
Si ya tienen ejecución de código, ya estás teniendo un mal día.
Pero la ELP importa porque puede convertirse en escape del contenedor o movimiento lateral.

5) CVE del kernel reportados contra imágenes

Los contenedores comparten el kernel del host. Si un escáner reporta un CVE del kernel “en la imagen”, trátalo como informativo en el mejor de los casos.
Tu arreglo está en los nodos host, no en el Dockerfile.

Chiste #1: Las puntuaciones CVSS son como los pronósticos meteorológicos—útiles, pero si planeas una cirugía basándote en ellas, tendrás papeleo.

Un marco de triaje que funciona bajo presión

Aquí está el flujo de trabajo que quiero que los equipos usen. No es perfecto. Es consistente, y la consistencia vence a las hazañas heroicas.

Paso A: Confirma que escaneaste el artefacto correcto

Pensarías que esto es obvio. No lo es.
La gente escanea :latest, o escanea una compilación local mientras producción ejecuta un digest más antiguo.
Siempre vincula los hallazgos a un digest inmutable.

Paso B: Reduce a “explotable + expuesto + no mitigado”

La severidad no es una decisión; es una pista.
Tu decisión debe estar guiada por:

  • Superficie expuesta: ¿es el componente vulnerable alcanzable desde fuera del perímetro de confianza?
  • Madurez del exploit: ¿exploit público? ¿weaponizado? ¿solo teórico?
  • Privilegios: ¿puede llevar a root en el contenedor? ¿escape? ¿acceso a datos?
  • Mitigaciones: configuración que deshabilita la función, políticas de red, sandboxing, WAF, rootfs de solo lectura.
  • Tiempo para arreglar: ¿puedes reconstruir una imagen base hoy, o tomará un mes desenredarlo?

Paso C: Decide entre reconstruir, parchear o aceptar

  • Reconstruir cuando la corrección está en los paquetes upstream y solo necesitas una nueva imagen base.
  • Parchear cuando controlas el código y la vulnerabilidad está en tu árbol de dependencias.
  • Aceptar (con registro) cuando no es explotable, no está expuesto o está mitigado—y puedes probarlo (VEX ayuda).

Paso D: Prevenir recurrencia con higiene de build

La mayor parte del “ruido del escáner” es autoinfligido: imágenes gigantes, herramientas de build en capas de runtime, imágenes base antiguas, gestión indisciplinada de dependencias.
Arregla el pipeline y el ruido baja.

Tareas prácticas: comandos, salidas y decisiones (12+)

Estas son tareas prácticas que puedes ejecutar en una estación de trabajo o runner CI con Docker disponible.
Cada una incluye qué significa la salida y qué decisión tomar.

Tarea 1: Identificar el digest exacto de la imagen que estás escaneando

cr0x@server:~$ docker image inspect --format '{{.RepoTags}} {{.Id}}' myapp:prod
[myapp:prod] sha256:9d2c7c0a0d4e2fd0f2c8a7f0b6b1a1f2a3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8

Significado: El ID de imagen (digest) es inmutable. Escanea esto, no una etiqueta que puede moverse.

Decisión: Si tu escáner reporta hallazgos sin referencia a digest, trata el informe como “posiblemente artefacto equivocado” hasta probar lo contrario.

Tarea 2: Confirma qué está ejecutando producción (digest, no etiqueta)

cr0x@server:~$ docker ps --no-trunc --format 'table {{.Names}}\t{{.Image}}\t{{.ID}}'
NAMES          IMAGE                                                                 ID
myapp-prod     myregistry.local/myapp@sha256:2b1d...e9c3                             61c9f1c1f9d9b0a3b2c1d7b0e3a1c8f9d2e4b5a6c7d8e9f0a1b2c3d4e5f6a7b8

Significado: El contenedor en ejecución referencia un digest de tu registro. Si el digest difiere de lo que escaneaste, tu escaneo es irrelevante.

Decisión: Reescanea el digest en ejecución, o actualiza el despliegue para que coincida con el digest escaneado antes de abrir tickets de remediación.

Tarea 3: Chequeo rápido de inventario—¿qué SO tenemos?

cr0x@server:~$ docker run --rm myapp:prod cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
VERSION="12 (bookworm)"

Significado: El contexto de la distro importa. ¿Debian backports? Sí, a veces. ¿Alpine? Base de datos y cadencia de parches diferente.

Decisión: Configura los escáneres para usar feeds específicos de la distro cuando sea posible; no confíes solo en datos genéricos upstream de “arreglado en”.

Tarea 4: Contar paquetes en la imagen de runtime (predictor de ruido)

cr0x@server:~$ docker run --rm myapp:prod dpkg-query -W | wc -l
143

Significado: Más paquetes suelen significar más CVE. Una imagen de runtime con 143 paquetes puede estar bien, pero es una pista.

Decisión: Si este número es de cientos o miles para un servicio simple, planea un build multietapa o moverte a distroless; estás pagando impuesto de vulnerabilidades por conveniencia.

Tarea 5: Encontrar las capas más grandes (a menudo donde se hornearon herramientas de build)

cr0x@server:~$ docker history --no-trunc myapp:prod | head
IMAGE          CREATED        CREATED BY                                      SIZE      COMMENT
sha256:9d2c…   3 days ago     /bin/sh -c apt-get update && apt-get install…   312MB
sha256:1aa3…   3 days ago     /bin/sh -c useradd -r -u 10001 appuser          1.2MB
sha256:2bb4…   3 days ago     /bin/sh -c #(nop)  COPY file:app /app           24MB

Significado: Esa capa de 312MB huele a compiladores, headers y lo que sea que alguien instaló “temporalmente.”

Decisión: Separa etapas de build y runtime; elimina caches del gestor de paquetes; mantén el runtime mínimo para reducir la superficie de CVE.

Tarea 6: Ejecutar Trivy con valores por defecto útiles (y leerlo correctamente)

cr0x@server:~$ trivy image --severity HIGH,CRITICAL --ignore-unfixed myapp:prod
2026-01-03T10:12:41Z	INFO	Detected OS: debian
2026-01-03T10:12:41Z	INFO	Number of language-specific files: 1
myapp:prod (debian 12)
=================================
Total: 3 (HIGH: 2, CRITICAL: 1)

libssl3 3.0.11-1~deb12u2   (CRITICAL)  CVE-202X-YYYY  fixed in 3.0.11-1~deb12u3
zlib1g  1:1.2.13.dfsg-1    (HIGH)      CVE-202X-ZZZZ  fixed in 1:1.2.13.dfsg-2

Significado: --ignore-unfixed elimina elementos “sin parche” que a menudo reducen el ruido en las operaciones diarias.
Los hallazgos restantes son accionables porque existe una versión arreglada en el canal de la distro.

Decisión: Si existen versiones arregladas, reconstruye con paquetes base actualizados. Si no, evalúa explotabilidad y controles compensatorios; no bloquees releases por “sin parche” por defecto.

Tarea 7: Generar un SBOM y tratarlo como un artefacto

cr0x@server:~$ syft myapp:prod -o spdx-json > myapp.spdx.json
cr0x@server:~$ jq -r '.packages[0].name, .packages[0].versionInfo' myapp.spdx.json
base-files
12.4+deb12u5

Significado: Ahora tienes un inventario legible por máquina vinculado a una build de imagen.

Decisión: Almacena SBOMs junto a las imágenes (en almacenamiento de artefactos). Úsalos para comparar builds y responder “¿qué cambió?” en minutos, no en reuniones.

Tarea 8: Correlacionar hallazgos del escáner con el estado del paquete instalado

cr0x@server:~$ docker run --rm myapp:prod dpkg-query -W libssl3
libssl3	3.0.11-1~deb12u2

Significado: Confirma lo que está instalado, no lo que adivinó el escáner.

Decisión: Si la versión instalada ya incluye un backport del parche (el proveedor dice arreglado), marca la entrada del escáner como falso positivo y documenta (preferiblemente como VEX).

Tarea 9: Verificar si el contenedor se ejecuta como root (multiplicador de riesgo)

cr0x@server:~$ docker inspect --format '{{.Config.User}}' myapp:prod

Significado: Vacío significa que por defecto es root. Muchos exploits pasan de “molestos” a “catastróficos” cuando les das root en el contenedor.

Decisión: Si se ejecuta como root, arréglalo salvo que haya una razón fuerte. Añade USER 10001 (o similar) y maneja permisos de archivos adecuadamente.

Tarea 10: Validar capacidades de Linux en runtime

cr0x@server:~$ docker inspect --format '{{json .HostConfig.CapAdd}} {{json .HostConfig.CapDrop}}' myapp-prod
null ["ALL"]

Significado: CapDrop ["ALL"] es una medida de endurecimiento fuerte. Si ves capacidades añadidas (NET_ADMIN, SYS_ADMIN), el radio de daño aumenta.

Decisión: Si se añaden capacidades “por si acaso”, quítalas y prueba. Las capacidades deben tratarse como contraseñas root: emitidas con moderación.

Tarea 11: Confirmar configuración de sistema de archivos de solo lectura (evidencia de mitigación)

cr0x@server:~$ docker inspect --format '{{.HostConfig.ReadonlyRootfs}}' myapp-prod
true

Significado: Rootfs de solo lectura reduce la persistencia de exploits y bloquea muchas cadenas de ataque de escribir-luego-ejecutar.

Decisión: Si es false, considera ponerlo en true y montar solo las rutas necesitadas como escribibles (/tmp, caché de la app) como tmpfs/volúmenes.

Tarea 12: Identificar qué puertos están realmente expuestos/escuchando

cr0x@server:~$ docker exec myapp-prod ss -lntp
State  Recv-Q Send-Q Local Address:Port  Peer Address:Port Process
LISTEN 0      4096   0.0.0.0:8080       0.0.0.0:*       users:(("myapp",pid=1,fd=7))

Significado: Un listener en 8080. Esa es tu superficie remota principal de ataque.

Decisión: Si los hallazgos del escáner apuntan a componentes no involucrados en esta ruta (p. ej., SSH, librerías FTP), baja la urgencia a menos que exista una cadena de explotación interna.

Tarea 13: Detectar “shell presente” y otras herramientas de conveniencia (superficie de ataque)

cr0x@server:~$ docker run --rm myapp:prod sh -lc 'command -v bash; command -v curl; command -v gcc; true'
/usr/bin/bash
/usr/bin/curl
/usr/bin/gcc

Significado: Mucha herramienta para un contenedor de runtime. Genial para depurar. También genial para atacantes.

Decisión: Mueve las herramientas a una imagen de depuración o contenedor de caja de herramientas efímero. Mantén las imágenes de producción aburridas.

Tarea 14: Comparar dos imágenes para ver si la reducción de CVE es real

cr0x@server:~$ trivy image --severity HIGH,CRITICAL myapp:prod | grep -E 'Total:|CRITICAL|HIGH' | head
Total: 17 (HIGH: 13, CRITICAL: 4)
cr0x@server:~$ trivy image --severity HIGH,CRITICAL myapp:prod-slim | grep -E 'Total:|CRITICAL|HIGH' | head
Total: 4 (HIGH: 3, CRITICAL: 1)

Significado: La imagen slim redujo hallazgos de forma material. Ahora confirma funcionalidad y requisitos operativos (logging, certificados CA, archivos de zona horaria).

Decisión: Si la imagen slim pasa pruebas de integración y aún soporta observabilidad, promuévela. Si no, aprendiste qué dependencias realmente necesitas.

Tarea 15: Detectar riesgo de “reconstrucción obsoleta” (actualizaciones de imagen base no incorporadas)

cr0x@server:~$ docker pull debian:12-slim
12-slim: Pulling from library/debian
Digest: sha256:7a8b...cdef
Status: Image is up to date for debian:12-slim

Significado: Confirmaste que la imagen base local coincide con el estado actual del registro. En CI siempre debes hacer pull para evitar construir sobre capas obsoletas.

Decisión: Si CI cachea imágenes base sin refrescar, arregla eso. La remediación de vulnerabilidades a menudo es “reconstruir semanalmente”, no “parchear manualmente en un contenedor.”

Tarea 16: Verificar que no enviaste secretos de tiempo de compilación en capas

cr0x@server:~$ docker run --rm myapp:prod sh -lc 'grep -R --line-number -E "BEGIN (RSA|OPENSSH) PRIVATE KEY" / 2>/dev/null | head -n 3 || true'

Significado: No tener salida es bueno. Los escáneres a veces encuentran secretos; también puedes hacer una comprobación de cordura tú mismo.

Decisión: Si encuentras algo, trátalo como incidente: rota claves, reconstruye imágenes y arregla el Dockerfile/contexto de build inmediatamente.

Chiste #2: Lo único más persistente que una vulnerabilidad es una caché de capas con malas decisiones horneadas.

Tres mini-historias del mundo corporativo

Mini-historia 1: El incidente causado por una suposición errónea

Una empresa SaaS de tamaño medio ejecutaba escaneos nocturnos de contenedores y tenía una política simple:
“Si es Crítica, bloquear despliegue.” La intención era buena. La implementación fue contundente.

Un viernes, apareció un CVE crítico en una librería de compresión de uso general.
Su escáner lo marcó en docenas de imágenes—API, workers, cron jobs e incluso una herramienta de migración puntual.
La suposición: “Crítico significa explotable por internet.” Así que la respuesta de seguridad fue detener todos los despliegues.

Mientras tanto, el problema operativo real no estaba relacionado: una migración de esquema de base de datos necesitaba desplegarse para detener una regresión de rendimiento de larga duración.
Con los despliegues bloqueados, la regresión se volvió visible al cliente. Latencia subió. Reintentos se multiplicaron. Los costos aumentaron.

Tras un largo fin de semana, descubrieron que la librería “crítica” solo estaba presente en una capa de toolchain de build para varias imágenes, no en la etapa de runtime.
En otras, estaba instalada pero no era alcanzable desde sus servicios expuestos. La corrección correcta habría sido una reconstrucción programada y una excepción dirigida con justificación.

La lección no fue “ignorar CVE críticos.” La lección fue “no confundas etiquetas de severidad con explotabilidad en tu arquitectura.”
Reemplazaron la regla de bloqueo por una regla de triaje: bloquear solo cuando (1) exista versión arreglada, (2) el componente esté en la imagen de runtime, (3) sea alcanzable desde una ruta expuesta, o (4) el CVE se sepa que está siendo explotado activamente.

Mini-historia 2: La optimización que salió mal

Un equipo de plataforma interno empresarial se cansó de tiempos de build largos y del bloat en el registro.
Introdujeron caching agresivo de capas y fijaron imágenes base a digests en Dockerfiles para “asegurar reproducibilidad.”
Las builds se volvieron más rápidas. Los resultados del escáner fueron estables. Todos aplaudieron.

Tres meses después, una nueva ola de alertas de escaneo llegó—esta vez de auditores externos, no de su CI.
Los auditores escanearon imágenes en el registro y encontraron paquetes antiguos y vulnerables que hacía tiempo habían sido corregidos upstream.
Los escáneres del equipo de plataforma no los marcaron porque no estaban reconstruyendo, y su pipeline de escaneo solo corría sobre “imágenes cambiadas.”

La optimización había creado una trampa: fijar bases a digests hizo las builds reproducibles, pero también las hizo permanentemente obsoletas a menos que alguien actualizara el pin.
Su caching facilitó olvidar que los parches de seguridad se entregan mediante reconstrucciones.

La remediación no fue complicada, solo operativamente molesta:
añadieron reconstrucciones programadas (semanales para servicios expuestos a internet, mensuales para herramientas internas), más un bot de PR automático que actualiza digests de imágenes base y ejecuta pruebas de integración.
El cache se mantuvo—pero dejó de ser una excusa para nunca reconstruir.

La lección: las optimizaciones de rendimiento que reducen fricción también reducen retroalimentación. Si quitas el “dolor” de reconstruir, debes añadir una cadencia explícita de reconstrucción o tu postura de seguridad se pudre en silencio.

Mini-historia 3: La práctica aburrida pero correcta que salvó el día

Una fintech tenía una costumbre de la que nadie se jactaba: cada imagen construida en CI producía tres artefactos—
el digest de la imagen, un SBOM y una atestación firmada de las entradas de build (digest de imagen base, SHA de git, args de build).
No era emocionante. Era papeleo, automatizado.

Entonces cayó una vulnerabilidad de alto perfil en una librería usada por su gateway público.
El dashboard del escáner se iluminó, como suelen hacer los dashboards. El equipo de seguridad preguntó lo de siempre: “¿Estamos afectados?”
Históricamente, esa pregunta desencadenaba semanas de búsqueda en repositorios y discusiones sobre qué servicio usa qué.

Esta vez, el SRE de guardia sacó el SBOM para el digest en ejecución del gateway, buscó el componente y tuvo una respuesta en minutos:
sí, la librería vulnerable estaba presente, y sí, la función afectada estaba habilitada en su configuración.
También usaron atestaciones para confirmar qué builds la introdujeron y qué entornos ejecutaban esos digests.

Reconstruyeron la imagen base, hicieron rollout progresivo y usaron el mapeo por digest para confirmar que prod se actualizó realmente.
Sin conjeturas. Sin hilos de Slack “¿pero lo desplegamos, verdad?”. Solo una corrección controlada.

La lección: la diferencia entre pánico y progreso es la evidencia.
Los SBOM y las atestaciones no son teatro de seguridad cuando están integrados en operaciones y vinculados a digests inmutables.

Manual rápido de diagnóstico

Cuando tienes un informe de escáner y tiempo limitado, haz esto en orden. El objetivo es encontrar el cuello de botella en la toma de decisiones rápido.

1) Confirmar identidad del artefacto

  • ¿Tenemos un digest de imagen?
  • ¿Ejecuta producción ese digest?
  • ¿El timestamp del escaneo es posterior a la build de la imagen?

Por qué primero: La mitad del drama de escaneos son artefactos desajustados. Arregla eso y muchos problemas “urgentes” se evaporan o se apuntan correctamente.

2) Determinar disponibilidad de arreglo

  • ¿Existe una versión de paquete corregida en el repo de la distro?
  • ¿Existe una release upstream para la dependencia de lenguaje?
  • ¿El problema está marcado “sin parche” porque no existe solución?

Por qué segundo: “No existe parche” cambia la conversación de “parchar ahora” a “mitigar y monitorizar.”

3) Comprobar exposición y privilegios en runtime

  • ¿Qué puertos están escuchando? ¿Qué es alcanzable desde fuera?
  • ¿Se ejecuta como root? ¿Hay capacidades extra? ¿Contenedor privilegiado?
  • ¿Rootfs de solo lectura? ¿perfil seccomp? ¿no-new-privileges?

Por qué tercero: Si el runtime está endurecido y el componente vulnerable no está expuesto, tienes tiempo para reconstruir correctamente en vez de apresurar un hotfix arriesgado.

4) Eliminar dependencias de build de la imagen de runtime

  • ¿El CVE está en compiladores, headers, gestores de paquetes, shells?
  • ¿Son necesarios en producción?

Por qué: Los builds multietapa pueden eliminar clases enteras de hallazgos sin “parchar” nada.

5) Decidir: reconstruir, parchear, aceptar con evidencia

  • Reconstruir cuando existe arreglo en paquetes base.
  • Parchear cuando la corrección está en dependencias de la app.
  • Aceptar solo con una justificación documentada y una fecha de revisión; idealmente producir declaraciones VEX.

Errores comunes: síntomas → causa raíz → solución

1) Síntoma: “Lo arreglamos, pero el escáner todavía lo muestra”

Causa raíz: Actualizaste código o paquetes pero no reconstruiste la imagen desde una base fresca; o reconstruiste pero el despliegue sigue ejecutando el digest antiguo.

Solución: Haz pull de la imagen base fresca en CI, reconstruye, push y despliega por digest. Verifica con docker ps / equivalente del orquestador que el digest cambió.

2) Síntoma: “El escáner dice vulnerable, el proveedor dice no afectado”

Causa raíz: Backports o fuentes de asesoría desajustadas (NVD vs rastreador de seguridad de la distro).

Solución: Prefiere fuentes de escaneo conscientes de la distro; registra una excepción con evidencia (string de release del paquete + estado del proveedor). Considera VEX para codificar “no afectado.”

3) Síntoma: “Cada imagen tiene cientos de CVE, lo ignoramos todo”

Causa raíz: Imágenes de runtime hinchadas, imágenes base antiguas y sin cadencia de reconstrucción. La fatiga de alertas se vuelve política.

Solución: Builds multietapa, base mínima de runtime, reconstrucciones programadas y condicionar solo en categorías accionables (arreglado+en runtime+expuesto+alto impacto).

4) Síntoma: “CVE crítico en paquete de kernel dentro del contenedor”

Causa raíz: Mala interpretación: las imágenes de contenedor no traen su propio kernel; los escáneres a veces reportan paquetes o headers relacionados con el kernel.

Solución: Parcha nodos host. Para la imagen, elimina headers/herramientas del kernel a menos que sean necesarias. Trata “CVE de kernel en imagen” como informativo salvo que empaquetes herramientas relacionadas con el kernel.

5) Síntoma: “Actualizamos la imagen base y rompimos validación TLS/CA”

Causa raíz: Pasar a slim/distroless sin incluir explícitamente certificados CA o datos de zona horaria que tu app espera.

Solución: Asegura ca-certificates (o equivalente); copia explícitamente bundles de certificados si usas distroless. Añade pruebas de integración para TLS saliente.

6) Síntoma: “El escáner reporta CVE en paquetes que no tenemos”

Causa raíz: El escáner detecta por firmas de archivos o adivina dependencias de lenguaje desde manifiestos que no coinciden con el contenido de runtime.

Solución: Confirma con consultas a la base de paquetes dentro de la imagen; genera SBOM; actualiza la configuración del escáner para preferir metadatos del gestor de paquetes.

7) Síntoma: “No podemos parchear porque la versión arreglada no existe todavía”

Causa raíz: El upstream no tiene parche, o tu distro no lo ha entregado, o tienes repositorios fijados.

Solución: Mitiga: deshabilita la función vulnerable, restringe la exposición, reduce privilegios, añade reglas WAF, aísla la red. Haz seguimiento y reconstruye cuando el parche llegue.

Listas de comprobación / plan paso a paso

Lista diaria de operaciones (evita que el ruido se convierta en cultura)

  1. Escanea imágenes por digest, no por etiqueta.
  2. Almacena resultados de escaneo vinculados a digest e ID de build.
  3. Rastrea hallazgos “accionables”: arreglo disponible + en runtime + expuesto/relevante.
  4. Genera tickets con nombre exacto del paquete/versión y ruta de remediación (reconstruir vs cambio de código).
  5. Requiere justificación para excepciones, incluyendo evidencia (estado del proveedor, mitigaciones en runtime).

Plan de higiene de seguridad semanal (aburrido, efectivo)

  1. Reconstruye imágenes expuestas a internet al menos semanalmente desde bases frescas.
  2. Vuelve a ejecutar escaneos en imágenes reconstruidas y compara deltas; investiga aumentos.
  3. Reemplaza imágenes base obsoletas; deja de usar distros end-of-life.
  4. Prunea herramientas de build de imágenes de runtime (multietapa).
  5. Verifica endurecimiento en runtime: usuario non-root, drop de capacidades, rootfs de solo lectura cuando sea posible.

Plan paso a paso de triaje para un hallazgo “Crítico”

  1. Chequeo de artefacto: confirma que el digest en ejecución coincide con el digest escaneado.
  2. Chequeo de componente: verifica que el paquete/biblioteca está instalada en la capa de runtime.
  3. Disponibilidad de arreglo: ¿hay paquete o release parcheado?
  4. Exposición: ¿está el componente vulnerable en la ruta de petición o alcanzable vía entrada de usuario?
  5. Privilegios: ¿root? ¿capacidades añadidas? ¿sistema de archivos escribible?
  6. Mitigaciones: ¿configuración deshabilita la función? ¿políticas de red? ¿sandboxing?
  7. Decisión: reconstruir ahora, parchear código o aceptar con evidencia y fecha de revisión.
  8. Verificación: desplegar por digest; reescanea; confirma que producción ejecuta el nuevo digest.

Preguntas frecuentes

1) ¿Debemos bloquear despliegues por escaneos de vulnerabilidades?

Sí, pero solo sobre hallazgos accionables y significativos: existe versión arreglada, el componente está en la imagen de runtime y el riesgo es relevante (expuesto/alto impacto).
Bloquear por hallazgos “sin parche” por defecto suele entrenar a la gente a saltarse el sistema.

2) ¿Qué escáner deberíamos confiar: Trivy, Docker Scout, Grype u otro?

La confianza no es una marca; es un flujo de trabajo. Usa al menos un escáner consciente de la distro y uno que pueda generar un SBOM.
El mejor escáner es el que mantienes actualizado y cuyo output puedes explicar a un auditor sin baile interpretativo.

3) ¿Por qué mi imagen distroless aún tiene CVE?

Distroless reduce paquetes, pero no elimina vulnerabilidades. Aún puedes enviar librerías vulnerables, bundles CA o runtimes de lenguaje.
Además, los escáneres pueden reportar CVE por componentes embebidos dentro de tu binario o runtime.

4) ¿Las vulnerabilidades “sin parche” son seguras para ignorar?

No es seguro ignorarlas. Es seguro tratarlas diferente.
Si no existe parche, tus opciones son mitigación (reducir exposición), reemplazo (otro componente) o aceptación con evidencia y disparador de revisión.

5) ¿Por qué vemos CVE en paquetes que nunca usamos?

Porque los paquetes están instalados, no usados. Los escáneres reportan presencia.
Tu trabajo es reducir la presencia (imágenes más pequeñas) y evaluar alcanzabilidad/exposición de lo que queda.

6) ¿Cómo manejamos falsos positivos por backport sin crear una laguna?

Haz excepciones estructuradas: vincúlalas a digest de imagen, string de release del paquete, estado del proveedor (“arreglado vía backport” / “no afectado”) y pon una fecha de revalidación.
Prefiere declaraciones estilo VEX sobre páginas wiki ad-hoc.

7) ¿Deberíamos preferir Alpine para menos CVE?

Elige una imagen base por compatibilidad operativa primero (problemas glibc vs musl son reales), luego por higiene de seguridad.
Menos CVE reportados no es lo mismo que menos riesgo; también puede ser cobertura de reporte distinta.

8) ¿Con qué frecuencia deberíamos reconstruir imágenes?

Servicios expuestos a internet: semanal es un buen valor por defecto. Jobs internos por lotes: mensual puede ser suficiente.
Si reconstruyes “solo cuando cambia el código”, eliges perder parches de seguridad entregados vía actualizaciones de imagen base.

9) ¿Las mitigaciones en runtime significan que podemos ignorar vulnerabilidades en la imagen?

Las mitigaciones reducen la explotabilidad; no borran las vulnerabilidades.
Usa mitigaciones para ganar tiempo y reducir el radio de daño, pero aún reconstruye/parcha cuando existan arreglos—especialmente para CVE ampliamente explotados.

10) ¿Cuál es la forma más simple de reducir ruido de escaneo rápidamente?

Builds multietapa y eliminar herramientas de build de las imágenes de runtime. Luego añade reconstrucciones programadas desde bases frescas.
Esas dos medidas suelen reducir hallazgos drásticamente sin limitar funcionalidad.

Conclusión: siguientes pasos que realmente reducen el riesgo

El escaneo de vulnerabilidades es una linterna, no un veredicto judicial. Trátalo como telemetría: correlaciona, confirma y actúa sobre lo que importa.
Si quieres menos ruido y más seguridad, no necesitas un nuevo dashboard. Necesitas evidencia y una cadencia.

  1. Escanea por digest y demuestra que producción ejecuta lo que escaneaste.
  2. Genera y almacena SBOMs para cada build; úsalos para responder “qué hay adentro” al instante.
  3. Reconstruye regularmente desde imágenes base frescas; las actualizaciones de seguridad suelen llegar por reconstrucción, no por cambios de código.
  4. Reduce la superficie de runtime con builds multietapa y bases mínimas.
  5. Endurece las configuraciones por defecto: non-root, drop de capacidades, rootfs de solo lectura cuando sea posible.
  6. Adopta excepciones estructuradas (idealmente VEX) para que “no afectado” no se convierta en “ignorado para siempre.”

Haz eso y tu escáner dejará de ser un generador de pánico. Se convertirá en lo que siempre debió ser: una herramienta que te ayuda a enviar sistemas más seguros sin perder los fines de semana.

← Anterior
MySQL vs PostgreSQL: «timeouts aleatorios» — red, DNS y pooling culpables
Siguiente →
Desconexiones IKEv2 en Windows: errores comunes y soluciones fiables

Deja un comentario