Mejores Prácticas para Endurecer Imágenes Docker y Reducir la Superficie de Ataque

Endurece imágenes Docker usando usuarios no root, bases más pequeñas, compilaciones multi-etapa, manejo de secretos y escaneos de vulnerabilidades.

Mejores Prácticas para Endurecer Imágenes Docker y Reducir la Superficie de Ataque

Endurecer imágenes Docker comienza con una pregunta simple: ¿qué hay dentro de esta imagen que tu aplicación realmente no necesita? Usuarios extra, shells, gestores de paquetes, herramientas de compilación y secretos filtrados aumentan el daño que un contenedor comprometido puede causar.

Usa estas prácticas cuando escribas o revises un Dockerfile, especialmente antes de que una imagen llegue a un registro compartido o clúster de producción.

Ejecutar Contenedores como Usuarios No Root

Uno de los principios de seguridad más fundamentales es el principio de mínimo privilegio. Por defecto, los procesos dentro de un contenedor Docker se ejecutan como usuario root. Esto les otorga privilegios extensos, que pueden ser explotados por atacantes si el contenedor se ve comprometido. Ejecutar tu aplicación como un usuario no root reduce drásticamente el daño potencial que un atacante puede infligir dentro del contenedor.

Crear un Usuario No Root

Puedes crear un nuevo usuario y grupo dentro de tu Dockerfile y luego cambiar a ese usuario antes de ejecutar tu aplicación.

# Usa un runtime oficial de Python como imagen base
FROM python:3.9-slim

# Establece el directorio de trabajo
WORKDIR /app

# Copia el contenido del directorio actual al contenedor en /app
COPY . /app

# Instala los paquetes necesarios especificados en requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

# Crea un usuario y grupo no root
RUN addgroup --system --gid 1001 appgroup && \
    adduser --system --uid 1001 --ingroup appgroup appuser

# Cambia al usuario no root
USER appuser

# Expone el puerto 80 al mundo exterior a este contenedor
EXPOSE 80

# Define la variable de entorno
ENV NAME World

# Ejecuta app.py cuando el contenedor se inicie
CMD ["python", "app.py"]

Consideraciones para Usuarios No Root

  • Permisos: Asegúrate de que el usuario no root tenga los permisos de lectura y escritura necesarios para los directorios y archivos requeridos por tu aplicación. Es posible que necesites usar chown para establecer la propiedad adecuadamente.
  • Enlace de Puertos: Los usuarios no root típicamente solo pueden enlazar puertos por encima de 1024. Si tu aplicación necesita enlazar un puerto privilegiado (por ejemplo, 80 o 443), considera usar un proxy inverso (como Nginx o Traefik) ejecutándose en el host o dentro de otro contenedor con permisos apropiados, o configura capacidades de Linux.

Minimizar Paquetes y Dependencias Instaladas

Cada paquete instalado en tu imagen Docker aumenta su tamaño y, más importante, su superficie de ataque. Cada paquete puede tener sus propias vulnerabilidades que los atacantes pueden explotar. Por lo tanto, es crucial incluir solo lo absolutamente necesario.

