Tema 10

10. Construcción segura de imágenes: Dockerfile, hardening, usuarios no root y reducción de superficie

Una imagen segura se construye con intención: base confiable, dependencias controladas, capas limpias, permisos mínimos, usuario no root y ausencia de secretos. El objetivo es entregar solo lo necesario para ejecutar la aplicación.

Objetivo Construir imágenes pequeñas, reproducibles y seguras
Enfoque Dockerfile, base images, multi-stage, permisos y secretos
Resultado Reducir vulnerabilidades y privilegios antes del runtime

10.1 Introducción

La seguridad de un contenedor empieza antes de ejecutarlo. Una imagen mal construida puede incluir paquetes vulnerables, herramientas innecesarias, secretos, usuarios privilegiados, archivos con permisos amplios o dependencias sin control.

El Dockerfile o archivo equivalente define muchas de esas decisiones. Por eso debe tratarse como código de infraestructura: versionado, revisado, escaneado y mantenido.

Construir imágenes seguras no significa agregar más controles dentro de la imagen, sino reducir lo que contiene y hacer explícito lo que necesita para funcionar.

10.2 Principios de una imagen segura

Una imagen segura debe ser mínima, verificable, reproducible y ejecutarse con permisos reducidos. Estos principios guían las decisiones de build.

  • Base confiable: usar imágenes mantenidas y provenientes de fuentes autorizadas.
  • Menor superficie: incluir solo paquetes, librerías y archivos necesarios.
  • Sin secretos: evitar credenciales en capas, variables, historial o archivos.
  • No root: ejecutar la aplicación con usuario sin privilegios.
  • Reproducibilidad: fijar versiones y poder reconstruir la imagen de forma consistente.
  • Actualización: reconstruir cuando base o dependencias reciben parches.
  • Trazabilidad: saber qué código, base y dependencias generaron cada imagen.
Una imagen segura es más fácil de auditar porque contiene menos cosas. Cada paquete innecesario es una dependencia que puede fallar, vulnerarse o requerir mantenimiento.

10.3 Elección de imagen base

La imagen base es el punto de partida. Si está desactualizada o viene de una fuente no confiable, ese riesgo se hereda en todas las imágenes construidas sobre ella.

Criterio Buena práctica Riesgo si se ignora
Origen Usar registries aprobados e imágenes oficiales o internas Base alterada o sin mantenimiento
Tamaño Preferir bases mínimas cuando sean compatibles Más paquetes y más vulnerabilidades
Soporte Elegir imágenes con actualizaciones frecuentes Vulnerabilidades sin parche disponible
Versión Fijar versión o digest en producción Builds no reproducibles o cambios inesperados
Compatibilidad Validar que la base contiene solo lo que la app requiere Dependencias innecesarias o fallas operativas

10.4 Tags, versiones y digest

Un tag como latest es cómodo, pero puede apuntar a contenido distinto con el tiempo. Para producción conviene usar versiones explícitas y, cuando sea posible, digest criptográfico.

Comparación práctica:

  • latest: simple, pero mutable y poco reproducible.
  • Versión específica: más estable y fácil de auditar.
  • Digest: identifica exactamente el contenido de la imagen.

Usar digest mejora trazabilidad, pero también exige proceso de actualización para no quedar atado a imágenes vulnerables antiguas.

10.5 Builds multi-stage

Los builds multi-stage separan el entorno de compilación del entorno de ejecución. Esto permite usar herramientas pesadas durante el build y copiar al resultado final solo los artefactos necesarios.

Beneficios:

  • Reduce tamaño de imagen final.
  • Evita incluir compiladores, gestores de paquetes y herramientas de build en producción.
  • Disminuye superficie de ataque.
  • Mejora separación entre dependencias de build y runtime.
  • Facilita auditoría de qué se ejecuta realmente.
El entorno de build puede necesitar muchas herramientas. El entorno de runtime debería contener solo lo imprescindible para ejecutar la aplicación.

10.6 Gestión de dependencias

Las dependencias son una fuente frecuente de vulnerabilidades. Incluyen paquetes del sistema operativo, librerías del lenguaje, módulos externos y binarios descargados durante el build.

Buenas prácticas:

  • Usar archivos lock cuando el ecosistema lo permita.
  • Fijar versiones de dependencias críticas.
  • Evitar descargar scripts remotos sin verificación.
  • Verificar checksums o firmas de binarios externos.
  • Eliminar caches y archivos temporales después de instalar.
  • Reconstruir imágenes cuando aparecen parches relevantes.
  • Escanear dependencias como parte del pipeline.

10.7 Usuarios no root

Ejecutar como root dentro del contenedor aumenta el impacto de una vulnerabilidad. Aunque root en un contenedor no siempre equivale a root del host, sigue otorgando más poder dentro del entorno aislado y puede combinarse con otras malas configuraciones.

Para reducir riesgo:

  • Crear un usuario específico para la aplicación.
  • Asignar permisos mínimos a archivos y directorios necesarios.
  • Evitar escribir en rutas del sistema.
  • Declarar el usuario con la instrucción USER.
  • Validar que la aplicación funciona sin privilegios elevados.
  • Combinar con filesystem de solo lectura cuando sea posible.

10.8 Permisos de archivos y directorios

Una imagen puede ejecutar como usuario no root y aun así tener permisos inseguros. Archivos escribibles por todos, claves privadas legibles o directorios críticos modificables pueden facilitar abuso.

Elemento Riesgo Control
Archivos de aplicación Modificación de código en runtime Propietario correcto y permisos de solo lectura
Directorios temporales Escritura fuera de rutas esperadas Limitar escritura a rutas necesarias
Claves o certificados Lectura por procesos no autorizados No incluirlos en imagen; montar secretos en runtime
Binarios Reemplazo o ejecución no prevista Permisos mínimos y ausencia de herramientas innecesarias

