← maurobernal.com.ar

Etiqueta: rootless

  • 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 →

Tags

tsql (27)mssql (26)sql (20)devops (20)dotnet (18)docker (15)performance (14)contenedores (11)dotnet10 (10)linux (9)csharp (8)microservicios (7)angular (7)angular21 (7)sql server (6)issabel (6)docker-compose (6)typescript (6)mysql (5).NET (5)