Container Docker sem root: usuário, USER e volumes
Container Docker sem root: crie um usuário com UID fixo, use a instrução USER, acerte o dono dos volumes e confirme com id que o processo não roda como root.
A aplicação sobe como root porque um dia a porta 80 e a pasta de log pediram. O hábito ficou. Agora um defeito de upload que deixa o cliente escolher o caminho do arquivo escreve em qualquer lugar que o root alcança dentro do container, inclusive no código da própria aplicação. A resposta curta: crie um usuário sem privilégio na imagem, rode o processo com ele usando a instrução USER, dê a esse usuário dono apenas das pastas que precisam ser graváveis e escute numa porta alta atrás do proxy. Depois confirme com id dentro do container. Se voltar uid=0, ainda é root.
Por que root no container é root demais
Sem a instrução USER, o processo roda como o usuário 0. O container isola com namespaces e cgroups, e o Docker já remove várias capacidades do root por padrão, como explica a página de segurança do Docker Engine. Mesmo assim, dentro do container esse processo pode alterar qualquer arquivo da imagem, instalar pacote, ler qualquer segredo montado e usar as capacidades que sobraram.
O risco maior aparece quando algo dá errado em outra camada. Se o container tem um volume do host montado, o root do container é dono de tudo nesse volume do ponto de vista do host, porque o usuário 0 de dentro é o mesmo usuário 0 de fora (a não ser que você use o modo rootless ou o remapeamento de usuários). Se existir uma falha de escape no kernel ou no runtime, sair como root é muito pior do que sair como um usuário comum.
Rodar sem root não torna o container seguro sozinho. Reduz o estrago de um defeito que, em algum momento, vai existir. O exemplo clássico é o upload: um arquivo malicioso que consegue escrever no disco faz muito menos estrago quando o processo só pode gravar numa pasta, e menos ainda se o upload for regravado e servido de um lugar que não executa, como mostramos em upload de imagem seguro.
Como criar e usar um usuário sem privilégio
Imagens que já trazem um usuário
Muitas imagens oficiais têm um usuário pronto. As imagens do Node trazem o node; as do ASP.NET a partir do .NET 8 trazem o app. Nesses casos basta declarar:
FROM node:22-slim WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci --omit=dev COPY . . USER node CMD ["node", "server.js"]
Criando o usuário numa imagem Debian
FROM debian:trixie-slim RUN groupadd --system --gid 10001 app \ && useradd --system --uid 10001 --gid app --no-create-home --shell /usr/sbin/nologin app WORKDIR /app COPY ./bin/ /app/ RUN mkdir -p /app/uploads && chown app:app /app/uploads USER app EXPOSE 8080 CMD ["/app/servidor"]
O exemplo usa a Debian 13 (trixie), a versão estável atual. Em imagens Alpine, o equivalente é addgroup -S app && adduser -S -G app app.
Repare no que não foi feito: o código em /app continua pertencendo ao root, só com permissão de leitura para os outros. O usuário app é dono apenas de /app/uploads. Se você usar COPY --chown=app:app no código inteiro, o processo passa a poder reescrever o próprio executável, que é exatamente o que você queria evitar.
Passo a passo para endurecer o container
- Fixe o UID. Um número conhecido, como 10001, facilita acertar o dono de pastas no host e evita colisão com usuários do sistema.
- Escute em porta alta. A aplicação ouve na 8080 e o proxy do host, ou o mapeamento de portas do Docker, entrega na 443 para fora. Isso elimina o motivo mais comum para rodar como root.
- Deixe o sistema de arquivos só de leitura. Com
read_only, nada fora de volumes e tmpfs pode ser alterado. Monte um tmpfs para/tmpse a aplicação precisar. - Remova capacidades.
cap_drop: ALLtira todas as capacidades de kernel que sobraram. Acrescente de volta só a que você provar que precisa. - Bloqueie a escalada.
no-new-privilegesimpede que um binário com setuid devolva privilégio ao processo.
Todas essas chaves estão na referência de serviços do Compose. E, já que o processo pode ler qualquer arquivo montado, mantenha senhas e chaves fora da imagem, como explicamos em segredo fora da imagem.
services:
api:
image: registry.exemplo.com.br/loja/api:2.3.0
user: "10001:10001"
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
ports:
- "127.0.0.1:8080:8080"
volumes:
- uploads:/app/uploads
volumes:
uploads:
E se a aplicação precisar ouvir numa porta abaixo de 1024? No Docker a partir da versão 20.10, o container já nasce com net.ipv4.ip_unprivileged_port_start=0, então um usuário sem privilégio consegue abrir a porta 80 dentro dele sem nada extra. Isso não vale com network_mode: host nem em todo runtime: no Kubernetes ou em outro motor, a regra clássica volta, e porta baixa pede a capacidade NET_BIND_SERVICE e, para usuário não root, setcap no executável. A porta alta funciona igual em qualquer lugar. Prefira ela e deixe a 443 com o proxy.
Acertando o dono dos volumes
Volume nomeado novo, montado numa pasta que já existe na imagem, é inicializado com o conteúdo e o dono dessa pasta. Por isso o chown no Dockerfile resolve o caso do uploads acima. Bind mount é diferente: a pasta do host mantém o dono do host. Se ela pertence ao root, o processo recebe permissão negada. Ajuste pelo número, que é o que o kernel compara:
sudo mkdir -p /srv/loja/uploads sudo chown -R 10001:10001 /srv/loja/uploads
Não resolva com chmod 777. Isso troca um problema de permissão por um problema de segurança.
Como confirmar que deu certo
docker compose exec api id
docker image inspect --format '{{.Config.User}}' registry.exemplo.com.br/loja/api:2.3.0
docker top $(docker compose ps -q api)
O primeiro comando deve mostrar uid=10001(app). O segundo mostra o usuário padrão gravado na imagem; resposta vazia significa root. O terceiro lista os processos do container vistos pelo host, com o usuário de cada um. Teste também a escrita: tentar criar um arquivo em /app deve falhar, e em /app/uploads deve funcionar.
Em Kubernetes, os campos equivalentes ficam no securityContext: runAsNonRoot, readOnlyRootFilesystem e allowPrivilegeEscalation. O runAsNonRoot tem a vantagem de recusar subir um container que tentaria rodar como root.
Erros comuns
- Dar ao usuário a posse do código. Um defeito de escrita passa a poder alterar o próprio programa, e a alteração vale até o próximo deploy.
chmod 777na pasta montada. Qualquer processo do host e de qualquer container com acesso escreve ali.- Montar o socket do Docker. Quem acessa
/var/run/docker.sockcria um container privilegiado e vira root no host, com ou semUSER. - Usar
--privilegedpara resolver permissão. Devolve todas as capacidades e acesso aos dispositivos. Anula todo o resto. - Usuário criado, mas sem
USER. Ouseraddestá no Dockerfile, o processo continua como root. Só oidconta a verdade. - UID aleatório. Cada rebuild pode gerar outro número, e o bind mount que funcionava passa a negar acesso.
Checklist
- A imagem tem instrução
USERcom usuário sem privilégio e UID fixo. docker compose exec api idnão mostrauid=0.- O código da aplicação pertence ao root e não é gravável pelo processo.
- Só as pastas de dado têm o usuário da aplicação como dono.
- A aplicação escuta numa porta acima de 1024.
read_only,cap_drop: ALLeno-new-privilegesativos, com exceções justificadas.- Nenhum container monta o socket do Docker sem motivo documentado.
Aplicando em vários containers sem susto
Trocar o usuário de um container que já está no ar costuma quebrar alguma coisa na primeira tentativa, quase sempre um volume com o dono errado. Vale fazer um serviço por vez e ter o caminho de volta pronto. O Dockyard, painel Docker da DevBras, mostra containers e stacks de vários servidores num lugar só, com papéis de acesso e atualização com rollback, o que ajuda a aplicar a mudança stack por stack e voltar à versão anterior se a permissão falhar. Quem quiser testar pode pedir um container grátis pelo suporte@devbras.com. Para comparar com outras opções, veja Portainer, Dockge, Komodo e Dockyard.
Rode id dentro de cada container que está no ar agora. Quantos respondem com zero?
Referência: https://docs.docker.com/develop/develop-images/dockerfile_best-practices/