Alternar tema

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

Easton editorial illustration: environment switchboard

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.

76%
Imagens com vulnerabilidades
Estatísticas do Docker Hub
67%
Vulnerabilidades de alto risco
Pesquisa da NSFOCUS
80%
Redução de risco
Configuração com usuário não root
Source: Relatório de pesquisa da NSFOCUS

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. 1

    Step 1: Criar um usuário não root no Dockerfile

    Crie um usuário e um grupo dedicados:
  2. 2

    Step 2: Resolver a vinculação de portas

    Resolva a vinculação de portas:
  3. 3

    Step 3: Configurar parâmetros de segurança em runtime

    Configure os parâmetros de segurança em runtime:
  4. 4

    Step 4: Controlar privilégios com Capabilities

    Faça um controle detalhado das Capabilities:
  5. 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. 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:

  1. 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.
  2. 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çãoCapabilities necessáriasObservação
Aplicação web em porta altaNenhumaPortas 3000 ou superiores não exigem privilégio especial
Aplicação web em porta baixaNET_BIND_SERVICENecessária para escutar nas portas 80 ou 443
Banco de dados, como MySQL ou PostgresNenhumaAs portas padrão são altas
Nginx/CaddyNET_BIND_SERVICESe escutarem diretamente nas portas 80 ou 443
VPN ou ferramenta de redeNET_ADMINNecessá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=unconfined ou label=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ão node:latest
  • ✅ Crie um usuário não root dedicado e defina UID/GID
  • ✅ Use COPY --chown para definir o proprietário dos arquivos
  • ✅ Coloque a instrução USER depois 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 --privileged ou 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:

  1. Leia a mensagem de erro específica e identifique se o problema está em permissões de arquivo ou na vinculação de portas
  2. Se for uma permissão de arquivo, verifique --chown e as permissões dos diretórios no Dockerfile
  3. 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árioAlternativa sem root
Vincular as portas 80 ou 443Use 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 sistemaInstale-os no Dockerfile antes da instrução USER; não instale pacotes em runtime
Alterar a configuração do sistemaInjete 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:

  1. Procure uma versão oficial não root; muitas imagens oferecem as tags -rootless ou -nonroot
  2. Substitua o usuário com o parâmetro --user
docker run --user=65534:65534 third-party-image  # 65534 corresponde ao usuário nobody
  1. Crie um novo Dockerfile baseado na imagem original e adicione USER
FROM third-party-image:latest
RUN adduser -D -u 5000 appuser
USER appuser
  1. 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:

  1. Verifique seus Dockerfiles e adicione USER aos que ainda não tiverem essa instrução
  2. Audite o ambiente de produção, localize todos os contêineres executados como root ou com --privileged e corrija imediatamente os que puder
  3. 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?
O root UID 0 dentro do contêiner e o root UID 0 do host são o mesmo usuário, pois o UID namespace não vem ativado por padrão. Se uma vulnerabilidade do kernel ou um erro de configuração fizer o namespace falhar, o processo root do contêiner também terá privilégios de root no host.

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?
Crie um usuário e um grupo dedicados, com UID/GID fixos, como 5000; use COPY --chown=appuser:appgroup para definir o proprietário dos arquivos; e coloque a instrução USER depois dos comandos de instalação e antes do comando de inicialização.

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?
Há duas opções:

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?
As Capabilities dividem os superprivilégios de root em mais de 40 capacidades independentes, permitindo conceder ao processo apenas as que ele realmente precisa.

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?
Uma configuração de segurança para produção combina:
• --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?
Há três soluções:

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?
Há três métodos:

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

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog