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 appgroupyadduser -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 comoappuser, no como root.
Puedes verificar el usuario en ejecución:
- Debe devolver
appusery noroot.
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 delatest, 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
- Analiza la imagen y lista las vulnerabilidades conocidas, su gravedad y, cuando existe, la versión que las corrige.
Trivy
trivyes 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:
--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_123Opciones 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"--mount=type=secret,id=token: monta el secreto solo durante esa instrucciónRUN; 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.
--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
--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.0DOCKER_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.0cosign sign: firma la imagen con una clave privada (fichero de ejemplocosign.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
USERno privilegiado. - Hardcodear secretos en el Dockerfile o en
ENV: quedan grabados en las capas. Usa gestores de secretos o BuildKit. - Usar
latesten producción: imposibilita reproducir builds y puede introducir cambios inseguros sin avisar. Fija versiones. - Ignorar los escaneos de vulnerabilidades: integra
trivyodocker scouten 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:14Las 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 OKSe 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.
Docker: De Principiante a Avanzado
Módulo 1: Introducción a Docker
- ¿Qué es Docker?
- Instalando Docker
- Arquitectura de Docker
- Comandos Básicos de Docker
- Entendiendo las Imágenes de Docker
- Creando tu Primer Contenedor Docker
Módulo 2: Trabajando con Imágenes Docker
- Docker Hub y Repositorios
- Construyendo Imágenes Docker
- Conceptos Básicos de Dockerfile
- Gestionando Imágenes Docker
- Etiquetado y Publicación de Imágenes
Módulo 3: Contenedores Docker
- Ejecutando Contenedores
- Ciclo de Vida del Contenedor
- Gestionando Contenedores
- Redes en Docker
- Persistencia de Datos con Volúmenes
Módulo 4: Docker Compose
- Introducción a Docker Compose
- Definiendo Servicios en Docker Compose
- Comandos de Docker Compose
- Aplicaciones Multi-Contenedor
- Variables de Entorno en Docker Compose
Módulo 5: Conceptos Avanzados de Docker
- Profundización en Redes Docker
- Opciones de Almacenamiento Docker
- Mejores Prácticas de Seguridad en Docker
- Optimizando Imágenes Docker
- Registro y Monitoreo en Docker
Módulo 6: Docker en Producción
- CI/CD con Docker
- Orquestando Contenedores con Docker Swarm
- Introducción a Kubernetes
- Desplegando Contenedores Docker en Kubernetes
- Escalado y Balanceo de Carga
