La comodidad de Docker no debe hacernos olvidar que un contenedor mal configurado puede convertirse en una puerta de entrada para un atacante. En esta lección estudiaremos las prácticas de seguridad esenciales: ejecutar procesos con un usuario no privilegiado, partir de imágenes base mínimas y de confianza, escanear vulnerabilidades, gestionar secretos correctamente, limitar capacidades del kernel, usar sistemas de archivos de solo lectura y firmar imágenes. Aplicar estas medidas reduce drásticamente la superficie de ataque de tus despliegues.

Aviso importante: las prácticas descritas en esta lección son recomendaciones generales con fines formativos. Antes de aplicarlas en un entorno real, deben validarse y adaptarse según las políticas de seguridad, cumplimiento (compliance) y gobierno de tu organización. Consulta siempre con el equipo de seguridad/compliance responsable.

No Ejecutar como Root: la Instrucción USER

Por defecto, los procesos dentro de un contenedor se ejecutan como root. Si un atacante logra escapar del contenedor, hereda privilegios de root en el host. La solución es definir un usuario no privilegiado con la instrucción USER en el Dockerfile.

FROM node:18-alpine

# Crear un grupo y un usuario sin privilegios
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

WORKDIR /app
COPY --chown=appuser:appgroup . .
RUN npm ci --omit=dev

# A partir de aquí, los procesos se ejecutan como appuser
USER appuser

CMD ["node", "server.js"]
  • addgroup -S appgroup y adduser -S appuser -G appgroup: crean un grupo y un usuario de sistema (-S) sin privilegios.
  • COPY --chown=appuser:appgroup: copia los ficheros asignando la propiedad al usuario no privilegiado.
  • USER appuser: a partir de esta línea, todas las instrucciones y el proceso principal se ejecutan como appuser, no como root.

Puedes verificar el usuario en ejecución:

docker exec -it mi_contenedor whoami
  • Debe devolver appuser y no root.

Imágenes Base Mínimas y de Confianza

Cuanto más pequeña y simple sea la imagen base, menos componentes vulnerables contiene.

Imagen base Tamaño aproximado Superficie de ataque Uso recomendado
ubuntu/debian Grande Alta Cuando se necesita un SO completo
alpine ~5 MB Baja Mayoría de aplicaciones
distroless Mínimo Muy baja Producción, máxima seguridad
scratch 0 (vacía) Mínima Binarios estáticos autocontenidos

Recomendaciones:

  • Usa imágenes oficiales o verificadas. En Docker Hub, busca la etiqueta Docker Official Image o Verified Publisher.
  • Fija versiones concretas (node:18.19-alpine) en lugar de latest, para garantizar reproducibilidad y evitar cambios inesperados.
  • Considera imágenes distroless de Google, que no incluyen ni shell ni gestor de paquetes, reduciendo enormemente la superficie de ataque.
# Ejemplo con distroless en una build multi-etapa
FROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /app/servidor .

FROM gcr.io/distroless/static-debian12
COPY --from=build /app/servidor /servidor
USER nonroot:nonroot
ENTRYPOINT ["/servidor"]
  • La imagen final no contiene shell, por lo que un atacante no podrá ejecutar comandos interactivos aunque comprometa el proceso.

Escaneo de Vulnerabilidades

Antes de desplegar una imagen, debes analizarla en busca de vulnerabilidades conocidas (CVE).

Docker Scout

docker scout cves mi_imagen:1.0
  • Analiza la imagen y lista las vulnerabilidades conocidas, su gravedad y, cuando existe, la versión que las corrige.

Trivy

trivy image mi_imagen:1.0
  • trivy es un escáner de código abierto muy popular. Examina paquetes del sistema y dependencias de la aplicación.

Puedes hacer que falle un pipeline si encuentra vulnerabilidades graves:

