Segurança em containers Docker para Devs

1. Princípios fundamentais de segurança em containers

1.1. O modelo de segurança do Docker: isolamento vs. compartilhamento do kernel

Diferente de máquinas virtuais, containers Docker compartilham o kernel do host. Isso significa que uma falha de segurança no kernel pode comprometer todos os containers. O Docker utiliza namespaces para isolar processos, rede e sistema de arquivos, e cgroups para limitar recursos. Entender essa arquitetura é o primeiro passo para escrever código seguro.

# Verificar namespaces ativos de um container
docker inspect --format '{{.State.Pid}}' meu-container
ls -la /proc/<PID>/ns/

1.2. A superfície de ataque de uma imagem: da base ao runtime

Cada camada de uma imagem Docker adiciona potencial vulnerabilidade. Uma imagem base desatualizada, pacotes desnecessários ou configurações inseguras aumentam a superfície de ataque. Como dev, você controla desde a escolha da imagem até como o container é executado.

1.3. Boas práticas de namespace e cgroups para desenvolvedores

Sempre execute containers com privilégios mínimos. Evite --privileged e prefira capacidades específicas quando necessário.

# Exemplo: container com privilégios mínimos
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE -p 8080:8080 minha-app

2. Construção de imagens seguras (Secure Build)

2.1. Escolha de imagens base oficiais e minimalistas

Prefira imagens oficiais e minimalistas como Alpine (5MB) ou Google Distroless. Elas reduzem drasticamente a superfície de ataque.

# Dockerfile inseguro
FROM ubuntu:latest
RUN apt-get update && apt-get install -y python3 nodejs curl vim

# Dockerfile seguro
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY . .
USER node
CMD ["node", "app.js"]

2.2. Uso de multi-stage builds para reduzir o tamanho

Multi-stage builds eliminam ferramentas de build desnecessárias da imagem final.

# Exemplo de multi-stage build
FROM golang:1.21 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server

FROM alpine:3.19
RUN apk add --no-cache ca-certificates
COPY --from=builder /app/server /server
USER nobody
EXPOSE 8080
CMD ["/server"]

2.3. Fixação de versões e verificação de assinaturas

Sempre use tags específicas (nunca latest) e ative Docker Content Trust para verificar assinaturas.

# Fixar versão da imagem base
FROM node:20.11.0-alpine3.19

# Ativar Docker Content Trust
export DOCKER_CONTENT_TRUST=1
docker pull node:20.11.0-alpine3.19

3. Gerenciamento de vulnerabilidades com scanning

3.1. Integração do Trivy no pipeline CI/CD

Trivy é uma ferramenta open-source que escaneia imagens Docker por vulnerabilidades conhecidas.

# Instalar Trivy
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh

# Escanear imagem
trivy image --severity HIGH,CRITICAL minha-app:latest

# Integração em pipeline GitHub Actions
name: Security Scan
on: [push]
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build image
        run: docker build -t minha-app .
      - name: Scan with Trivy
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'minha-app'
          format: 'table'
          exit-code: '1'

3.2. Interpretação de relatórios de vulnerabilidades

CVEs (Common Vulnerabilities and Exposures) são identificadores únicos para vulnerabilidades. Priorize correção de CVEs com severidade CRITICAL e HIGH. Falsos positivos ocorrem quando a vulnerabilidade existe na base image mas não é acessível no runtime.

3.3. Estratégias de remediação

  • Rebuild: Atualizar pacotes na imagem atual
  • Patch: Aplicar correções específicas
  • Substituição: Trocar para uma base image mais recente ou diferente
# Exemplo de rebuild com atualização de pacotes
FROM alpine:3.19
RUN apk update && apk upgrade --no-cache
COPY --from=builder /app/server /server
CMD ["/server"]

4. Execução segura de containers (Secure Runtime)

4.1. Uso de usuários não-root

Nunca execute containers como root. Crie um usuário específico no Dockerfile.

# Dockerfile seguro
FROM node:20-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
COPY --chown=appuser:appgroup . /app
WORKDIR /app
CMD ["node", "app.js"]

# Ou no runtime
docker run --user 1000:1000 minha-app

4.2. Limitação de capabilities

Capabilities Linux são privilégios específicos. Remova todas e adicione apenas as necessárias.

# Remover todas as capabilities e adicionar apenas NET_BIND_SERVICE
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE minha-app

4.3. Filesystem read-only e volumes temporários

Proteja o filesystem do container contra escrita não autorizada.

# Container com filesystem read-only
docker run --read-only --tmpfs /tmp --tmpfs /var/run minha-app

5. Proteção de segredos e variáveis sensíveis

5.1. Evitando segredos em Dockerfiles

Nunca hardcode senhas ou chaves. O cache de camadas do Docker pode expor esses dados.

# ERRADO - segredo exposto no cache
FROM alpine
ENV DB_PASSWORD=minha_senha

# CERTO - usar variáveis em tempo de execução
docker run -e DB_PASSWORD=minha_senha minha-app

5.2. Uso de Docker secrets (Swarm)

Para ambientes orquestrados, use Docker Secrets.

# Criar secret
echo "minha_senha" | docker secret create db_password -

# Usar no serviço
docker service create --secret db_password --name meu-servico minha-imagem

5.3. Integração com cofres externos

Para produção, integre com HashiCorp Vault ou AWS Secrets Manager.

# Exemplo com Vault (pseudo-código)
from hvac import Client
client = Client(url='http://vault:8200')
client.auth.approle.login(role_id=ROLE_ID, secret_id=SECRET_ID)
secret = client.read('secret/data/db')['data']['data']['password']

6. Boas práticas de rede e isolamento

6.1. Redes definidas pelo usuário

Use redes bridge customizadas para isolar containers entre si.

# Criar rede isolada
docker network create --driver bridge minha-rede

# Conectar containers à rede
docker run --network minha-rede --name api minha-app
docker run --network minha-rede --name db postgres:16-alpine

6.2. Exposição mínima de portas

Exponha apenas portas necessárias e restrinja o binding.

# Expor apenas porta 8080 no localhost
docker run -p 127.0.0.1:8080:8080 minha-app

6.3. Políticas de rede

Evite links obsoletos (--link) e use redes definidas pelo usuário com comunicação via DNS.

7. Auditoria e monitoramento de segurança

7.1. Análise de logs do daemon Docker

Monitore eventos do daemon Docker para detectar atividades suspeitas.

# Visualizar eventos do Docker
docker events --filter 'event=create' --filter 'event=start'

# Logs do daemon
journalctl -u docker.service

7.2. Ferramentas de detecção

Docker Bench Security verifica conformidade com benchmarks de segurança.

# Executar Docker Bench Security
docker run --net host --pid host --userns host --cap-add audit_control \
  -e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST \
  -v /var/lib:/var/lib \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /etc:/etc --label docker_bench_security \
  docker/docker-bench-security

Falco monitora chamadas de sistema em tempo real.

7.3. Checklist final para produção

# Checklist de segurança
1. Imagem base oficial e minimalista
2. Usuário não-root
3. Capabilities mínimas
4. Filesystem read-only
5. Secrets gerenciados externamente
6. Rede isolada
7. Scan de vulnerabilidades no CI/CD
8. Logs ativos
9. Atualizações regulares
10. Docker Bench Security em execução

Referências