Imágenes endurecidas: reduce las alertas CVE de tus contenedores
El escáner de vulnerabilidades de mi equipo terminó un lunes y arrojó 180 CVE sobre una imagen Docker que solo corre un binario de Go estático. Reviso la lista y la mayoría no toca mi aplicación: son paquetes de coreutils que mi binario nunca invoca, un intérprete de Perl que no existe en runtime, tres compiladores que ningún atacante va a usar, y un gestor de paquetes (apt) que en la imagen final no se ejecuta nunca. La gente de seguridad abrió un ticket con prioridad alta basado en un número que no refleja el riesgo real.
Ese ruido no es un bug del escáner — es un problema de la imagen base que heredaste. Y en 2026 hay una respuesta madura para eso: las imágenes distroless de Google, las scratch images de Docker, las variantes slim de Debian, y Chainguard. Esta guía recorre las opciones, mide la reducción real de CVE en una aplicación típica, y documenta el camino para migrar una imagen production sin romper el runtime.
TL;DR — Lo que vas a tener al final
- Tiempo de setup: 2 horas para auditar y elegir la imagen base; 4–8 horas para migrar una app production completa con su CI
- Requisitos: Docker 20.10+ instalado, un proyecto actual para migrar (cuenta con Dockerfile), y el escáner que uses (Trivy, Grype o similar)
- Resultado: Reducción de 70–95% de CVE reportadas, imagen final 5–10× más pequeña, superficie de ataque mínima
- Troubleshooting: Binarios dinámicos no funcionan en scratch (solución: usar static linking o distroless con libc incluida), debugger ausente (solución: build multi-stage con debug image separada), shell no disponible en distroless (solución: entrar como otra imagen con kubectl debug o docker run –pid=container)
Si administras contenedores en producción, conoces la escena: el escáner de vulnerabilidades termina, arroja 180 CVE sobre una imagen que solo corre un binario de Go, y alguien del equipo de seguridad abre un ticket con prioridad alta. Revisas la lista y la mayoría no toca tu aplicación: son paquetes de coreutils, un intérprete de Perl que nadie invoca, tres compiladores y un gestor de paquetes que en runtime jamás se ejecuta.
Ese ruido no es un problema del escáner. Es un problema de la imagen base que heredaste. Y en 2026 hay una respuesta madura para eso: las imágenes endurecidas de tipo distroless, que traen exclusivamente lo necesario para correr tu aplicación. El 25 de agosto de 2026 Red Hat anunció que su catálogo de imágenes endurecidas quedó soportado dentro de la API InspectorScan y el escaneo básico de ECR en AWS, lo que hace que este enfoque deje de ser un experimento de nicho y pase a ser algo que puedes integrar sin cambiar tu pipeline.
Este artículo va al grano sobre lo que importa: qué gana y qué pierde tu equipo con una imagen endurecida, cómo auditar una imagen que no tiene shell, cómo verificar su firma antes de confiar en ella, y en qué escenarios concretos es mejor no usarla.
El problema real: el escáner grita por cosas que no usas
Una imagen base tradicional está diseñada para ser cómoda. Trae shell, gestor de paquetes, utilidades de red, herramientas de compresión y decenas de librerías compartidas por si acaso. Eso es útil cuando estás desarrollando y quieres entrar al contenedor a mirar. En producción se traduce en dos costos concretos.
El primero es operativo: cada paquete que empaquetas hereda su propio flujo de CVE. Tu aplicación no cambió, pero mañana sale una vulnerabilidad en una utilidad que tu proceso jamás llama, y aun así aparece en el reporte y hay que triarla, documentarla y justificar por qué no aplica. El segundo es de seguridad real: un shell y un gestor de paquetes dentro del contenedor son exactamente las herramientas que un atacante necesita después de conseguir ejecución de código para moverse, descargar payloads e instalar persistencia.
La respuesta de las imágenes endurecidas es quitar todo eso de raíz. Sin shell no hay bash -c. Sin gestor de paquetes no hay instalación de software en runtime. Sin utilidades de red no hay descarga de la siguiente etapa del ataque. Y como el conteo de paquetes cae drásticamente, el reporte del escáner se vuelve legible.
Qué es una imagen endurecida (y qué no es)
El proyecto upstream que construye este catálogo se llama Project Hummingbird, publicado bajo licencia MIT y disponible en hummingbird-project.io. Red Hat empaqueta y firma ese trabajo como Red Hat Hardened Images, con disponibilidad general desde el 12 de mayo de 2026, y las publica en un catálogo web navegable en images.redhat.com. Un detalle que suele sorprender: no necesitas suscripción para usarlas, igual que ocurre con las Universal Base Image (UBI).
Las medidas de endurecimiento que aplican son concretas y verificables, no marketing:
- Huella mínima de software: solo los paquetes indispensables para que el workload arranque.
- Usuario no-root por defecto: los contenedores corren con UID
65532cuando es técnicamente posible. - Build hermético sin red: la construcción ocurre sin acceso a internet, lo que evita que una dependencia cambie silenciosamente entre builds.
- Diseño distroless: sin gestor de paquetes y, en la mayoría de los casos, sin shell.
- Criptografía validada FIPS: existen variantes con módulos criptográficos validados FIPS 140-3.
- Actualización rápida: los parches de paquetes se publican lo antes posible, en lugar de esperar una ventana de release.
Hay dos cosas que estas imágenes no son, y conviene decirlas antes de que alguien las venda como bala de plata. No protegen contra vulnerabilidades en tu propio código ni en las dependencias de tu aplicación —un desbordamiento en tu binario de Go sigue siendo tuyo— y no sustituyen una cadena de suministro con procedencia y atestación. Reducen la superficie de la base, que es una capa del problema, no el problema completo.
Endurecida vs UBI vs base tradicional
La decisión no es “endurecida siempre”. Depende de si tu workload es estático o necesita mutar en runtime. Esta es la comparación honesta entre las tres opciones que vas a evaluar:
| Criterio | Imagen endurecida (distroless) | UBI | Base tradicional (Debian/Alpine) |
|---|---|---|---|
| Gestor de paquetes | No incluido, para impedir cambios en runtime | dnf o microdnf |
apt o apk |
| Shell | Ausente en la mayoría de las imágenes | Incluido | Incluido |
| Usuario por defecto | No-root (UID 65532) | Root en la mayoría de los casos | Root en la mayoría de los casos |
| Objetivo de CVE | Cero CVE conocidos en el runtime | Ciclo estándar de parcheo | Ciclo del mantenedor de la distro |
| Debugging interactivo | Requiere variante builder o contenedor efímero externo | Directo con exec |
Directo con exec |
| Caso de uso ideal | Microservicios en producción con dependencias fijas | Desarrollo y despliegue de propósito general | Prototipado y entornos de laboratorio |
La regla práctica: si tu contenedor necesita instalar algo después de arrancar, o si tu script de entrada es un entrypoint.sh de 80 líneas, todavía no estás listo para distroless. Arregla eso primero.
Primer contacto: correr una imagen sin shell
El registry recomendado es registry.access.redhat.com/hi/, que es donde viven las imágenes con las llaves de firma oficiales de Red Hat. Empieza con algo simple para ver cómo se comporta una imagen cuyo entrypoint es la aplicación: los argumentos van directo al binario, sin shell intermedio.
# El entrypoint es curl: los argumentos pasan directo al binario
podman run --rm registry.access.redhat.com/hi/curl:latest -sI https://example.com
# Una base de datos con su variable de entorno habitual
podman run --rm -e POSTGRES_PASSWORD=cambiaesto -p 5432:5432
registry.access.redhat.com/hi/postgresql:latest
Si vienes de Docker Hub, el cambio de mentalidad está en la primera línea: no escribes curl -sI ... dentro de un shell del contenedor, porque no hay shell. El binario es el proceso 1.
Aquí aparece la primera trampa real, y es la que más tickets genera. Como el contenedor no corre como root, no puede abrir puertos privilegiados. La imagen de nginx endurecida escucha en 8080, no en 80, así que un mapeo copiado de tu compose.yml viejo va a fallar con un contenedor que arranca y muere:
# INCORRECTO: la imagen endurecida no escucha en 80
podman run -d -p 8000:80 registry.access.redhat.com/hi/nginx:1.28
# CORRECTO: el puerto interno expuesto es 8080
mkdir -p ./html
echo "hola desde una imagen endurecida" > ./html/index.html
podman run -d --name web-hardened
-p 127.0.0.1:8001:8080
-v $PWD/html:/usr/share/nginx/html:z
registry.access.redhat.com/hi/nginx:1.28
curl -s 127.0.0.1:8001
Dos detalles de ese comando importan en Fedora, RHEL y cualquier host con SELinux activo. El sufijo :z reetiqueta el volumen para que el contenedor pueda leerlo —sin eso obtienes un 403 que parece un problema de nginx pero es de SELinux— y usa :Z en su lugar cuando quieres que ese volumen sea accesible únicamente por un contenedor.
La segunda trampa es de permisos. Si el contenedor necesita escribir en un volumen montado, el UID no-root tiene que poder hacerlo:
# Alinear el dueño del directorio con el UID no-root de la imagen
sudo chown -R 65532:65532 /srv/datos-app
# Alternativa solo para desarrollo con Podman rootless:
# el root del contenedor mapea a tu usuario del host
podman run --user root -v /srv/datos-app:/data:z
registry.access.redhat.com/hi/postgresql:latest
El --user root es aceptable con Podman rootless porque ese root está mapeado a tu usuario sin privilegios en el host. En un runtime con daemon corriendo como root, ese mismo flag te devuelve al problema que estabas tratando de evitar.
Auditar lo que hay dentro cuando no hay shell
La objeción inmediata de cualquier sysadmin es válida: si no hay shell, ¿cómo audito la imagen? El reflejo de podman exec ... bash falla con un mensaje que confunde a quien lo ve por primera vez, porque parece un problema de PATH y en realidad el binario no existe en la imagen.
# Esto falla, y el mensaje es esperado, no un error de configuración
podman exec -it web-hardened bash -c "ls /usr/bin"
# Error: crun: executable file `bash` not found in $PATH
# La vía correcta: exportar el filesystem y leerlo desde el host
podman export web-hardened | tar t | grep '/bin/' | wc -l
Ese conteo de binarios ilustra la diferencia de superficie: en la comparación publicada por Red Hat, la imagen de nginx de Docker Hub arroja 270 entradas en rutas /bin/ frente a 31 en la endurecida. Úsalo como intuición, no como métrica de seguridad: el propio equipo de Red Hat advierte explícitamente que el número de binarios, el tamaño de la imagen y la cantidad de capas no son indicadores fiables de superficie de ataque, porque dependen de flags del compilador, archivos de idioma y features opcionales.
El dato que sí es auditable es el inventario de paquetes. Aunque la imagen no traiga rpm ni dnf, sí conserva la base de datos RPM, y eso es deliberado: los escáneres de seguridad dependen de esos metadatos para funcionar.
# La base de datos RPM sigue ahí aunque no haya binarios de rpm/dnf
podman export web-hardened | tar t | grep rpmdb
# usr/lib/sysimage/rpm/rpmdb.sqlite
# Extraerla y consultarla desde el host
mkdir -p temp-rpm
podman export web-hardened |
tar -x -C temp-rpm --strip-components=4 usr/lib/sysimage/rpm/rpmdb.sqlite
rpm -qa --nosignature --dbpath $PWD/temp-rpm/ | wc -l
Para la imagen de nginx endurecida ese conteo da 47 paquetes, que es el orden de magnitud de una base mínima real. El flag --nosignature es necesario porque la llave de firma de Red Hat requiere soporte para algoritmos criptográficos post-cuánticos que la mayoría de distribuciones aún no importan; si necesitas mantener la validación de firma activa, corre rpm dentro de una variante builder en lugar de en el host.
Otra observación que vas a notar al inspeccionar: estas imágenes traen más capas que su equivalente de Docker Hub —69 contra 19 en el caso de nginx— y eso contradice el consejo clásico de minimizar capas. La razón es técnica y buena: el proyecto reorganiza las capas grandes en capas pequeñas direccionables por contenido agrupadas por paquete de origen, de modo que se comparten entre imágenes. El efecto práctico es que descargas menos bytes al actualizar. El número de capas tampoco es un atributo de seguridad.
Escanear y medir el ruido de CVE
Aquí es donde el beneficio se vuelve un número que puedes llevar a una reunión. Escanea la imagen endurecida y la tradicional con la misma herramienta y compara los reportes. Las versiones actuales al momento de escribir esto son Grype 0.118.0 y Syft 1.51.1, ambas publicadas el 27 de agosto de 2026, y Trivy 0.74.0 del 14 de agosto de 2026.
# Escaneo comparativo con Grype
grype registry.access.redhat.com/hi/nginx:1.28
grype docker.io/library/nginx:1.28
# Solo lo que realmente exige acción, en formato procesable
grype registry.access.redhat.com/hi/nginx:1.28
--fail-on high -o json > grype-hardened.json
# Alternativa con Trivy si ya es tu herramienta en CI
trivy image --severity HIGH,CRITICAL registry.access.redhat.com/hi/nginx:1.28
Si el escaneo tarda una eternidad la primera vez, es la descarga de la base de vulnerabilidades, no la imagen; en CI conviene cachear ese directorio. Y si Grype reporta cero hallazgos, no asumas que falló: confirma que resolvió el inventario de paquetes con syft, porque un escáner que no encuentra metadatos también devuelve una lista vacía.
# Verificar que el escáner sí está viendo los paquetes
syft registry.access.redhat.com/hi/nginx:1.28 -o table | head -20
# Generar el SBOM en formato estándar para el archivo de auditoría
syft registry.access.redhat.com/hi/nginx:1.28
-o spdx-json > nginx-hardened.spdx.json
Ese SBOM es lo que un auditor de cumplimiento va a pedirte, y es también la razón por la que estas imágenes encajan en marcos como CIS, STIG y OpenSCAP: el inventario es corto, está documentado y es reproducible.
Verificar firma y procedencia antes de confiar
Una imagen mínima que no verificas es una imagen mínima en la que estás confiando por fe. Las imágenes del catálogo van firmadas y puedes validarlas con Cosign, cuya versión actual es la 3.1.3 del 6 de agosto de 2026.
# Verificar la firma con la llave pública de Red Hat
cosign verify
--key "https://security.access.redhat.com/data/63405576.txt"
--insecure-ignore-tlog
registry.access.redhat.com/hi/nginx:1.28
# Descargar el SBOM firmado que acompaña a la imagen
cosign download sbom --platform linux/amd64
registry.access.redhat.com/hi/nginx:1.28 > nginx.sbom.json
El flag --insecure-ignore-tlog aparece porque estas firmas no se publican en el log de transparencia público de Sigstore; omitirlo hace que la verificación falle con un error de entrada no encontrada, que es la confusión más común al copiar comandos de otros proyectos. Si tu política interna exige transparencia verificable, ese es un punto a discutir antes de estandarizar.
Vale la pena saber de dónde viene la confianza: los paquetes y las imágenes se construyen con Konflux, que provee cumplimiento SLSA nivel 3. Y hay un detalle de registry que decide si tu verificación va a funcionar: registry.access.redhat.com/hi/ lleva las llaves oficiales, mientras que el espejo quay.io/hummingbird/ publica las mismas imágenes sin esa firma oficial. Los espacios quay.io/hummingbird-community/ y quay.io/hummingbird-rawhide/ son, respectivamente, imágenes de comunidad con menos compromisos de soporte y paquetes de Fedora Rawhide directos. Para producción, usa el primero.
Multi-stage: el patrón que sí funciona en producción
Casi nadie puede construir su aplicación dentro de una imagen sin compilador. La solución no es abandonar distroless, es separar build de runtime. Para eso existen las variantes: :latest es la distroless de producción, y :latest-builder conserva shell, dnf y utilidades de compilación.
# Etapa de build: aquí sí hay shell, dnf y compilador
FROM registry.access.redhat.com/hi/go:latest-builder AS builder
RUN dnf install -y gcc
COPY . /src
WORKDIR /src
RUN go build -o /app .
# Etapa de runtime: base mínima, solo el binario compilado
FROM registry.access.redhat.com/hi/core-runtime:latest
COPY --from=builder /app /app
ENTRYPOINT ["/app"]
El error más frecuente en este patrón es que el binario compilado en la etapa builder dependa de librerías que la etapa runtime no tiene, y el contenedor muera al arrancar con un error de librería compartida. Para Go la solución habitual es compilar estáticamente; para lenguajes con dependencias nativas, verifica el inventario de la imagen runtime con syft antes de dar el build por bueno.
En producción usa tags versionados, no :latest. El esquema del catálogo es explícito: :3.11 se queda en la serie 3.11, mientras :latest puede saltar de versión mayor y romperte un build que ayer funcionaba. Para runtimes con varias familias soportadas —Python, Node.js, .NET— existen imágenes separadas por versión mayor precisamente para evitar ese salto.
Si tu entorno exige criptografía certificada, las variantes :latest-fips y :latest-fips-builder traen módulos validados FIPS 140-3, con dos stacks según la imagen: OpenSSL para Go, Nginx, Node.js, Python, Ruby y curl, y NSS para OpenJDK. Con una condición que no puedes ignorar: la validación aplica solo cuando el host es un sistema RHEL instalado en modo FIPS. Correr la variante FIPS sobre un host que no está en modo FIPS te da la librería, no el cumplimiento.
Las trampas que te van a morder
Más allá del puerto y los permisos, hay tres sorpresas que conviene conocer antes de migrar y no durante el incidente.
La primera es que “distroless” no significa “sin ningún gestor de paquetes”. Las imágenes de Node.js endurecidas sí incluyen npm, y lo mismo ocurre con los gestores nativos de otros runtimes interpretados. Es una concesión deliberada: quitarlo rompería la compatibilidad con las imágenes oficiales de Docker Hub. Si tu política prohíbe gestores de paquetes en producción, tienes que eliminarlo tú con un multi-stage; no asumas que la imagen ya lo hizo.
La segunda son los certificados. Si tu aplicación habla con un servicio interno que usa una CA propia, hay que montar el bundle en la ruta que espera el stack TLS de la imagen, y esa ruta difiere entre imágenes basadas en OpenSSL y las basadas en Java:
# Imágenes con OpenSSL (curl, nginx, python, go)
podman run --rm
-v /ruta/a/ca.crt:/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem:ro,Z
registry.access.redhat.com/hi/curl:latest https://servicio-interno/
# Imágenes con Java, que usa su propio truststore PKCS12
podman run --rm
-v /ruta/a/cacerts:/etc/pki/ca-trust/extracted/java/cacerts:ro,Z
registry.access.redhat.com/hi/openjdk:latest java -jar app.jar
Si el TLS falla con un error de certificado desconocido después de montar el archivo, revisa que montaste la ruta del stack correcto: montar el bundle PEM en una imagen de Java no tiene ningún efecto, porque el JVM lee el truststore, no el PEM.
La tercera es de observabilidad. Sin shell, tu playbook de debugging cambia. Las herramientas que asumen exec con shell dejan de servir, y los sidecars de diagnóstico o contenedores efímeros de depuración pasan a ser parte del diseño, no un extra. Decide eso antes de migrar el primer servicio crítico, no después.
Cuándo NO usar imágenes endurecidas
Hay tres escenarios en los que la respuesta honesta es quedarte con UBI o con tu base actual.
El primero es debugging interactivo habitual. Si tu operación depende de entrar al contenedor a mirar, o si el arranque del servicio pasa por scripts de shell complejos, distroless te va a costar más de lo que te ahorra hasta que refactorices ese arranque.
El segundo son los builds dinámicos. Si el proceso instala paquetes en runtime —un plugin que se descarga al iniciar, una extensión que se compila en el primer arranque— una imagen sin gestor de paquetes simplemente no puede hacerlo.
El tercero es el ciclo de vida empresarial. Si necesitas que el soporte y los parches de tu base estén alineados con el ciclo de RHEL a largo plazo, UBI es la que te da esa garantía; el catálogo endurecido sigue de cerca los ciclos upstream de cada proyecto, lo que es una ventaja para estar al día y una desventaja si tu requisito es estabilidad de versión por años.
Resumen
La reducción de alertas CVE no se consigue afinando el escáner ni negociando excepciones: se consigue no empaquetando software que no usas. Las imágenes endurecidas distroless llevan esa idea al extremo razonable —sin shell, sin gestor de paquetes, usuario no-root por defecto, build hermético— y hoy están disponibles sin suscripción y con soporte en el escaneo nativo de AWS.
El camino de adopción de bajo riesgo es este: elige un servicio con dependencias fijas, escánealo con Grype antes y después para tener el número real, corrige el puerto y los permisos de volumen que casi seguro te van a fallar, verifica firma y SBOM con Cosign, y mueve el build a multi-stage con la variante builder. Cuando ese servicio lleve dos semanas estable, repite con el siguiente.
Y mantén la expectativa calibrada: esto endurece la base, no tu código ni tus dependencias de aplicación. Es una capa del problema, y es la capa más barata de arreglar.
Preguntas frecuentes
¿Por qué Imágenes endurecidas: reduce las alertas CVE de tus contenedores es relevante para sysadmins Linux?
Forma parte del flujo diario de administración. Los conceptos explicados se aplican directamente a servidores en producción y resuelven problemas reales que enfrentan sysadmins hispanohablantes.
¿Qué necesito para empezar con Imágenes endurecidas: reduce las alertas?
Distribución Linux actualizada (Debian, Ubuntu, RHEL, Fedora o Arch), acceso root, y conexión a internet. Para producción: entorno de staging separado antes de aplicar cambios.
¿Cuáles son los errores más comunes con Imágenes endurecidas: reduce las alertas?
Los tres más frecuentes: (1) saltar la lectura de logs, (2) aplicar cambios directos en producción sin probar, (3) olvidar permisos y ownership. La documentación y un plan de rollback siempre.
¿Cuándo usar Imágenes endurecidas: reduce las alertas y cuándo una alternativa?
Esta solución es ideal para el caso descrito. Alternativas como Docker, Ansible o configuración manual pueden ser mejores según contexto: portabilidad, escala y reproducibilidad son los factores clave.
¿Cómo migro desde una configuración previa a Imágenes endurecidas: reduce las alertas?
Documentar el estado actual, probar la nueva configuración en staging, validar funcionalmente, y aplicar en producción con rollback plan listo. Los posts de SoyLinux incluyen troubleshooting para los problemas más comunes.
Lee también
- Guía Fedora 44: Novedades, Instalación y Upgrade para Sysadmins Linux
- Configura VLANs en Linux con nmcli: Guía paso a paso para sysadmins
- Protege tu SSH: Guía esencial para configurar Fail2ban en Linux
- Guía Definitiva: Configura Kubernetes con kubeadm en Ubuntu para Sysadmins Linux
- Configura Prometheus y Grafana: Guía DevOps para SysAdmins Linux

Pingback: AUR bajo ataque: qué pasó en junio 2026 y cómo proteger tu Arch - Soy Linux
Pingback: OCI mínimas y seguras para ISVs: rocks de Canonical - Soy Linux