trivy image --severity HIGH,CRITICAL --exit-code 1 mi_imagen:1.0
  • --severity HIGH,CRITICAL: solo considera vulnerabilidades altas y críticas.
  • --exit-code 1: devuelve un código de error si encuentra alguna, ideal para integrarlo en CI/CD y bloquear despliegues inseguros.

Gestión de Secretos

Nunca incluyas contraseñas, tokens o claves directamente en el Dockerfile, en variables de entorno visibles ni en la imagen. Quedarían expuestas en el historial de capas.

# MAL: el secreto queda grabado en una capa de la imagen para siempre
ENV API_KEY=clave_super_secreta_123

Opciones correctas:

  • Docker secrets (en Swarm): se montan en /run/secrets/ y nunca se escriben en disco de forma persistente.
  • BuildKit secret mounts: para usar secretos solo durante la construcción sin dejar rastro.
  • Gestores externos: HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, etc.

Ejemplo con un secreto de BuildKit:

# syntax=docker/dockerfile:1
FROM alpine
RUN --mount=type=secret,id=token \
    cat /run/secrets/token > /dev/null && echo "token usado en build sin persistir"
docker build --secret id=token,src=./token.txt -t mi_imagen:1.0 .
  • --mount=type=secret,id=token: monta el secreto solo durante esa instrucción RUN; no queda en ninguna capa.
  • --secret id=token,src=./token.txt: indica el fichero local que contiene el secreto (datos ficticios en este ejemplo).

Capabilities y Sistema de Archivos de Solo Lectura

Eliminar capacidades del kernel

Los contenedores reciben por defecto un conjunto de capabilities de Linux que la mayoría de aplicaciones no necesitan. Elimínalas todas y añade solo las imprescindibles.

docker run -d --name web \
  --cap-drop ALL \
  --cap-add NET_BIND_SERVICE \
  nginx
  • --cap-drop ALL: elimina todas las capacidades.
  • --cap-add NET_BIND_SERVICE: vuelve a conceder solo la necesaria para enlazar puertos por debajo de 1024.

Sistema de archivos de solo lectura

docker run -d --name web \
  --read-only \
  --tmpfs /tmp \
  nginx
  • --read-only: monta el sistema de archivos raíz del contenedor como solo lectura, impidiendo que un atacante escriba ficheros maliciosos.
  • --tmpfs /tmp: como algunas aplicaciones necesitan escribir en /tmp, se concede un área escribible en memoria.

Otras restricciones útiles

docker run -d --name app \
  --security-opt no-new-privileges \
  --memory 256m \
  --pids-limit 100 \
  mi_imagen:1.0
  • --security-opt no-new-privileges: impide que un proceso obtenga más privilegios mediante binarios setuid.
  • --memory 256m: limita la memoria, mitigando ataques de agotamiento de recursos.
  • --pids-limit 100: limita el número de procesos, mitigando fork bombs.

Principio de Mínimo Privilegio

Todo lo anterior responde a una misma idea rectora: conceder solo lo estrictamente necesario.

  • Usuario no root por defecto.
  • Solo las capabilities imprescindibles.
  • Sistema de archivos de solo lectura siempre que sea posible.
  • Sin acceso a la red si la aplicación no lo requiere (--network none).
  • Límites de recursos definidos.
  • Sin montar el socket de Docker (/var/run/docker.sock) dentro de contenedores salvo necesidad justificada, ya que equivale a dar control total del host.

Firma de Imágenes

La firma garantiza que una imagen no ha sido alterada y proviene de quien dice provenir.

# Habilitar Docker Content Trust para forzar imágenes firmadas
export DOCKER_CONTENT_TRUST=1
docker pull mi_registro/mi_imagen:1.0
  • DOCKER_CONTENT_TRUST=1: con esta variable activada, Docker solo descargará y ejecutará imágenes firmadas, rechazando las que no lo estén.

Para entornos modernos, herramientas como Cosign (del proyecto Sigstore) permiten firmar y verificar imágenes con criptografía:

