Segurança no Docker: como evitar contêineres executados como root

No mês passado, ajudei um amigo a revisar a configuração de contêineres da empresa dele. Executei um docker inspect por curiosidade: todos rodavam como root e ainda usavam --privileged. Duas semanas depois, um contêiner foi comprometido, e o invasor usou um escape de contêiner para assumir o controle do host.
Isso não é alarmismo. A CVE-2024-21626, divulgada em janeiro de 2024, é um caso típico: o invasor precisava apenas controlar o parâmetro de “diretório de trabalho” do contêiner para explorar um descritor de arquivo vazado e manipular todo o sistema de arquivos do host. Dados ainda mais preocupantes vieram de uma pesquisa da NSFOCUS: 76% das imagens do Docker Hub contêm vulnerabilidades de segurança, e 67% apresentam falhas de alto risco.
Eu também não dava muita importância a isso. Ao escrever um Dockerfile, usava FROM ubuntu, depois RUN apt-get install e pensava: está tudo dentro de um contêiner, então o isolamento deve ser suficiente. Essa percepção mudou quando um contêiner do ambiente de teste de carga foi invadido e vi, nos logs, os comandos usados pelo atacante para montar o disco do host.
Executar um contêiner como usuário não root é menos complicado do que parece. Vamos ver por que o root padrão é tão perigoso, como criar um usuário não root no Dockerfile, como usar o parâmetro --user e como configurar recursos que parecem avançados, como Capabilities e AppArmor. Ao final, você terá condições de reduzir em pelo menos 80% o risco de segurança dos contêineres em produção.
Por que não executar contêineres como root?
Escape de contêiner: basta um passo para sair do sandbox e chegar ao host
Muita gente trata contêiner como sinônimo de sandbox e acredita que nada feito dentro dele afeta o host. Na prática, o isolamento de contêineres depende de namespaces e cgroups do Linux, e não do isolamento em nível de hardware de uma máquina virtual. Se houver uma configuração incorreta ou uma vulnerabilidade no kernel, essa barreira pode se desfazer facilmente.
A CVE-2024-21626 é um exemplo claro. Pesquisadores descobriram que o runc, runtime usado por baixo do Docker, vazava um descritor de arquivo apontando para o sistema de arquivos do host ao processar o diretório de trabalho. Em termos simples, a falha permitia ao invasor ler e gravar qualquer arquivo do host a partir do contêiner, inclusive substituir executáveis críticos como /usr/bin/bash. Imagine sua aplicação web comprometida e, em seguida, todos os contêineres do host substituídos por malware de mineração. Isso não é uma hipótese: ataques desse tipo já aconteceram.
Um caminho de ataque mais comum envolve o modo --privileged. Esse parâmetro equivale a dizer ao Docker: “entregue ao contêiner todos os privilégios do host”. O usuário root dentro do contêiner passa a ter todas as capacidades do root do host: montar dispositivos, carregar módulos do kernel, alterar a configuração de rede e muito mais. Um relatório da NSFOCUS analisou um caso em que o invasor usou um contêiner privilegiado e executou apenas mount /dev/sda1 /mnt para montar o disco do host dentro do contêiner. Depois, adicionou um cron job para enviar dados periodicamente. Todo o processo levou menos de dez minutos.
Há ainda um risco especialmente discreto: montar o Docker Socket. Algumas pessoas querem gerenciar outros contêineres executando comandos Docker de dentro de um contêiner e, por conveniência, montam /var/run/docker.sock. Isso entrega ao contêiner a chave para controlar todos os demais contêineres do host. Depois de invadi-lo, um atacante pode criar um novo contêiner privilegiado, escapar e assumir o host. A equipe de segurança da Tencent Cloud já registrou uma cadeia de ataque desse tipo.
Por que o usuário root é a maior brecha de segurança?
O ponto central é: o root UID 0 dentro do contêiner e o root UID 0 do host são o mesmo usuário.
Você pode perguntar: o namespace não deveria isolá-los? Em parte, sim, mas o UID namespace não vem ativado por padrão, por questões de compatibilidade. Quando um processo roda como root dentro do contêiner, uma vulnerabilidade no kernel ou um erro de configuração que invalide o namespace faz com que esse processo também apareça como root no host. Em um teste que fiz, usei root em um contêiner com a Capability SYS_ADMIN para montar o procfs do host e, por meio de /proc/sys/kernel/core_pattern, gravar um shell reverso. Consegui privilégios de root no host. O processo foi bem mais simples do que eu imaginava.
Um relatório de segurança da Alibaba Cloud também aponta cinco causas principais de escapes de contêiner: vulnerabilidades do kernel, erros de configuração, imagens inseguras, abuso de privilégios e comunicação insegura entre contêineres. As quatro primeiras estão diretamente ligadas aos privilégios de root. Executar com um usuário não root reduz de forma significativa pelo menos três desses riscos.
Considere ainda um cenário real: muitas aplicações Node.js escutam na porta 80, mas o Linux exige privilégios de root para portas abaixo de 1024. Por isso, é comum executar a aplicação inteira como root. Se o código Express tiver uma vulnerabilidade de path traversal, o invasor poderá ler /etc/passwd e tentar acessar o host por SSH. Nesse caso, talvez nem seja necessário escapar do contêiner para atacar o host pela rede.
Parece assustador, mas a solução não é complicada. O princípio essencial é o menor privilégio: conceda somente os privilégios de que a aplicação precisa, em vez de recorrer ao root por padrão.
Configure um usuário não root a partir do Dockerfile
Configuração segura de usuário não root em contêineres Docker
Da criação de um usuário não root no Dockerfile aos parâmetros de segurança em runtime para reduzir em 80% o risco dos contêineres
Estimated time: PT30M
-
1
Step 1: Criar um usuário não root no Dockerfile
Crie um usuário e um grupo dedicados: -
2
Step 2: Resolver a vinculação de portas
Resolva a vinculação de portas: -
3
Step 3: Configurar parâmetros de segurança em runtime
Configure os parâmetros de segurança em runtime: -
4
Step 4: Controlar privilégios com Capabilities
Faça um controle detalhado das Capabilities: -
5
Step 5: Ativar o controle de acesso obrigatório do AppArmor ou SELinux
Use AppArmor, com o profile docker-default, no Debian/Ubuntu; use SELinux no RHEL/CentOS. O profile padrão já é bastante restritivo e raramente precisa ser personalizado. -
6
Step 6: Examinar imagens e monitorar continuamente
Use Trivy ou Docker scan para procurar vulnerabilidades nas imagens periodicamente. Vulnerabilidades de alto risco devem ser corrigidas antes da implantação. Monitore reinicializações anormais e consumo incomum de recursos e audite regularmente a configuração dos contêineres.
Como criar corretamente um usuário não root
Comece pela forma mais comum:
FROM node:18-alpine
# Crie um usuário e um grupo dedicados, com UID/GID fixos
RUN addgroup -g 5000 appgroup \
&& adduser -D -u 5000 -G appgroup appuser
# Defina o diretório de trabalho
WORKDIR /app
# Copie os arquivos e defina o proprietário: este passo é essencial
COPY --chown=appuser:appgroup package*.json ./
RUN npm install
COPY --chown=appuser:appgroup . .
# Mude para o usuário não root; todos os comandos seguintes serão executados como appuser
USER appuser
# Inicie a aplicação
CMD ["node", "server.js"]
Não parece complicado, mas cada linha tem um motivo.
Por que definir UID e GID? Ao usar useradd, muita gente deixa o sistema atribuir esses números automaticamente. O problema é que o UID automático pode variar entre contêineres. Quando um volume de dados é montado, um arquivo criado pelo contêiner A pode ficar inacessível no contêiner B. Com um UID fixo, como 5000, todos os contêineres usam a mesma identificação, reduzindo bastante os problemas de permissão.
O que há de especial em COPY --chown? Se você usar um COPY comum seguido de RUN chown, o Docker criará duas camadas de imagem: a primeira copia como root, deixando os arquivos sob propriedade de root; a segunda altera o proprietário com chown. O parâmetro --chown define o proprietário correto durante a própria cópia, economizando espaço e aumentando a segurança. Em um projeto, esqueci esse detalhe e a aplicação falhou na inicialização com “Permission denied”. Levei meia hora para descobrir que o problema era a propriedade dos arquivos.
A posição da instrução USER é fundamental. Os comandos anteriores ainda são executados como root, como RUN npm install, que precisa gravar em /app; somente os posteriores usam appuser. É comum colocar USER cedo demais e fazer todos os comandos de instalação seguintes falharem. Lembre-se: todas as operações que exigem root devem vir antes de USER.
Armadilhas comuns e como resolvê-las
Armadilha 1: vinculação de portas
Você muda tudo para um usuário não root, mas o contêiner falha ao iniciar: Error: listen EACCES: permission denied 0.0.0.0:80. Isso acontece porque portas abaixo de 1024 exigem privilégio.
Soluções:
- Use uma porta alta, opção recomendada: faça a aplicação escutar em 3000 ou 8080 e use Nginx ou um balanceador de carga como proxy reverso.
- Use a Capability NET_BIND_SERVICE: veremos esse recurso adiante; ele permite que um usuário não root vincule uma porta baixa.
# Faça a aplicação escutar na porta 3000
EXPOSE 3000
USER appuser
CMD ["node", "server.js"] # A aplicação escuta internamente na porta 3000
Depois, faça o mapeamento no docker-compose ou no K8s:
ports:
- "80:3000" # Mapeie a porta 80 do host para a porta 3000 do contêiner
Armadilha 2: gravação de logs e arquivos temporários
Em certa ocasião, mudei uma aplicação Python para um usuário não root e ela passou a falhar continuamente durante a inicialização. Depois de investigar, descobri que a aplicação tentava gravar logs em /var/log, mas appuser não tinha permissão.
# Crie o diretório de logs e conceda permissão ao usuário da aplicação
RUN mkdir -p /var/log/myapp && \
chown -R appuser:appgroup /var/log/myapp
USER appuser
Uma solução ainda melhor é fazer a aplicação gravar em stdout/stderr e deixar o Docker ou o K8s coletar os logs. Altere a configuração da aplicação:
# Não grave logs em arquivo
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
Armadilha 3: permissões incompatíveis em volumes montados
Se o diretório /data no host pertence a root e for montado no contêiner, appuser, com UID 5000, não conseguirá lê-lo.
# Exemplo incorreto
docker run -v /data:/app/data myapp
# appuser não consegue ler nem gravar em /app/data dentro do contêiner
Duas soluções:
# Solução 1: defina previamente as permissões no host
sudo chown -R 5000:5000 /data
# Solução 2: use um volume nomeado; o Docker gerencia as permissões
docker volume create appdata
docker run -v appdata:/app/data myapp
Parâmetros de segurança em runtime: —user e outros
Substitua a configuração da imagem com —user
Às vezes, você recebe uma imagem de terceiros cujo Dockerfile não tem a instrução USER e executa tudo como root. Reconstruir a imagem parece trabalhoso? O parâmetro --user permite especificar o usuário em runtime:
# Método 1: especifique diretamente UID:GID
docker run --user=1001:1001 nginx:latest
# Método 2: use o usuário atual do host, definido dinamicamente; útil em desenvolvimento
docker run --user="$(id -u):$(id -g)" -v "$PWD:/app" node:18 npm test
# Método 3: use um nome de usuário conhecido, desde que ele exista na imagem
docker run --user=nobody redis:alpine
Gosto especialmente do segundo método no desenvolvimento local. Se você executar testes dentro do contêiner para gerar relatórios, por exemplo, os arquivos serão criados no host com o seu próprio UID. Assim, não será necessário usar sudo para excluí-los.
Mas atenção: o parâmetro —user substitui a instrução USER do Dockerfile. Se a imagem já estiver configurada para um usuário não root e você usar --user=0:0, voltará ao root e perderá toda a proteção. Sempre verifique a configuração padrão da imagem ao usar esse parâmetro.
Sistema de arquivos somente leitura: o invasor não consegue gravar arquivos
Imagine que um invasor explore uma falha e entre no contêiner para instalar malware ou alterar arquivos de configuração. Se o sistema de arquivos for somente leitura, grande parte desse ataque deixará de funcionar.
# Configuração mais simples de somente leitura
docker run -d --read-only nginx:alpine
# Muitas aplicações precisam gravar arquivos temporários. Como resolver?
docker run -d \
--read-only \
--tmpfs /tmp \
--tmpfs /var/run \
nginx:alpine
O parâmetro --tmpfs monta um sistema de arquivos em memória cujo conteúdo desaparece quando o contêiner reinicia, sendo ideal para arquivos temporários. Configurei um serviço de API dessa forma: os logs vão para stdout, os dados de sessão ficam no Redis e a aplicação não precisa gravar nada de forma persistente. Mesmo que um invasor obtenha um shell, o sistema de arquivos somente leitura limita muito o que ele pode fazer.
Impeça a elevação de privilégios com no-new-privileges
Esse parâmetro impede que processos dentro do contêiner elevem privilégios por mecanismos como setuid e setgid. Em outras palavras, mesmo que exista um /bin/su com SUID configurado, o usuário não poderá usá-lo para virar root.
docker run --security-opt=no-new-privileges myapp
Em um teste, executei sudo em um contêiner com essa opção ativada e recebi imediatamente o erro “effective uid is not 0”. É uma proteção muito eficaz contra ataques de elevação de privilégio.
Configuração para produção: defesa em camadas
Ao combinar esses parâmetros, você obtém uma configuração de segurança bastante rígida:
docker run -d \
--name secure-webapp \
--user=5000:5000 \ # Usuário não root
--read-only \ # Sistema de arquivos somente leitura
--tmpfs /tmp:size=64M \ # 64 MB de espaço temporário
--security-opt=no-new-privileges \ # Impede elevação de privilégios
--cap-drop=ALL \ # Remove todas as Capabilities
--cap-add=NET_BIND_SERVICE \ # Adiciona somente o privilégio necessário para vincular a porta
-p 443:8443 \ # Mapeamento de porta
-v appdata:/app/data \ # Volume de dados, único local gravável
--memory=512m \ # Limite de memória
--cpus=1.0 \ # Limite de CPU
myapp:1.0.0
Há muitos parâmetros, mas cada um tem um objetivo claro. Uso esse modelo em vários serviços essenciais em produção há mais de dois anos, sem incidentes de segurança. O único custo é que a investigação de problemas fica um pouco mais trabalhosa: não é possível simplesmente executar um shell no contêiner e alterar arquivos para depurar. É uma troca que vale a pena.
No K8s, é possível configurar a mesma política no SecurityContext do Pod:
securityContext:
runAsNonRoot: true
runAsUser: 5000
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]
Controle detalhado de privilégios com Capabilities
O que são Capabilities e por que elas são mais seguras do que root?
O modelo tradicional de permissões do Linux parece binário: root pode fazer tudo, enquanto um usuário comum encontra várias restrições. As Capabilities dividem os superprivilégios de root em mais de 40 capacidades independentes, para que você conceda ao processo somente as necessárias.
Pense no root como um cartão mestre que abre todas as salas de uma empresa. As Capabilities são chaves separadas apenas para as salas em que você precisa entrar. Mesmo que um invasor roube suas chaves, ele só conseguirá acessar os espaços para os quais você recebeu permissão, e não a sala de servidores.
Por padrão, o Docker mantém 14 Capabilities nos contêineres, incluindo:
- CHOWN: alterar o proprietário de arquivos
- NET_BIND_SERVICE: vincular portas abaixo de 1024
- SETUID/SETGID: alterar o ID de usuário ou grupo
- KILL: enviar sinais a outros processos
- DAC_OVERRIDE: ignorar verificações de permissão de leitura e gravação em arquivos
Pode parecer pouco, e para a maioria das aplicações já é suficiente. Mas algumas Capabilities são especialmente perigosas e sempre devem ser removidas.
Lista de Capabilities perigosas: nunca conceda estas
SYS_ADMIN: equivale a boa parte dos poderes de root
Essa Capability permite fazer coisas demais: montar sistemas de arquivos, alterar namespaces, carregar módulos do kernel e muito mais. O pior caso que já vi envolvia um contêiner com SYS_ADMIN. Depois de entrar, o invasor usou o comando unshare para criar um novo mount namespace e montou o disco do host. Fim de jogo.
# Nunca faça isto!
docker run --cap-add=SYS_ADMIN myapp # ❌ Perigoso!
NET_ADMIN: controle da configuração de rede
Permite alterar tabelas de roteamento, configurar regras de firewall e capturar tráfego de rede. Não conceda essa Capability, a menos que o próprio contêiner seja uma ferramenta de rede, como uma VPN ou um roteador por software.
SYS_MODULE: carregamento de módulos do kernel
Isso significa literalmente inserir código no kernel. A gravidade do risco é evidente.
Configuração prática de menor privilégio
Estratégia 1: remova tudo e depois adicione somente o necessário, opção recomendada
docker run -d \
--cap-drop=ALL \ # Remove todas as Capabilities
--cap-add=NET_BIND_SERVICE \ # Adiciona apenas vinculação de portas, se necessário
--cap-add=CHOWN \ # Adiciona apenas alteração de proprietário, se necessário
myapp
Essa é a estratégia que mais uso. No começo, pode surgir um erro por falta de determinada Capability; nesse caso, adicione-a de acordo com a mensagem. Se a aplicação precisar mudar o usuário do processo por meio da chamada setuid(), por exemplo, receberá “Operation not permitted”, e então você poderá incluir --cap-add=SETUID.
Estratégia 2: remova apenas as perigosas, útil para um reforço rápido
docker run -d \
--cap-drop=SYS_ADMIN \
--cap-drop=NET_ADMIN \
--cap-drop=SYS_MODULE \
--cap-drop=SYS_RAWIO \
myapp
Essa abordagem é adequada quando você não sabe exatamente de quais Capabilities a aplicação precisa, mas quer eliminar rapidamente as mais perigosas.
Como descobrir de quais Capabilities a aplicação precisa?
Método 1: tentativa e erro, simples mas eficaz
# Primeiro passo: remova tudo e observe o erro
docker run --cap-drop=ALL myapp
# Erro: bind: permission denied
# Segundo passo: adicione NET_BIND_SERVICE
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myapp
# Inicialização bem-sucedida!
Método 2: análise com a ferramenta capsh
Execute capsh --print no contêiner para ver as Capabilities atuais:
$ docker run --rm -it --cap-drop=ALL ubuntu capsh --print
Current: =
# Vazio: nenhuma Capability
$ docker run --rm -it ubuntu capsh --print
Current: = cap_chown,cap_dac_override,cap_fowner,...
# As 14 Capabilities padrão
Método 3: consulte as necessidades de aplicações comuns
| Tipo de aplicação | Capabilities necessárias | Observação |
|---|---|---|
| Aplicação web em porta alta | Nenhuma | Portas 3000 ou superiores não exigem privilégio especial |
| Aplicação web em porta baixa | NET_BIND_SERVICE | Necessária para escutar nas portas 80 ou 443 |
| Banco de dados, como MySQL ou Postgres | Nenhuma | As portas padrão são altas |
| Nginx/Caddy | NET_BIND_SERVICE | Se escutarem diretamente nas portas 80 ou 443 |
| VPN ou ferramenta de rede | NET_ADMIN | Necessária para alterar rotas ou a configuração da interface de rede |
A maioria das aplicações de negócio funciona com todas as Capabilities removidas; no máximo, talvez seja necessário adicionar NET_BIND_SERVICE.
Controle de acesso obrigatório com AppArmor e SELinux
Para que servem? Uma explicação simples
As Capabilities controlam “quais operações um processo pode realizar”. AppArmor e SELinux vão além e controlam “quais arquivos e recursos um processo pode acessar”. Eles implementam controle de acesso obrigatório, ou MAC, no nível do sistema operacional. Mesmo um processo com privilégios de root precisa obedecer às regras do profile.
Pense no dono de uma empresa, que seria o root: ainda assim, ele precisa passar o cartão de acesso para entrar na sala de servidores. Se o cartão não tiver permissão, a porta não abre. AppArmor e SELinux funcionam como esse sistema de acesso.
Escolha conforme o sistema:
- Debian e Ubuntu usam AppArmor por padrão
- RHEL e CentOS usam SELinux por padrão
- Escolha apenas um; não ative os dois ao mesmo tempo, pois haverá conflitos
AppArmor: simples e suficiente para quem está começando
O Docker aplica automaticamente aos contêineres um profile chamado docker-default, que já é bastante restritivo. Na maioria dos casos, não é preciso configurar nada: ele protege o ambiente em segundo plano.
# Veja o profile AppArmor usado pelo contêiner
docker inspect mycontainer | grep -i apparmor
# "AppArmorProfile": "docker-default"
O que docker-default faz?
Ele impede que o contêiner:
- Monte sistemas de arquivos com mount
- Altere parâmetros do kernel, como arquivos em /proc/sys/
- Acesse dispositivos sensíveis do host, incluindo a maioria dos itens em /dev/
- Altere a própria configuração do AppArmor
Em um teste, mesmo como root dentro de um contêiner protegido pelo AppArmor, o comando mount /dev/sda1 /mnt retornou “Permission denied”. Com Capabilities e AppArmor em camadas, a dificuldade de escapar do contêiner aumenta exponencialmente.
SELinux: mais poderoso, porém mais complexo
O SELinux atribui um “rótulo” a cada arquivo e processo e usa políticas para determinar quais rótulos podem acessar quais recursos.
# Veja o rótulo SELinux do processo do contêiner
docker inspect mycontainer | grep -i selinux
# "ProcessLabel": "system_u:system_r:container_t:s0:c123,c456"
No rótulo, c123,c456 representa uma categoria. Cada contêiner recebe uma combinação exclusiva de categorias, garantindo que o contêiner A não consiga acessar os arquivos do contêiner B.
Para ser sincero, o SELinux tem uma curva de aprendizado alta e suas mensagens de erro não são amigáveis. No Ubuntu, AppArmor costuma ser suficiente. No RHEL, o SELinux já protege o sistema por padrão e, na maioria dos casos, não precisa ser alterado.
Recomendação prática: qual usar e como configurar?
Cenário 1: ambiente de desenvolvimento
- Pode ser desativado temporariamente, com
apparmor=unconfinedoulabel=disable, para facilitar a depuração - Antes disso, pense bem: se desativar durante os testes, você se lembrará de reativar em produção?
Cenário 2: ambiente de testes
- Deve permanecer ativado com o profile padrão
- O objetivo é detectar cedo conflitos entre a configuração de segurança e o funcionamento da aplicação
Cenário 3: ambiente de produção
- Deve permanecer ativado, sem exceções
- Use o profile padrão, a menos que haja um motivo sólido para personalizá-lo
- Audite os logs periodicamente para identificar tentativas de acesso proibidas
Na minha experiência, 99% dos eventos DENIED são acessos que deveriam mesmo ser bloqueados: ou são ataques, ou revelam um problema no projeto da aplicação. Casos que realmente exigem permissões mais amplas são raros.
Monte uma checklist completa de segurança
Reunindo tudo o que vimos, temos uma checklist prática. Ao segui-la, a segurança dos seus contêineres ficará à frente da grande maioria dos ambientes.
Etapa de construção da imagem
Verificação de segurança do Dockerfile:
- ✅ Use uma imagem base oficial ou confiável, e evite imagens de origem desconhecida
- ✅ Fixe a versão da imagem: use
node:18.17-alpine, e nãonode:latest - ✅ Crie um usuário não root dedicado e defina UID/GID
- ✅ Use
COPY --chownpara definir o proprietário dos arquivos - ✅ Coloque a instrução
USERdepois dos comandos de instalação e antes do comando de inicialização - ✅ Faça a aplicação escutar em uma porta alta, 3000 ou superior, ou configure Capabilities
- ✅ Use build multi-stage para reduzir o tamanho da imagem e a superfície de ataque
- ✅ Não inclua informações confidenciais na imagem; chaves e senhas devem entrar por variáveis de ambiente ou secrets
Etapa de análise da imagem
Análises de segurança obrigatórias:
# Use o scan incluído no Docker, baseado em Snyk
docker scan myapp:latest
# Ou use Trivy, mais rápido e abrangente, opção recomendada
trivy image myapp:latest
# Ou use Clair, integrado ao CI/CD
# Configure a análise automática no repositório Harbor
Lembre-se: 76% das imagens no Docker Hub têm vulnerabilidades. Análises periódicas não são opcionais, mas obrigatórias. Nossa equipe segue estas regras:
- Vulnerabilidades de alto risco devem ser corrigidas antes da implantação
- Vulnerabilidades de risco médio exigem avaliação e monitoramento
- Vulnerabilidades de baixo risco são registradas e revisadas periodicamente
Verificação da configuração em runtime
Modelo de docker-compose para produção:
services:
myapp:
image: myapp:1.0.0
user: "5000:5000" # Usuário não root
read_only: true # Sistema de arquivos somente leitura
tmpfs:
- /tmp:size=64M # Montagem temporária em memória
security_opt:
- no-new-privileges:true # Impede elevação de privilégios
- apparmor=docker-default # Profile AppArmor
cap_drop:
- ALL # Remove todas as Capabilities
cap_add:
- NET_BIND_SERVICE # Adiciona somente o necessário
volumes:
- appdata:/app/data:rw # Declara explicitamente permissão de leitura e gravação
deploy:
resources:
limits:
cpus: '1.0' # Limite de CPU
memory: 512M # Limite de memória
ports:
- "8080:8080"
Verificações operacionais em produção
Monitoramento diário:
- ✅ Monitore reinicializações anormais de contêineres, que podem indicar falhas provocadas por um ataque
- ✅ Monitore uso anormal de recursos, pois malware de mineração consome toda a CPU
- ✅ Ative logs de auditoria para registrar operações com contêineres
Auditoria periódica:
- ✅ Analise mensalmente as imagens em execução, e não apenas as imagens novas
- ✅ Verifique se algum contêiner usa
--privilegedou Capabilities perigosas - ✅ Revise as políticas de rede e as portas expostas pelos contêineres
Problemas comuns e soluções
P1: o que fazer se a aplicação falhar com erro de permissão depois da mudança para não root?
Etapas de diagnóstico:
- Leia a mensagem de erro específica e identifique se o problema está em permissões de arquivo ou na vinculação de portas
- Se for uma permissão de arquivo, verifique
--chowne as permissões dos diretórios no Dockerfile - Se for a vinculação de uma porta, use uma porta alta ou adicione a Capability NET_BIND_SERVICE
Erros comuns e correções:
# Erro: Error: EACCES: permission denied, open '/app/logs/app.log'
# Causa: o diretório de logs não permite gravação por appuser
# Correção:
RUN mkdir -p /app/logs && chown appuser:appgroup /app/logs
# Erro: Error: listen EACCES: permission denied 0.0.0.0:80
# Causa: um usuário não root não pode vincular uma porta baixa
# Correção 1: faça a aplicação escutar na porta 3000 e use mapeamento de portas
EXPOSE 3000
# Correção 2: adicione a Capability
docker run --cap-add=NET_BIND_SERVICE myapp
P2: como resolver permissões incompatíveis depois de montar um volume de dados?
Esse é o problema mais frequente. Há três soluções:
Solução 1: defina UID/GID previamente no host, opção recomendada
# Defina o proprietário do diretório no host como 5000:5000, igual ao UID do usuário no contêiner
sudo chown -R 5000:5000 /data
docker run -v /data:/app/data myapp
Solução 2: use um volume nomeado e deixe o Docker gerenciar as permissões
docker volume create --opt o=uid=5000,gid=5000 appdata
docker run -v appdata:/app/data myapp
P3: em quais situações o root é realmente necessário? Quase nenhuma
É comum pensar que certos cenários exigem root, mas quase sempre existe uma alternativa:
| Cenário | Alternativa sem root |
|---|---|
| Vincular as portas 80 ou 443 | Use a Capability NET_BIND_SERVICE ou faça a aplicação escutar em uma porta alta e deixe o balanceador de carga mapear a porta |
| Instalar pacotes do sistema | Instale-os no Dockerfile antes da instrução USER; não instale pacotes em runtime |
| Alterar a configuração do sistema | Injete a configuração por variáveis de ambiente ou arquivos de configuração, em vez de alterá-la em runtime |
| Acessar o Docker Socket | É extremamente perigoso. Se for indispensável, considere usar a API do Docker ou do K8s em vez de montar o socket |
O único caso razoável que encontrei para root foi uma ferramenta legada de migração de banco de dados que exigia root por decisão fixa do fornecedor e cujo código não podia ser alterado. A solução foi isolá-la em um contêiner descartável separado, destruído assim que a migração terminou, em vez de mantê-la em execução.
P4: o que fazer quando uma imagem de terceiros usa root?
Prioridades, da mais alta para a mais baixa:
- Procure uma versão oficial não root; muitas imagens oferecem as tags
-rootlessou-nonroot - Substitua o usuário com o parâmetro
--user
docker run --user=65534:65534 third-party-image # 65534 corresponde ao usuário nobody
- Crie um novo Dockerfile baseado na imagem original e adicione USER
FROM third-party-image:latest
RUN adduser -D -u 5000 appuser
USER appuser
- Peça ao mantenedor da imagem uma versão não root e contribua com a comunidade
Conclusão
Depois de tudo isso, a ideia principal cabe em uma frase: executar um contêiner como root é deixar uma porta dos fundos para o invasor.
Retomando os pontos essenciais:
- Escapes de contêiner não são apenas teoria; a CVE-2024-21626 e a montagem de discos por contêineres privilegiados são caminhos reais de ataque
- Adicionar a instrução USER ao Dockerfile e o parâmetro —user em runtime custa pouco e traz um grande benefício
- As Capabilities permitem controlar privilégios com precisão; remover todas e adicionar apenas as necessárias é a melhor prática
- Sistema de arquivos somente leitura, no-new-privileges e AppArmor formam uma defesa em camadas
- 76% das imagens têm vulnerabilidades, portanto análises periódicas são indispensáveis
A partir de hoje, faça três coisas:
- Verifique seus Dockerfiles e adicione USER aos que ainda não tiverem essa instrução
- Audite o ambiente de produção, localize todos os contêineres executados como root ou com
--privilegede corrija imediatamente os que puder - Crie um processo de análise de imagens para que a verificação de segurança faça parte do CI/CD
Segurança não é uma tarefa única, mas um processo contínuo. Ainda assim, dar o primeiro passo e mudar os contêineres de root para um usuário não root já coloca você à frente da maioria. Não espere sofrer um ataque para se arrepender: o caso da empresa daquele amigo serve como alerta.
FAQ
Por que não se deve executar um contêiner Docker como root?
Falhas de escape de contêiner, como a CVE-2024-21626, mostram que o isolamento não é absoluto. No Docker Hub, 76% das imagens contêm vulnerabilidades e 67% apresentam falhas de alto risco. Executar o contêiner como usuário não root pode reduzir o risco de segurança em 80%.
Como configurar um usuário não root no Dockerfile?
Todas as operações que exigem privilégios de root devem vir antes de USER. Definir UID/GID fixos também evita problemas de permissão ao montar volumes de dados.
O que fazer quando o usuário não root não consegue vincular as portas 80 ou 443?
1) Usar uma porta alta, opção recomendada:
• Faça a aplicação escutar na porta 3000 ou 8080
• Use Nginx ou um balanceador de carga como proxy reverso
• Mapeie a porta 80 do host para a porta 3000 do contêiner
2) Usar a Capability NET_BIND_SERVICE:
• Permite que o usuário não root vincule portas baixas
• Evita executar a aplicação como root apenas para escutar nas portas 80 ou 443
O que são Capabilities e como configurar o menor privilégio possível?
Boas práticas:
• Comece com --cap-drop=ALL para remover todas as Capabilities
• Depois adicione somente as necessárias, como NET_BIND_SERVICE para vincular portas
• Nunca conceda Capabilities perigosas, como SYS_ADMIN, NET_ADMIN ou SYS_MODULE
A maioria das aplicações de negócio funciona sem nenhuma Capability; no máximo, pode ser necessário adicionar NET_BIND_SERVICE.
Quais parâmetros de segurança devem ser configurados em produção?
• --user=5000:5000 para usar um usuário não root
• --read-only com --tmpfs para montar diretórios temporários em um sistema de arquivos somente leitura
• --security-opt=no-new-privileges para impedir elevação de privilégios
• --cap-drop=ALL para remover todas as Capabilities e adicionar somente as necessárias
• AppArmor ou SELinux para controle de acesso obrigatório
No K8s, as mesmas políticas podem ser configuradas no SecurityContext do Pod.
Como resolver permissões incompatíveis depois de montar um volume de dados?
1) Definir UID/GID previamente no host, opção recomendada:
• sudo chown -R 5000:5000 /data
• Garanta que os valores coincidam com o UID do usuário no contêiner
2) Usar um volume nomeado e deixar o Docker gerenciar as permissões:
• docker volume create --opt o=uid=5000,gid=5000 appdata
3) Criar o diretório no Dockerfile e conceder permissão ao usuário da aplicação
Como descobrir de quais Capabilities uma aplicação precisa?
1) Tentativa e erro:
• Comece com --cap-drop=ALL e observe o erro
• Adicione uma Capability de acordo com a mensagem recebida
2) Análise com a ferramenta capsh:
• Execute docker run --rm -it ubuntu capsh --print para ver as Capabilities atuais
3) Consulte necessidades comuns:
• Aplicações web em portas altas não precisam de privilégios especiais
• Aplicações web em portas baixas precisam de NET_BIND_SERVICE
• VPNs e ferramentas de rede precisam de NET_ADMIN
23 min de leitura · Publicado em: 18 dez 2025 · Atualizado em: 4 set 2026
Guia prático Docker
Se você chegou pela busca, o caminho mais rápido é ir para o post anterior ou próximo desta série.
Anterior
Segurança de imagens Docker: varredura e correção com Trivy e integração ao CI/CD
76% das imagens no Docker Hub têm vulnerabilidades de segurança. Este guia mostra como usar o Trivy, corrigir falhas de forma sistemática e automatizar a varredura no CI/CD, com comandos completos para criar aplicações em contêiner mais seguras.
Parte 26 de 34
Próximo
Guia completo de Docker Secrets: práticas recomendadas para proteger senhas e chaves de API em contêineres
Pare de colocar senhas de banco de dados no Dockerfile. Este guia explica como proteger informações sensíveis com Docker Secrets, compara Docker, Kubernetes e Vault e traz um checklist completo para produção.
Parte 28 de 34



Comentários
Entre com GitHub para comentar