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

Não quer configurar manualmente?

Implante o VPS para Docker em menos de 5 minutos com a Runstack. Infraestrutura da OPEN Datacenter, com servidores em São Paulo.

Perguntas frequentes

Sim, namespaces do Docker isolam, mas não são impenetráveis. Vulnerabilidades de escape de container (como runc CVE-2019-5736) permitem que um processo root no container obtenha root no host. Além disso: com acesso root no container, um atacante pode instalar ferramentas, modificar configurações e explorar outras vulnerabilidades com mais facilidade. Usuário não-root + read-only filesystem + no-new-privileges é a combinação mínima para produção.

Capabilities dividem os privilégios do root em unidades menores: NET_BIND_SERVICE (bind em portas < 1024), CAP_SYS_PTRACE (debugar processos), CAP_DAC_OVERRIDE (ignorar permissões de arquivo), etc. Docker por padrão mantém ~14 capabilities. Removendo todas com cap_drop: ALL e adicionando apenas as necessárias, um processo comprometido não pode usar capabilities perigosas. Para Node.js típico: nenhuma capability é necessária se a porta for > 1024.

O modelo de ameaça é diferente. Docker Desktop (local): containers isolados da sua máquina, risco menor. VPS de produção: containers expostos à internet, com tráfego real e dados reais, cada camada de segurança conta. No VPS: sempre usar usuário não-root, limites de recursos, rede restritiva, sem docker.sock exposto. Além disso, o daemon do Docker no VPS deve ter firewall configurado corretamente (o bypass do UFW é um problema real).

Docker Hub (Docker Scout) e Trivy escaniam CVEs em pacotes do sistema operacional e dependências de linguagem. Trivy adiciona: varredura de arquivos de configuração (Dockerfile, docker-compose.yml, Kubernetes manifests) em busca de más práticas, varredura de segredos hardcoded (tokens, senhas em variáveis de ambiente), e análise de permissões de arquivo. Para uso no CI/CD: Trivy é gratuito e open-source, integra com GitHub Actions nativamente e pode bloquear o pipeline se encontrar CVE crítico.

Docker rootless: executa o daemon e os containers sem privilégios root no host, mesmo que um container escape, obtém apenas os privilégios do usuário não-root que executa o daemon. É a forma mais segura de rodar Docker. Limitações: algumas funcionalidades não funcionam (storage drivers específicos, exposição de portas < 1024 sem configuração adicional, overlay network). Para VPS de produção simples com Node.js: Docker rootless + Podman são excelentes opções. A adoção está crescendo mas requer mais configuração inicial.