Mejores Prácticas para la Gestión de Paquetes:

  • Usa Imágenes Base Mínimas: Considera imágenes slim, distroless o basadas en Alpine cuando se ajusten a tu runtime. Las imágenes más pequeñas tienden a incluir menos paquetes, pero siempre prueba la compatibilidad porque Alpine usa musl libc y puede comportarse de manera diferente a las imágenes Debian o Ubuntu.
  • Limpia Después de la Instalación: Después de instalar paquetes, limpia cualquier caché del gestor de paquetes o archivos temporales. Esto no solo reduce el tamaño de la imagen, sino que también elimina posibles áreas de preparación para atacantes.
    # Ejemplo para imágenes basadas en Debian/Ubuntu
    RUN apt-get update && apt-get install -y --no-install-recommends some-package && \
        rm -rf /var/lib/apt/lists/*
    
    # Ejemplo para imágenes basadas en Alpine
    RUN apk add --no-cache some-package
    
  • Compilaciones Multi-Etapa: Esta es una técnica poderosa para mantener tu imagen final ligera. Usas una etapa para compilar tu aplicación (instalando herramientas de compilación, compiladores, etc.) y una segunda etapa limpia para copiar solo los artefactos necesarios desde la etapa de compilación. Esto evita que las dependencias de compilación terminen en tu imagen de producción.
    # --- Etapa de Compilación ---
    FROM golang:1.18-alpine AS builder
    WORKDIR /app
    COPY . .
    RUN go build -o myapp
    
    # --- Etapa de Producción ---
    FROM alpine:latest
    WORKDIR /app
    COPY --from=builder /app/myapp .
    CMD ["./myapp"]
    
  • Actualiza las Dependencias Regularmente: Mantén tus dependencias de aplicación e imágenes base actualizadas para incorporar parches de seguridad.

Implementar Verificaciones de Salud Robustas

Las verificaciones de salud son cruciales para monitorear el estado de tus contenedores. Docker puede usar estas verificaciones para determinar si un contenedor se está ejecutando correctamente y para reiniciar o eliminar automáticamente contenedores no saludables. Una verificación de salud bien definida ayuda a asegurar que tu aplicación no solo se está ejecutando, sino que también responde y funciona como se espera.

Definir Verificaciones de Salud

Una instrucción HEALTHCHECK en tu Dockerfile especifica un comando que Docker ejecutará periódicamente dentro del contenedor para probar su salud. Si el comando sale con un estado distinto de cero, el contenedor se considera no saludable.

# Ejemplo para una aplicación web
FROM nginx:latest

# ... otras instrucciones ...

# Verifica si el proceso de Nginx se está ejecutando y escuchando en el puerto 80
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:80/ || exit 1

# ... otras instrucciones ...

Mejores Prácticas para Verificaciones de Salud:

  • Mantenlas Simples: El comando de verificación de salud debe ser ligero y rápido de ejecutar. Evita lógica compleja que podría ralentizar la verificación o introducir sus propios puntos de fallo.
  • Prueba la Funcionalidad Clave: La verificación debería idealmente probar la funcionalidad central de tu aplicación, no solo si un proceso se está ejecutando. Para un servidor web, esto podría significar verificar si puede responder a una solicitud HTTP básica.
  • Configura start-period: Para aplicaciones que tardan en inicializarse, usa la opción start-period para darles tiempo de inicio antes de que las verificaciones de salud comiencen a fallar.

Gestionar Secretos y Datos Sensibles de Forma Segura

Nunca incrustes secretos como claves API, contraseñas o certificados directamente en tu Dockerfile o imagen. Estos secretos se convertirán en parte de la capa de la imagen y son fácilmente descubribles. En su lugar, usa secretos Docker o variables de entorno gestionadas por tu plataforma de orquestación (como Kubernetes o Docker Swarm) para información sensible.

Secretos Docker en Modo Swarm

Docker Swarm proporciona un mecanismo nativo para gestionar secretos. Puedes crear secretos y montarlos como archivos dentro de los contenedores.

# Crea un secreto
docker secret create my_api_key api_key.txt

# Despliega un servicio usando el secreto
docker service create --secret my_api_key my_web_app

Variables de Entorno con Precaución

Aunque las variables de entorno son convenientes, también son visibles al inspeccionar un contenedor en ejecución (docker inspect). Úsalas para datos de configuración no sensibles. Para datos sensibles, se prefieren los Secretos Docker o sistemas externos de gestión de secretos.

Usar Etiquetas de Imagen Específicas

Al hacer referencia a imágenes base u otras imágenes en tu Dockerfile (por ejemplo, FROM ubuntu:latest), siempre usa etiquetas de versión específicas en lugar de latest. Usar latest puede llevar a compilaciones impredecibles, ya que la etiqueta latest puede cambiar con el tiempo, potencialmente introduciendo cambios disruptivos o incluso vulnerabilidades de seguridad sin tu conocimiento.

# Evita esto:
# FROM ubuntu:latest

# Prefiere esto:
FROM ubuntu:22.04

Escanear Imágenes en Busca de Vulnerabilidades

Escanea regularmente tus imágenes Docker en busca de vulnerabilidades conocidas. Varias herramientas pueden ayudarte con esto, tanto en tu pipeline CI/CD como en tu registro.

Herramientas de Escaneo Populares

  • Trivy: Un escáner de vulnerabilidades simple y completo para contenedores. Escanea paquetes del sistema operativo y dependencias de aplicaciones.
    trivy image your-image-name:tag
    
  • Clair: Una herramienta de análisis estático de código abierto para detectar vulnerabilidades en imágenes de contenedores.
  • Docker Scout: Un servicio de Docker que analiza imágenes de contenedores en busca de vulnerabilidades y proporciona recomendaciones.

Integrar estos escaneos en tu proceso de compilación asegura que estés al tanto y puedas abordar posibles problemas de seguridad antes de desplegar tus imágenes.

Entender las Capas de la Imagen

Las imágenes Docker se construyen en capas. Cuando haces un cambio en tu Dockerfile, se crea una nueva capa. Entender cómo funcionan las capas puede ayudarte a optimizar tu Dockerfile tanto para el tamaño como para la seguridad. Coloca las instrucciones que cambian con menos frecuencia (como instalar paquetes base) al principio del Dockerfile, y las instrucciones que cambian con más frecuencia (como copiar el código de la aplicación) al final. Esto aprovecha efectivamente la caché de compilación de Docker y puede acelerar las compilaciones.

Más importante para la seguridad, la información sensible o exposiciones accidentales en capas anteriores pueden persistir. Asegúrate de que cualquier archivo o comando sensible se maneje de manera que no permanezcan en las capas finales de la imagen si ya no son necesarios.

Hacer del Endurecimiento una Rutina

Comienza con los Dockerfiles que se envían a producción. Elimina las herramientas de compilación de las imágenes finales, ejecuta como un usuario no root, fija las imágenes base y escanea cada compilación. Luego trata el endurecimiento de imágenes como parte de la revisión de código normal, no como una limpieza única.