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.