Programação Por Equipe DevBras

Capa do artigo: Container Docker sem root: usuário, USER e volumes

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

  1. 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.
  2. 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.
  3. Deixe o sistema de arquivos só de leitura. Com read_only, nada fora de volumes e tmpfs pode ser alterado. Monte um tmpfs para /tmp se a aplicação precisar.
  4. Remova capacidades. cap_drop: ALL tira todas as capacidades de kernel que sobraram. Acrescente de volta só a que você provar que precisa.
  5. Bloqueie a escalada. no-new-privileges impede 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 777 na pasta montada. Qualquer processo do host e de qualquer container com acesso escreve ali.
  • Montar o socket do Docker. Quem acessa /var/run/docker.sock cria um container privilegiado e vira root no host, com ou sem USER.
  • Usar --privileged para resolver permissão. Devolve todas as capacidades e acesso aos dispositivos. Anula todo o resto.
  • Usuário criado, mas sem USER. O useradd está no Dockerfile, o processo continua como root. Só o id conta 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 USER com usuário sem privilégio e UID fixo.
  • docker compose exec api id não mostra uid=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: ALL e no-new-privileges ativos, 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?

Avalie este artigo

Referência: https://docs.docker.com/develop/develop-images/dockerfile_best-practices/