Docker security hardening: containers seguros

    Um container Docker rodando como root com filesystem gravável e sem limites de recursos tem superfície de ataque enorme — se comprometido, pode afetar o host. Aplicar as camadas de segurança corretas isola o container ao ponto em que um comprometimento da aplicação não significa comprometimento do servidor.

    Rodar containers como usuário não-root

    A mudança de maior impacto: nunca rodar aplicação como root no container:

    dockerfile
    # Dockerfile com usuário não-root (Node.js):
    FROM node:22-alpine
    
    # Criar usuário dedicado para a aplicação:
    RUN addgroup -g 1001 -S nodejs &&     adduser -S -u 1001 -G nodejs nodejs
    
    WORKDIR /app
    
    # Copiar dependências primeiro (cache de build):
    COPY --chown=nodejs:nodejs package*.json ./
    RUN npm ci --only=production
    
    # Copiar código fonte:
    COPY --chown=nodejs:nodejs . .
    
    # Trocar para usuário não-root ANTES de expor porta:
    USER nodejs
    
    EXPOSE 3000
    CMD ["node", "server.js"]
    
    # Verificar que o container não roda como root:
    docker run --rm minha-app whoami  # deve retornar "nodejs", não "root"
    docker run --rm minha-app id      # uid=1001(nodejs) gid=1001(nodejs)
    
    # Forçar usuário não-root em runtime (mesmo que Dockerfile não defina):
    docker run --user 1001:1001 minha-app
    
    # docker-compose.yml:
    # services:
    #   app:
    #     image: minha-app
    #     user: "1001:1001"

    Filesystem read-only e tmpfs

    Montar o filesystem do container como somente leitura:

    yaml
    # docker run com read-only filesystem:
    docker run   --read-only   --tmpfs /tmp:size=100m,noexec   --tmpfs /var/run:size=10m   -v /data/app:/app/data:rw   minha-app
    
    # docker-compose.yml:
    services:
      app:
        image: minha-app
        read_only: true
        tmpfs:
          - /tmp:size=100m,noexec,nosuid
          - /var/run:size=10m
        volumes:
          - app-data:/app/data  # volume específico para dados graváveis
        security_opt:
          - no-new-privileges:true  # processo não pode ganhar privilégios adicionais
    
    # Benefício: se a app for comprometida, atacante não pode:
    # - instalar malware no filesystem do container
    # - modificar binários da aplicação
    # - criar arquivos de persistência
    
    # Para Node.js com read-only: ajuste o working dir:
    # WORKDIR /app
    # RUN chown nodejs:nodejs /app
    # Volume para logs: --tmpfs /app/logs:size=50m

    Limites de recursos e capabilities

    Restringir CPU, memória e capabilities do Linux:

    yaml
    # docker-compose.yml — limites de recursos e segurança:
    services:
      app:
        image: minha-app
        read_only: true
    
        # Limites de recursos (evitar DoS por consumo excessivo):
        deploy:
          resources:
            limits:
              cpus: '0.5'        # máximo 50% de 1 CPU
              memory: 512M
            reservations:
              cpus: '0.1'
              memory: 128M
    
        # Remover TODAS as capabilities e adicionar apenas as necessárias:
        cap_drop:
          - ALL
        cap_add:
          - NET_BIND_SERVICE  # necessário para bind em portas < 1024 (ex: porta 80)
          # A maioria das apps Node.js não precisa de nenhuma capability
    
        security_opt:
          - no-new-privileges:true    # bloquear setuid/setgid
          - seccomp:./seccomp.json    # perfil seccomp customizado
    
        # Desabilitar acesso a devices do host:
        devices: []
    
        # Impedir modificação de capabilities em runtime:
        ulimits:
          nofile:
            soft: 65536
            hard: 65536
          nproc:
            soft: 100
            hard: 100
    
        tmpfs:
          - /tmp:size=64m,noexec,nosuid,nodev
        restart: unless-stopped

    Varredura de vulnerabilidades com Trivy

    Escanear imagens Docker em busca de CVEs antes de fazer deploy:

    bash
    # Instalar Trivy:
    sudo apt-get install -y wget apt-transport-https gnupg lsb-release
    wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
    echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" |   sudo tee /etc/apt/sources.list.d/trivy.list
    sudo apt-get update && sudo apt-get install trivy
    
    # Escanear imagem local:
    trivy image minha-app:latest
    
    # Apenas vulnerabilidades críticas e altas:
    trivy image --severity CRITICAL,HIGH minha-app:latest
    
    # Formato JSON para CI/CD:
    trivy image --format json --output trivy-report.json minha-app:latest
    
    # Falhar o build se encontrar CRITICAL:
    trivy image --exit-code 1 --severity CRITICAL minha-app:latest
    
    # GitHub Actions:
    # - name: Scan image
    #   uses: aquasecurity/trivy-action@master
    #   with:
    #     image-ref: minha-app:latest
    #     format: sarif
    #     output: trivy-results.sarif
    #     severity: CRITICAL,HIGH
    
    # Escanear filesystem do Dockerfile (sem precisar buildar):
    trivy fs --severity HIGH,CRITICAL .
    
    # Manter base images atualizadas (principal fonte de CVEs):
    # node:22-alpine tem menos CVEs que node:22-slim que tem menos que node:22
    docker pull node:22-alpine && docker build --no-cache -t minha-app .

    Daemon do Docker: hardening do host

    Configurar o daemon Docker com opções de segurança:

    bash
    # /etc/docker/daemon.json — configurações de segurança do daemon:
    sudo tee /etc/docker/daemon.json << 'EOF'
    {
      "icc": false,
      "no-new-privileges": true,
      "live-restore": true,
      "log-driver": "json-file",
      "log-opts": {
        "max-size": "10m",
        "max-file": "3"
      },
      "userns-remap": "default",
      "seccomp-profile": "/etc/docker/seccomp.json"
    }
    EOF
    
    # Explicando as opções:
    # icc: false          → desabilitar comunicação direta entre containers
    #                       (apenas via redes definidas explicitamente)
    # no-new-privileges   → containers não podem elevar privilégios
    # live-restore        → containers continuam rodando se o daemon reiniciar
    # userns-remap        → mapear UID 0 do container para UID não-root no host
    #                       (root no container = usuário limitado no host)
    
    # IMPORTANTE: userns-remap pode quebrar volumes com permissões específicas
    # Testar antes em ambiente de staging
    
    # Restringir acesso ao socket Docker:
    # Nunca expor /var/run/docker.sock em containers de produção!
    # docker run -v /var/run/docker.sock:/var/run/docker.sock  ← PERIGOSO
    # Qualquer container com acesso ao socket pode escapar para o host
    
    sudo systemctl restart docker

    $ runstack deploy --plan starter

    Não quer configurar manualmente?

    Não quer configurar manualmente? Implante o VPS para Docker em menos de 3 minutos com a Runstack. Infraestrutura da OPEN DATACENTER, com servidores no Brasil.

    Perguntas frequentes