10.9 Secretos durante el build

Los builds a veces necesitan acceder a repositorios privados, paquetes internos o servicios de firma. Esos accesos no deben quedar persistidos en capas, historial, logs o variables finales.

Prácticas seguras:

  • Usar mecanismos de secretos de build en lugar de ARG o ENV.
  • No copiar archivos de credenciales al contexto de build.
  • Excluir secretos mediante .dockerignore.
  • Evitar imprimir tokens en logs de instalación.
  • Usar credenciales temporales de corta duración.
  • Rotar credenciales si hay sospecha de exposición.
Borrar un secreto en una instrucción posterior no garantiza que desaparezca de la imagen. Puede quedar en una capa previa o en el historial del build.

10.10 Contexto de build y .dockerignore

El contexto de build es el conjunto de archivos enviados al motor de construcción. Si incluye archivos innecesarios, aumenta el riesgo de copiar secretos, artefactos temporales, dependencias locales o documentación sensible.

El archivo .dockerignore ayuda a excluir:

  • Directorios .git y metadatos del repositorio.
  • Archivos .env, credenciales y claves privadas.
  • Logs, dumps, backups y archivos temporales.
  • Dependencias locales que deben instalarse de forma controlada.
  • Artefactos de build previos.
  • Documentación interna que no requiere la aplicación.

10.11 Reducción de superficie

Reducir superficie significa eliminar todo lo que no aporte a la ejecución de la aplicación. Esto limita vulnerabilidades disponibles y dificulta acciones posteriores de un atacante.

Medidas habituales:

  • Eliminar gestores de paquetes del runtime si no se necesitan.
  • No incluir shells, curl, wget, compiladores o herramientas de debug en producción salvo necesidad justificada.
  • Usar imágenes distroless o mínimas cuando sean compatibles.
  • Eliminar caches y archivos temporales.
  • Exponer solo el puerto necesario.
  • Definir un comando de inicio claro y no ambiguo.
  • Separar imágenes de desarrollo y producción.

10.12 Metadatos, etiquetas y trazabilidad

Una imagen debe poder vincularse con su origen. Las etiquetas de metadatos ayudan a saber qué repositorio, commit, versión, pipeline y fecha generaron el artefacto.

Metadatos útiles:

  • Repositorio de código fuente.
  • Commit o versión de aplicación.
  • Fecha de build.
  • Pipeline o job que construyó la imagen.
  • Dueño del servicio.
  • Licencia y proveedor de imagen base.
  • Referencia a SBOM o reporte de escaneo.

10.13 Escaneo durante el pipeline

El escaneo de imágenes detecta vulnerabilidades conocidas en paquetes del sistema, librerías y dependencias. Debe ejecutarse antes de publicar y desplegar imágenes.

Momento Qué valida Acción esperada
Pull request Dockerfile y dependencias declaradas Detectar malas prácticas temprano
Build Imagen resultante Bloquear vulnerabilidades críticas según política
Registry Imágenes almacenadas Alertar nuevas CVE sobre imágenes existentes
Pre-deploy Imagen candidata a producción Verificar firma, digest y cumplimiento

10.14 Ejemplo de Dockerfile más seguro

Un Dockerfile seguro depende del lenguaje y aplicación, pero este ejemplo muestra ideas generales: build separado, usuario no root, copia mínima y runtime reducido.

FROM node:22-bookworm-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-bookworm-slim AS runtime
ENV NODE_ENV=production
WORKDIR /app
RUN useradd --create-home --shell /usr/sbin/nologin appuser
COPY --from=build /app/dist ./dist
COPY --from=build /app/package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]

Este ejemplo no es universal. En producción habría que fijar versiones con más precisión, escanear dependencias, evaluar una base más mínima y usar secretos solo en runtime.

10.15 Errores frecuentes

  • Usar latest en producción.
  • Construir desde imágenes base sin mantenimiento.
  • Ejecutar como root porque es más rápido resolver permisos.
  • Copiar todo el repositorio sin .dockerignore.
  • Incluir secretos mediante ARG, ENV o archivos temporales.
  • Instalar herramientas de debug en imágenes productivas.
  • No limpiar caches ni dependencias de build.
  • Escanear imágenes pero no bloquear ni remediar vulnerabilidades críticas.

10.16 Lista de verificación

  • ¿La imagen base es confiable, mantenida y con versión controlada?
  • ¿Se evita latest para despliegues productivos?
  • ¿Se usa multi-stage para separar build y runtime?
  • ¿La imagen final contiene solo lo necesario?
  • ¿La aplicación corre como usuario no root?
  • ¿Los secretos quedan fuera del Dockerfile, capas, contexto y logs?
  • ¿El contexto de build tiene .dockerignore adecuado?
  • ¿La imagen se escanea, etiqueta y vincula con commit o pipeline?

10.17 Qué debes recordar de este tema

  • La seguridad de contenedores empieza en el build de la imagen.
  • Una imagen mínima reduce vulnerabilidades y facilita auditoría.
  • Los secretos nunca deben quedar en capas, historial, contexto o logs.
  • Ejecutar como usuario no root limita impacto ante compromiso.
  • Escaneo, trazabilidad y actualización deben integrarse al pipeline.

10.18 Conclusión

Construir imágenes seguras exige disciplina en el Dockerfile, control de dependencias, reducción de superficie y permisos mínimos. Cada decisión tomada en el build afecta directamente el riesgo del contenedor en runtime.

En el próximo tema estudiaremos el escaneo de vulnerabilidades en imágenes, dependencias y paquetes del sistema.