Listado de la etiqueta: secrets

Seguridad en Docker: errores que cometí y cómo los corregí

Desplegué una imagen con credenciales hardcodeadas en el Dockerfile. Una vez. Cuando me di cuenta, la imagen estaba en el registry privado de la empresa y nadie más la había visto — pero el susto fue suficiente para que revisara la seguridad de todos mis contenedores ese mismo día.

Los errores de seguridad más comunes en Docker

La mayoría de los problemas de seguridad en Docker no son bugs exóticos: son malas prácticas que cometemos por desconocimiento o por apurarnos. Acá van los que yo cometí y cómo los corregí.

Error 1: Correr contenedores como root

# ❌ Por defecto, el proceso corre como root dentro del contenedor
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]
# Si el proceso es comprometido, tiene privilegios de root dentro del contenedor

# ✅ Crear y usar un usuario no-root
FROM node:20-alpine
WORKDIR /app

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

# Copiar archivos con el usuario correcto
COPY --chown=appuser:appgroup . .
RUN npm install --only=production

USER appuser
CMD ["node", "server.js"]

Error 2: Secretos en el Dockerfile o en variables de entorno planas

# ❌ NUNCA - el secreto queda grabado en una capa para siempre
ENV DB_PASSWORD=mipassword123
RUN curl -H "Authorization: Bearer TOKENREALAQUI" https://api.interna.com/config

# Aunque hagas otra capa que lo "borre", sigue en la historia de la imagen:
docker history mi-imagen  # el secreto es visible

# ✅ Para secrets en build-time, usar BuildKit secrets
# Esto NO deja rastro en las capas
# syntax=docker/dockerfile:1
FROM alpine
RUN --mount=type=secret,id=api_token     TOKEN=$(cat /run/secrets/api_token) &&     curl -H "Authorization: Bearer $TOKEN" https://api.interna.com/config

# Build:
docker build --secret id=api_token,src=./secrets/api_token .

# ✅ Para secrets en runtime, usar Docker secrets (Swarm) o variables de entorno
# via archivo .env que NUNCA va al repositorio
docker run --env-file .env.prod mi-imagen

Error 3: Imágenes base desactualizadas

# Escanear una imagen en busca de vulnerabilidades conocidas
docker scout cves mi-imagen:latest

# O con Trivy (más completo, lo que uso en CI)
docker run --rm   -v /var/run/docker.sock:/var/run/docker.sock   aquasec/trivy image mi-imagen:latest

# Resultado típico:
# 2026-03-11T10:00:00Z INFO Detected OS: alpine 3.18
# CRITICAL: 0, HIGH: 1, MEDIUM: 3, LOW: 8
# HIGH: libssl CVE-2024-XXXXX - update to 3.1.5-r0

Error 4: El .dockerignore inexistente

# Sin .dockerignore, COPY . . incluye todo — incluyendo:
# - .git/ (historial completo del repo)
# - .env (credenciales locales)
# - node_modules/ (pesado e innecesario)
# - tests/ (código de tests en la imagen de producción)

# .dockerignore completo que uso en todos mis proyectos:
.git
.gitignore
.env
.env.*
!.env.example
**/node_modules
**/bin
**/obj
**/*.log
**/.DS_Store
docker-compose*.yml
Dockerfile*
tests/
docs/
README.md
.github/

Dockerfile inseguro vs seguro: comparación completa

# ❌ Dockerfile inseguro
FROM node:latest                    # versión impredecible
ENV API_KEY=abc123supersecret       # secreto en capa
WORKDIR /app
COPY . .                            # sin .dockerignore, incluye .env y .git
RUN npm install                     # instala todo incluyendo devDependencies
EXPOSE 3000
CMD ["node", "server.js"]           # corre como root
# ✅ Dockerfile seguro
FROM node:20.11.0-alpine3.19        # versión fija y verificable

WORKDIR /app

# Usuario no-root
RUN addgroup -S app && adduser -S app -G app

# Dependencias primero (aprovecha cache, sin devDependencies)
COPY --chown=app:app package*.json ./
RUN npm ci --only=production && npm cache clean --force

# Código fuente (sin secretos - .dockerignore los excluye)
COPY --chown=app:app src/ ./src/

# Sin variables de entorno sensibles en la imagen
# Se pasan en runtime con --env-file

USER app
EXPOSE 3000

# Healthcheck
HEALTHCHECK --interval=30s --timeout=3s --retries=3   CMD wget -q -O /dev/null http://localhost:3000/health || exit 1

CMD ["node", "src/server.js"]

Limitar capacidades del contenedor

# Eliminar todas las capabilities de Linux y agregar solo las necesarias
docker run   --cap-drop=ALL   --cap-add=NET_BIND_SERVICE \   # solo si necesita bind a puerto < 1024
  --read-only \                   # sistema de archivos de solo lectura
  --tmpfs /tmp \                  # área de escritura temporal
  --security-opt=no-new-privileges   --user 1001:1001   mi-imagen:latest

El checklist que uso antes de cada deploy

  • ✅ ¿La imagen corre con usuario no-root?
  • ✅ ¿Hay .dockerignore con .env excluido?
  • ✅ ¿Los secretos van en runtime, no en la imagen?
  • ✅ ¿La imagen base tiene versión fija?
  • ✅ ¿Pasé Trivy o docker scout y no hay CVEs críticos?
  • ✅ ¿Los puertos expuestos son solo los necesarios?
  • ✅ ¿Hay healthcheck configurado?

Artículo anterior: Microservicios con Docker | Serie Docker Completo | Próximo: Docker vs Kubernetes →


Artículo anterior: Microservicios con Docker: lo que aprendí armando mi primera arquitectura | Serie Docker Completo | Próximo: Docker vs Kubernetes: cuándo me alcanza con uno y cuándo necesito el otro →