cosign sign --key cosign.key mi_registro/mi_imagen:1.0
cosign verify --key cosign.pub mi_registro/mi_imagen:1.0
  • cosign sign: firma la imagen con una clave privada (fichero de ejemplo cosign.key).
  • cosign verify: verifica la firma con la clave pública antes de confiar en la imagen.

Errores Comunes y Consejos

  • Ejecutar todo como root: el error más frecuente y peligroso. Define siempre un USER no privilegiado.
  • Hardcodear secretos en el Dockerfile o en ENV: quedan grabados en las capas. Usa gestores de secretos o BuildKit.
  • Usar latest en producción: imposibilita reproducir builds y puede introducir cambios inseguros sin avisar. Fija versiones.
  • Ignorar los escaneos de vulnerabilidades: integra trivy o docker scout en tu pipeline y bloquea CVE críticas.
  • Montar el socket de Docker sin necesidad: equivale a entregar el host. Evítalo salvo casos muy concretos y controlados.
  • Consejo: trata la seguridad como un proceso continuo, no como una tarea puntual. Reescanea las imágenes con regularidad, ya que aparecen nuevas vulnerabilidades constantemente.

Ejercicios

Ejercicio 1: Contenedor sin privilegios

Escribe un Dockerfile que cree un usuario no privilegiado y ejecute una aplicación como ese usuario. Construye la imagen y verifica con whoami que no se ejecuta como root.

Ejercicio 2: Escaneo de vulnerabilidades

Descarga una imagen antigua a propósito (por ejemplo, node:14) y escanéala con docker scout o trivy. Identifica cuántas vulnerabilidades críticas tiene y razona por qué conviene actualizar la imagen base.

Ejercicio 3: Endurecimiento en tiempo de ejecución

Lanza un contenedor Nginx aplicando: sistema de archivos de solo lectura, eliminación de todas las capabilities salvo NET_BIND_SERVICE, no-new-privileges y un límite de memoria. Comprueba que el servidor sigue funcionando.

Soluciones

Solución 1

FROM alpine:3.20
RUN addgroup -S app && adduser -S app -G app
USER app
CMD ["sh", "-c", "whoami && sleep 3600"]
docker build -t app_segura:1.0 .
docker run -d --name segura app_segura:1.0
docker logs segura   # Debe mostrar 'app', no 'root'

Solución 2

docker pull node:14
docker scout cves node:14
# Alternativa:
trivy image --severity CRITICAL node:14

Las imágenes antiguas acumulan numerosas CVE críticas porque sus paquetes y dependencias llevan años sin actualizarse. Migrar a una versión mantenida y mínima (por ejemplo node:18-alpine) reduce drásticamente el riesgo.

Solución 3

docker run -d --name web_segura \
  --read-only \
  --tmpfs /var/cache/nginx \
  --tmpfs /var/run \
  --cap-drop ALL \
  --cap-add NET_BIND_SERVICE \
  --security-opt no-new-privileges \
  --memory 128m \
  -p 8080:80 \
  nginx

curl -I http://localhost:8080   # Debe responder 200 OK

Se conceden áreas tmpfs para los directorios donde Nginx necesita escribir, manteniendo el resto del sistema de archivos en solo lectura.

Conclusión

En esta lección hemos cubierto las prácticas de seguridad fundamentales en Docker: ejecutar como usuario no privilegiado, partir de imágenes mínimas y de confianza, escanear vulnerabilidades, gestionar secretos sin exponerlos, limitar capabilities y recursos, usar sistemas de archivos de solo lectura y firmar imágenes, todo ello bajo el principio de mínimo privilegio. Recuerda validar siempre estas medidas con el equipo de seguridad/compliance de tu organización antes de aplicarlas en producción.

En la siguiente lección, Optimizando Imágenes Docker, veremos cómo construir imágenes más pequeñas y rápidas mediante builds multi-etapa, gestión inteligente de la caché de capas, .dockerignore y herramientas de análisis de tamaño.

© Copyright 2026. Todos los derechos reservados