15 out 2026 · segurança, linux, containers · 5 min
Como rodar um contêiner sem root de verdade
Um contêiner sem user roda com 14 capacidades do kernel e escreve em /etc sem reclamar. Trocando só o user para 1000, as capacidades efetivas vão a zero e a escrita em /etc passa a falhar. É o teste que cabe numa tela de terminal, e é o que o compose.yaml desta postagem monta.
O que cada trava resolve
user troca o dono do processo. cap_drop: ALL tira as capacidades que sobrariam. read_only transforma a imagem em sistema de arquivos que não aceita escrita. no-new-privileges impede que nada dentro do processo vire root durante a execução.
O kernel explica a parte mais importante: root é um usuário comum com um pacote extra de capacidades anexado. Capabilities são exatamente esse pacote. Tirar as capacidades é tirar o root.
O guia de segurança do Docker já vem dizendo isso desde sempre: por padrão o motor mantém uma lista curta de capacidades e solta o resto. O endurecimento a seguir não inventa nada, ele fecha a lista até o fim.
O que o leitor precisa ver aí: as capacidades caem de 14 para 0 já na segunda coluna, sem depender do cap_drop. E a última coluna mostra o detalhe que mais derruba serviço em produção, o volume.
O artefato
O compose.yaml abaixo é o arquivo inteiro, comentado linha por linha. Ele precisa de um Dockerfile de quatro linhas ao lado, que cria o usuário sem privilégio e o diretório /dados já com o dono certo.
services:
app:
build: .
image: exemplo-endurecido:1
# 1. uid e gid do processo. A imagem precisa ter esse usuario.
user: "10001:10001"
# 2. nenhuma capacidade do kernel sobrando.
cap_drop:
- ALL
# 3. sistema de arquivos de leitura: /etc, /usr e o resto fecham.
read_only: true
# 4. ninguem vira root durante a execucao.
security_opt:
- no-new-privileges:true
# escrita que pode existir, guardada em memoria e morta com o container
tmpfs:
- /tmp:rw,size=16m,mode=1777
- /run:rw,size=4m
# volume nomeado. O dono do volume tem de bater com o user acima.
volumes:
- dados:/dados
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
volumes:
dados:
E o Dockerfile que ele espera:
FROM debian:stable-slim
# usuario sem privilegio e diretorio de dados com o dono certo
RUN groupadd -g 10001 app \
&& useradd -u 10001 -g 10001 -M -s /usr/sbin/nologin app \
&& mkdir -p /dados && chown 10001:10001 /dados
USER 10001:10001
WORKDIR /app
CMD ["sleep", "infinity"]
Repare no chown da terceira linha. Ele resolve a quebra mais comum, e ela não tem nada a ver com segurança.
Sobre /dados valem as mesmas regras de qualquer volume: ele só vale alguma coisa se a cópia for testada. O backup com rsync restaurável mostra o teste que leva poucos minutos.
Como verificar
O verifica-contexto.sh abaixo é o script que roda lá dentro. Ele não instala nada e não escreve fora das pastas testadas. Ele é também o que produz a figura.
#!/bin/sh
# verifica-contexto.sh: mostra quem o processo e, dentro do container.
# Copie para dentro do container e rode com: sh verifica-contexto.sh
# Nao instala nada e nao escreve nada fora das pastas testadas.
id
grep -E '^(CapEff|NoNewPrivs)' /proc/self/status
# conta os bits ligada em CapEff: root normal vem com 14
c=$(awk '/^CapEff/{print $2}' /proc/self/status)
n=0
for i in $(seq 0 63); do
n=$((n + ((0x$c >> i) & 1)))
done
echo "caps efetivos: $n"
# escrita em cada pasta: a imagem aceita ou esta fechada?
for dir in /etc /var/lib /tmp /dados; do
[ -d "$dir" ] || continue
if touch "$dir/sonda.txt" 2>/dev/null; then
echo "escrita em $dir: deu certo"
else
echo "escrita em $dir: falhou"
fi
done
Suba o serviço e pergunte ao processo quem ele é:
docker compose up -d
docker compose exec app sh /app/verifica-contexto.sh
A saída esperada é o uid 10001, CapEff: 0000000000000000, NoNewPrivs: 1, falha em /etc e em /var/lib, e sucesso em /tmp e /dados. Se aparecer qualquer bit diferente de zero em CapEff, alguma trava não chegou ao container.
Para ver o antes, basta tirar uma linha do compose. Sem user, o mesmo script volta com uid 0, 14 capacidades e a escrita em /etc dada como certa.
Agora a parte que assusta: monte o mesmo volume sem --user e o processo root escreve em /dados sem uma palavra. Volte a rodar como 1000 e o mesmo volume dá Permission denied, porque o diretório continua pertencendo ao root. O volume nomeado não sabe qual uid o serviço vai usar.
Se der errado
O apt-get update morre com E: List directory /var/lib/apt/lists/partial is missing. Acquire (13: Permission denied). Causa: o read_only fechou o caminho que o apt usa. Correção: pacote se instala no Dockerfile, nunca no container em execução.
O processo reinicia sem dizer por quê. Causa: tmpfs sem espaço, ou volume sem permissão. Correção: aumentar size do tmpfs e conferir o dono com ls -ld /dados.
A imagem não sobe com no-new-privileges. Causa: entrada em /etc/shadow ou setcap, que dependem de elevação. Correção: trocar por USER fixo no Dockerfile, que resolve o mesmo problema.
Tabela
| Trava | O que tira | Custo |
|---|---|---|
user |
root vira usuário comum | 0 ms, capacidades caem para 0 |
cap_drop: ALL |
o resto das capacidades | 0 ms |
read_only |
escrita na imagem | 0 ms, quebra apt em execução |
no-new-privileges |
elevação durante o run | 0 ms |
| tmpfs em /tmp | escrita que some com o container | sai da RAM |
O tmpfs sai da RAM de verdade, e some quando o container morre. Para ver isso de perto com número, o post sobre auditoria de memória mostra quanto custa um processo que cresce sem teto.
FAQ
Preciso de --privileged para algo de rede? Só se o programa mexe em interface, rota ou porta baixa. Para quase tudo, rede comum e cap_add: NET_BIND_SERVICE bastam.
Como descubro o uid certo da minha imagem? docker run --rm imagem cat /etc/passwd e procure a conta que o serviço já usa. Reaproveitar conta da imagem evita o useradd no Dockerfile.
cap_drop: ALL não quebra TLS? Não. TLS não usa capacidade do kernel, usa socket comum.
Funciona igual em Kubernetes? A lógica é a mesma, com securityContext no manifesto em vez do compose. Os campos readOnlyRootFilesystem e runAsNonRoot dizem a mesma coisa.
Por que o cap_drop parece redundante com o user? Parece, e na prática ele é a rede de segurança. Se um dia o user ficar de fora por engano, o cap_drop ainda segura.
Fontes
Guia de segurança do Docker, referência de serviços do Compose e capabilities(7), do manual do Linux. Para a outra ponta da mesma ideia, fora do contêiner, o SSH em vez de senha mostra o que muda quando a identidade deixa de ser root.