Hardening de Container: 10 Práticas Para Não Rodar Root em Produção
Container não é uma fronteira de segurança forte por padrão. Usuário não-root, imagem mínima, filesystem read-only, capabilities e mais: as práticas que reduzem a superfície de ataque.
Neste artigo
Há uma ilusão confortável de que containers são seguros por isolarem processos. A realidade é mais dura: containers compartilham o kernel do host e, na configuração padrão, oferecem um isolamento bem mais fraco do que uma máquina virtual. Um container mal configurado — rodando como root, com o filesystem gravável, com capabilities do kernel que não usa — é uma porta convidativa para que uma falha na aplicação vire comprometimento do host inteiro e, dali, dos containers vizinhos.
Hardening de container é o conjunto de práticas que reduzem essa superfície de ataque, aplicando o princípio do menor privilégio em cada camada: a imagem, o runtime, as permissões, a rede. Nenhuma prática isolada é uma bala de prata; a segurança vem da soma delas, em profundidade. Aqui vão dez que separam um container de produção sério de um acidente esperando acontecer.
1. Nunca rode como root
Por padrão, processos dentro de um container rodam como root (UID 0). Se um atacante explora uma falha na aplicação, ele ganha privilégios de root dentro do container — e, dependendo de outras brechas, isso pode escalar para o host. A correção é direta e é a mais importante da lista: crie e use um usuário não privilegiado.
``dockerfile FROM debian:12-slim # Cria um usuário sem privilégios e sem shell de login. RUN useradd --system --no-create-home --shell /usr/sbin/nologin app # ... copia a aplicação ... USER app # a partir daqui, o processo roda como 'app' ENTRYPOINT ["/usr/local/bin/meu-servico"] ``
Reforce isso no runtime para que nem por engano o container suba como root:
``yaml securityContext: runAsNonRoot: true # o Kubernetes recusa o pod se tentar rodar como root runAsUser: 10001 ``
2. Comece de uma imagem base mínima
Cada pacote na imagem é código que pode ter uma vulnerabilidade. Uma imagem baseada numa distribuição completa carrega centenas de binários e bibliotecas que sua aplicação nunca usa, mas que ampliam a superfície de ataque. Prefira bases mínimas: variantes slim, Alpine, ou imagens distroless, que contêm apenas o runtime da linguagem e as dependências, sem shell, sem gerenciador de pacotes, sem utilitários.
Menos binários significam menos CVEs para corrigir e menos ferramentas à mão de um atacante que consiga entrar. Um container sem sh, curl ou wget dificulta enormemente a vida de quem quer baixar um payload ou explorar o ambiente depois de uma invasão inicial.
3. Use multi-stage build para não vazar ferramentas de build
Compiladores, gerenciadores de pacotes e ferramentas de build não têm nenhuma razão para existir na imagem final de produção. O multi-stage build separa o ambiente de compilação do ambiente de execução, copiando para a imagem final apenas o artefato pronto.
```dockerfile # Estágio de build: tem o compilador e o SDK completo. FROM golang:1.22 AS build WORKDIR /src COPY . . RUN CGO_ENABLED=0 go build -o /app/servico ./cmd/servico
# Estágio final: distroless, só o binário. Nada de compilador. FROM gcr.io/distroless/static:nonroot COPY --from=build /app/servico /servico USER nonroot:nonroot ENTRYPOINT ["/servico"] ```
A imagem final não tem Go, não tem shell, não tem nada além do binário estático e o usuário não-root. É pequena, rápida de distribuir e drasticamente mais difícil de explorar.
4. Deixe o filesystem read-only
Uma aplicação bem-comportada raramente precisa escrever no próprio filesystem em runtime. Tornar o filesystem do container somente-leitura impede que um atacante grave binários maliciosos, modifique a aplicação ou persista uma backdoor.
```yaml securityContext: readOnlyRootFilesystem: true # Para os poucos diretórios que precisam de escrita, monte volumes efêmeros: volumeMounts:
- name: tmp
mountPath: /tmp volumes:
- name: tmp
emptyDir: {} # escrita vai para um volume em memória, não na imagem ```
Se a aplicação precisa escrever, ela escreve em volumes explícitos e controlados, não no filesystem base. Isso também deixa claro, na configuração, exatamente onde a escrita é permitida — qualquer tentativa fora dali falha.
5. Descarte as Linux capabilities que não usa
O root do Linux não é monolítico; ele é dividido em capabilities — permissões granulares como "abrir portas privilegiadas", "mudar o dono de arquivos", "manipular a rede". Por padrão, containers recebem um conjunto de capabilities que a maioria das aplicações nunca usa. Descartar todas e adicionar de volta apenas as estritamente necessárias fecha um vetor inteiro de escalonamento.
``yaml securityContext: capabilities: drop: ["ALL"] # remove todas add: ["NET_BIND_SERVICE"] # devolve só o que precisa (ex.: bind na porta 80) allowPrivilegeEscalation: false ``
O allowPrivilegeEscalation: false complementa: impede que um processo ganhe mais privilégios do que o pai, bloqueando truques com binários setuid.
6. Nunca use o modo privilegiado
O modo privilegiado (privileged: true) dá ao container acesso praticamente total ao host — todos os devices, todas as capabilities. É a antítese do hardening. Salvo casos raríssimos e muito bem justificados (alguns agentes de infraestrutura), um container de aplicação nunca deve ser privilegiado. Se você se pegou usando o modo privilegiado para "resolver" um problema de permissão, quase certamente existe uma capability específica que resolve o mesmo sem abrir o host inteiro.
7. Escaneie imagens em busca de vulnerabilidades
As dependências da sua imagem — a base, as bibliotecas do sistema, os pacotes da linguagem — ganham CVEs novos o tempo todo. Uma imagem segura hoje tem uma vulnerabilidade conhecida amanhã. Scanners de vulnerabilidade de container analisam a imagem, listam os pacotes e cruzam com bancos de CVEs conhecidos.
Integre o scan ao pipeline de CI: nenhuma imagem sobe para produção sem passar por uma verificação, e vulnerabilidades críticas quebram o build. Complemente com reescaneamento periódico das imagens já em produção, porque o CVE novo aparece depois que a imagem já subiu. E mantenha as bases atualizadas — uma imagem que não é reconstruída há meses acumula vulnerabilidades corrigidas que você simplesmente não aplicou.
8. Não coloque segredos na imagem
Chaves, senhas e tokens jamais devem ser embutidos na imagem — nem via ENV, nem copiados como arquivo, nem passados como argumento de build. A imagem é distribuída, armazenada em registries e cacheada em camadas; um segredo dentro dela vaza para todo mundo com acesso à imagem, e permanece no histórico de camadas mesmo se você "apagar" em uma camada posterior.
```dockerfile # ERRADO: o segredo vira parte da imagem, para sempre. ENV API_KEY="sk-1234567890abcdef"
# CERTO: a imagem não conhece o segredo; ele é injetado em runtime # pela plataforma (Secret montado como arquivo ou variável de ambiente). ```
Segredos entram em runtime, vindos de um cofre ou de um Secret da plataforma, idealmente montados como arquivo em memória. A imagem em si deve ser publicável sem vazar nada.
9. Fixe versões e verifique a proveniência
Uma imagem base referenciada como :latest é um alvo móvel: o que você testou hoje pode ser outra coisa amanhã, e um comprometimento upstream entra no seu pipeline sem aviso. Fixe as tags e, melhor ainda, fixe o digest (o hash imutável da imagem), garantindo que você roda exatamente o que auditou.
``dockerfile # Fixa o conteúdo exato pelo digest, imune a mudanças na tag. FROM debian:12-slim@sha256:a1b2c3... ``
No mesmo espírito, prefira imagens de fontes confiáveis e considere verificar assinaturas de proveniência, para ter certeza de que a imagem que você puxou é a que o publicador realmente produziu, e não uma versão adulterada.
10. Limite recursos e isole em runtime
Um container sem limites de CPU e memória pode, sob falha ou ataque, consumir todos os recursos do host e derrubar os vizinhos — um DoS acidental ou intencional. Definir limites de recursos é hardening tanto de segurança quanto de estabilidade.
``yaml resources: requests: cpu: "100m" memory: "128Mi" limits: cpu: "500m" memory: "256Mi" # o container é morto se estourar, sem afetar os outros ``
Complemente com isolamento adicional onde a criticidade justificar: perfis de seccomp que restringem quais chamadas de sistema o container pode fazer, políticas de rede que limitam com quem cada pod fala, e — para cargas de altíssimo risco ou multi-tenant — runtimes de sandbox que dão isolamento mais forte que o compartilhamento direto do kernel.
Um bônus: reduza a superfície além do container
Duas práticas complementares fecham lacunas que os dez itens anteriores tocam de leve. A primeira é o isolamento de rede. Por padrão, em muitos clusters, qualquer pod fala com qualquer outro pod. Isso significa que um container comprometido pode varrer a rede interna e atacar serviços vizinhos livremente. Políticas de rede que aplicam o menor privilégio também na comunicação — cada serviço só fala com quem precisa falar — contêm o movimento lateral de um atacante que conseguiu o primeiro pé dentro.
``yaml # Nega todo tráfego de entrada por padrão; libera só o necessário em outra policy. apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny-ingress spec: podSelector: {} policyTypes: ["Ingress"] ``
A segunda é gerar e guardar um inventário do que há em cada imagem — uma lista de materiais de software (SBOM). Quando um novo CVE crítico é anunciado, a pergunta imediata é "quais das minhas imagens contêm a biblioteca afetada?". Sem um inventário, essa resposta é uma caçada manual e demorada; com ele, é uma consulta. O SBOM transforma a resposta a vulnerabilidades de reativa e lenta em rápida e sistemática, o que importa muito quando o relógio corre contra uma falha divulgada.
Defesa em profundidade, não bala de prata
Nenhum desses dez itens sozinho torna um container seguro. O usuário não-root não salva se você embutiu um segredo na imagem. A imagem mínima não adianta se o container roda privilegiado. A força vem de empilhar as camadas: se uma falha, a próxima ainda segura o atacante. É o princípio da defesa em profundidade aplicado ao container.
Vale também situar o hardening no fluxo maior. Boa parte dessas práticas se aplica na construção da imagem (Dockerfile) e é verificável no pipeline de CI — o scan que quebra o build, o lint que recusa USER root, a política que barra o modo privilegiado. Assim, a segurança deixa de depender de alguém lembrar na hora do deploy e passa a ser garantida automaticamente, imagem após imagem.
O resumo prático: rode não-root, parta do mínimo, não carregue o que não usa, negue por padrão e devolva só o necessário, e nunca confie que "container já isola". Cada privilégio que você não concede é um caminho que o atacante não tem. Em segurança de container, o menor privilégio não é uma boa prática opcional — é a diferença entre uma falha contida e um host comprometido.