Conteúdo

Tutorial de segurança no Docker: 10 práticas para proteger contêineres

Os contêineres oferecem certo nível de isolamento, mas isso não garante segurança. Estas são proteções básicas que você deve aplicar do desenvolvimento à implantação, cada uma acompanhada de resultados de execução reais.

1. Não execute como root

$ docker run --rm alpine:3.23 id
uid=0(root) gid=0(root) groups=0(root),0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

$ docker run --rm -u 10001:10001 alpine:3.23 id
uid=10001 gid=10001 groups=10001

$ docker run --rm node:24-alpine id node
uid=1000(node) gid=1000(node) groups=1000(node),1000(node)
  • Por padrão, o usuário dentro do contêiner é root (uid=0). Se uma vulnerabilidade no programa for explorada, o invasor terá privilégios máximos no contêiner e, combinando isso com outras vulnerabilidades, poderá afetar o host.
  • -u 10001:10001 define o usuário durante a execução, sem exigir alterações na imagem.
  • Muitas imagens oficiais já incluem um usuário comum, como node na imagem node (uid 1000) e nonroot nas imagens distroless.

A melhor prática é trocar de usuário diretamente no Dockerfile:

RUN adduser -D -u 10001 app      # Alpine
# RUN useradd --system --uid 10001 app   # Debian / Ubuntu
USER app

Veja exemplos práticos em aplicações Python e aplicações Node.js.

2. Use imagens base confiáveis e fixe a versão

  • Dê preferência às Docker Official Images (por exemplo, python, node, nginx) ou a imagens publicadas por organizações conhecidas.
  • Não use imagens de origem desconhecida, principalmente se o nome for parecido com o de uma imagem popular (por exemplo, ngnix).
  • Fixe a versão, por exemplo, node:24-alpine ou, com mais precisão, node:24.21-alpine, em vez de node ou latest. Para fixar completamente, adicione o digest: node:24-alpine@sha256:....

3. Deixe a imagem o menor possível

Cada pacote na imagem pode ter vulnerabilidades. Use slim, alpine, distroless ou builds em vários estágios e mantenha apenas o necessário para a execução. Quanto menos ferramentas desnecessárias (compiladores, curl, ssh), menos opções o invasor terá para explorá-las e mais limpos serão os resultados da varredura de vulnerabilidades.

4. Não inclua segredos na imagem

Teste prático: os segredos permanecem na imagem

O Dockerfile a seguir comete três erros comuns:

FROM alpine:3.23
ARG NPM_TOKEN
ENV DB_PASSWORD=super-secret
COPY .env /app/.env
RUN echo "use the token: $NPM_TOKEN" > /dev/null && rm /app/.env
CMD ["sh"]

A última linha remove .env depois do uso, então parece estar tudo certo. Faça o build e confira:

$ docker build -q -f Dockerfile.bad --build-arg NPM_TOKEN=npm_abc123secret -t leaky .
sha256:c9523bb4d0484e3abf049d15dec57df3282ff8203574c03127a8dfc6ee37bf9b

$ docker run --rm leaky ls /app

$ docker inspect -f "{{.Config.Env}}" leaky
[PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin DB_PASSWORD=super-secret]

$ docker history --no-trunc --format "{{.CreatedBy}}" leaky | head -n 4
CMD ["sh"]
RUN |1 NPM_TOKEN=npm_abc123secret /bin/sh -c echo "use the token: $NPM_TOKEN" > /dev/null && rm /app/.env # buildkit
COPY .env /app/.env # buildkit
ENV DB_PASSWORD=super-secret
  • ls /app não mostra nada; parece que .env foi removido.
  • Mas docker inspect mostra diretamente o DB_PASSWORD=super-secret definido por ENV.
  • docker history mostra o NPM_TOKEN=npm_abc123secret passado durante o build: o valor de ARG fica registrado na camada em que é usado.

E quanto ao .env? Uma imagem é formada por várias camadas sobrepostas. rm apenas marca o arquivo como “excluído” em uma nova camada; o conteúdo continua na camada de COPY. Exporte a imagem com docker save e localize .env entre as camadas:

$ docker save leaky -o leaky.tar && mkdir x && tar -xf leaky.tar -C x && for l in x/blobs/sha256/*; do tar -tzf $l 2>/dev/null | grep -q "app/.env" && tar -xzOf $l app/.env; done; rm -rf x leaky.tar
API_KEY=sk-live-1234567890abcdef

O conteúdo completo de .env foi extraído. Qualquer pessoa que consiga baixar essa imagem pode fazer o mesmo. Ao enviar uma imagem para um Registry, presuma que todos podem ver o conteúdo de cada camada.

Detecte problemas antecipadamente com docker build --check

docker build --check identifica ARG e ENV que parecem conter segredos com base nos nomes das variáveis:

$ docker build --check -f Dockerfile.bad . 2>&1 | sed -n "/Check complete/,\$p"
Check complete, 2 warnings have been found!

WARNING: SecretsUsedInArgOrEnv - https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
Do not use ARG or ENV instructions for sensitive data (ARG "NPM_TOKEN")
Dockerfile.bad:2
--------------------
   1 |     FROM alpine:3.23
   2 | >>> ARG NPM_TOKEN
   3 |     ENV DB_PASSWORD=super-secret
   4 |     COPY .env /app/.env
--------------------

WARNING: SecretsUsedInArgOrEnv - https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
Do not use ARG or ENV instructions for sensitive data (ENV "DB_PASSWORD")
Dockerfile.bad:3
--------------------
   1 |     FROM alpine:3.23
   2 |     ARG NPM_TOKEN
   3 | >>> ENV DB_PASSWORD=super-secret
   4 |     COPY .env /app/.env
   5 |     RUN echo "use the token: $NPM_TOKEN" > /dev/null && rm /app/.env
--------------------

NPM_TOKEN e DB_PASSWORD foram sinalizados. Mas a verificação considera apenas os nomes de ARG e ENV; ela não detecta problemas como copiar arquivos secretos para a imagem com COPY .env. Por isso, .dockerignore continua sendo necessário.

Boas práticas

  • Nunca passe senhas ou chaves de API para o Dockerfile usando ENV ou ARG.
  • Use .dockerignore para excluir .env, chaves privadas e .git, evitando que sejam copiados para dentro da imagem por COPY . ..
  • Passe os segredos necessários em tempo de execução usando variáveis de ambiente, --env-file ou secrets do Compose. Consulte Variáveis de ambiente e arquivo .env.
  • Para segredos necessários durante o build (por exemplo, um token para instalar pacotes privados), use a montagem de secrets do BuildKit:
FROM alpine:3.23
RUN --mount=type=secret,id=npm_token \
    test -s /run/secrets/npm_token && echo "token length: $(wc -c < /run/secrets/npm_token)"
CMD ["sh"]
$ NPM_TOKEN=npm_abc123secret docker build --progress plain --no-cache --secret id=npm_token,env=NPM_TOKEN -f Dockerfile.good -t safe . 2>&1 | grep "token length"
#5 [stage-0 2/2] RUN --mount=type=secret,id=npm_token     test -s /run/secrets/npm_token && echo "token length: $(wc -c < /run/secrets/npm_token)"
#5 0.261 token length: 16

$ docker history --no-trunc --format "{{.CreatedBy}}" safe | head -n 2
CMD ["sh"]
RUN /bin/sh -c test -s /run/secrets/npm_token && echo "token length: $(wc -c < /run/secrets/npm_token)" # buildkit

$ docker run --rm safe ls /run/secrets
ls: /run/secrets: No such file or directory
  • Durante o build, RUN consegue ler o token de /run/secrets/npm_token (com 16 caracteres).
  • Em docker history, aparece apenas o próprio comando, sem o valor do token.
  • A imagem também não contém /run/secrets; o secret é montado somente durante a execução daquele RUN.

5. Não exponha portas desnecessárias à internet

# Ruim: banco de dados aberto para o mundo todo
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=secret postgres:18

# Bom: apenas conexões locais são permitidas
docker run -d -p 127.0.0.1:5432:5432 -e POSTGRES_PASSWORD=secret postgres:18

No Linux, o Docker modifica as regras do iptables diretamente, e as portas abertas com -p ignoram as configurações de firewalls como ufw e firewalld: mesmo que o ufw não permita a porta 5432, conexões externas ainda podem chegar. A opção mais segura é não usar -p nos serviços internos, como bancos de dados e caches, permitindo conexões apenas de outros contêineres pela rede do Docker. Por exemplo, em Nginx como proxy reverso, somente o Nginx tem ports.

6. Restrinja as permissões do contêiner

Sistema de arquivos somente para leitura

$ docker run --rm --read-only alpine:3.23 touch /tmp/x
touch: /tmp/x: Read-only file system

$ docker run --rm --read-only --tmpfs /tmp alpine:3.23 sh -c "touch /tmp/x && echo ok"
ok

--read-only torna o sistema de arquivos do contêiner somente para leitura, impedindo que um invasor modifique o programa ou grave arquivos maliciosos. Para os locais em que o programa precisa gravar (por exemplo, /tmp), monte separadamente uma área temporária na memória com --tmpfs ou monte um Volume.

Remova Linux capabilities

No Linux, as permissões de root são divididas em várias capabilities, como CHOWN (alterar o proprietário de um arquivo) e NET_BIND_SERVICE (vincular portas abaixo de 1024). Por padrão, o Docker concede algumas capabilities ao contêiner:

$ docker run --rm alpine:3.23 chown nobody /etc/hostname && echo "chown ok"
chown ok

$ docker run --rm --cap-drop ALL alpine:3.23 chown nobody /etc/hostname
chown: /etc/hostname: Operation not permitted

$ docker run --rm --cap-drop ALL --cap-add CHOWN alpine:3.23 chown nobody /etc/hostname && echo "chown ok"
chown ok
  • Por padrão, chown pode ser executado.
  • Depois que --cap-drop ALL remove todas as capabilities, até o root recebe Operation not permitted.
  • Use --cap-add para adicionar de volta somente o que for necessário.

A maioria dos aplicativos web não precisa de nenhuma capability. Você pode usar --cap-drop ALL diretamente.

Impedir a elevação de privilégios

$ docker run --rm alpine:3.23 grep NoNewPrivs /proc/self/status
NoNewPrivs:	0

$ docker run --rm --security-opt no-new-privileges alpine:3.23 grep NoNewPrivs /proc/self/status
NoNewPrivs:	1

NoNewPrivs: 1 indica que os processos no contêiner não podem obter privilégios maiores por meio de programas setuid (como sudo e su). Também é recomendável ativar essa opção ao executar o contêiner como um usuário comum.

Juntando tudo:

docker run -d \
  --user 10001:10001 \
  --read-only --tmpfs /tmp \
  --cap-drop ALL \
  --security-opt no-new-privileges \
  myapp

Em Compose:

services:
  app:
    image: myapp
    user: "10001:10001"
    read_only: true
    tmpfs:
      - /tmp
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true

Nunca use --privileged sem necessidade: ele concede todas as capabilities ao contêiner e permite acesso a todos os dispositivos do host, praticamente desativando todo o isolamento.

7. Limite os recursos

$ docker run --rm --pids-limit 50 alpine:3.23 sh -c "for i in \$(seq 100); do sleep 5 & done; wait" 2>&1 | sort | uniq -c
      1 sh: can't fork: Resource temporarily unavailable

$ docker run -d --name limited --memory 512m --cpus 1 --pids-limit 200 nginx:1.30-alpine
WARNING: Your kernel does not support swap limit capabilities or the cgroup is not mounted. Memory limited without swap.
2edbff0c81d73b83f0055b781d8d8ad20661b2b64ef1dfd5a6def334ef02723c

$ docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.PIDs}}" limited
NAME      CPU %     MEM USAGE / LIMIT   PIDS
limited   0.00%     3.406MiB / 512MiB   2

$ docker inspect -f "Memory={{.HostConfig.Memory}} NanoCpus={{.HostConfig.NanoCpus}} PidsLimit={{.HostConfig.PidsLimit}}" limited
Memory=536870912 NanoCpus=1000000000 PidsLimit=200

$ docker rm -f limited
limited
  • Com --pids-limit 50, quando o contêiner tenta criar 100 processos, o limite é excedido e aparece can't fork: Resource temporarily unavailable. Isso impede que uma fork bomb derrube o host inteiro.
  • --memory 512m --cpus 1 limita a memória e a CPU. O LIMIT exibido por docker stats é 512MiB. Se um processo ultrapassar o limite de memória, o sistema o encerra (OOM), em vez de deixar a memória do host inteiro se esgotar.
  • A mensagem WARNING: Your kernel does not support swap limit capabilities indica que o kernel do host não tem a limitação de swap ativada. O limite de memória continua valendo, mas não é possível limitar também o swap; você pode ignorar esse aviso.

Em Compose:

services:
  app:
    image: myapp
    mem_limit: 512m
    cpus: 1
    pids_limit: 200

8. Proteja o socket do Docker

/var/run/docker.sock é a interface de controle do Docker. Quem pode acessá-lo pode criar qualquer contêiner, inclusive montando o diretório raiz do host. Na prática, montamos / do host no contêiner:

$ docker run --rm -v /:/host alpine:3.23 sh -c "head -n 2 /host/etc/os-release; ls /host/var/lib/docker | head -n 5"
NAME="Alpine Linux"
ID=alpine
buildkit
containerd
containers
engine-id
image

O contêiner leu diretamente /etc/os-release e /var/lib/docker do host Docker (o ambiente de teste deste tutorial usa Alpine Linux). O mesmo método também permite ler e gravar em qualquer arquivo, como /etc/shadow e chaves SSH. Portanto:

  • Poder executar comandos docker equivale a ter privilégios de root no host. Adicionar um usuário ao grupo docker é o mesmo que conceder a ele acesso root.
  • Não monte -v /var/run/docker.sock:/var/run/docker.sock em um contêiner sem necessidade, a menos que você confie totalmente naquela imagem (por exemplo, ferramentas que precisam desse acesso, como Traefik, Portainer e Trivy).
  • Não exponha a porta TCP do daemon do Docker (2375) na rede: isso equivale a expor os privilégios de root do host.
  • Se precisar de um isolamento mais forte, pesquise o modo rootless do Docker.

9. Analise vulnerabilidades em imagens

A ferramenta de código aberto Trivy pode analisar vulnerabilidades conhecidas nos pacotes do sistema operacional e da linguagem de programação presentes em uma imagem. Execute-a em um contêiner, sem precisar instalá-la:

$ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v trivy-cache:/root/.cache aquasec/trivy:latest image --quiet --scanners vuln --severity HIGH,CRITICAL nginx:1.30-alpine

Report Summary

┌───────────────────────────────────┬────────┬─────────────────┐
│              Target               │  Type  │ Vulnerabilities │
├───────────────────────────────────┼────────┼─────────────────┤
│ nginx:1.30-alpine (alpine 3.24.2) │ alpine │        3        │
└───────────────────────────────────┴────────┴─────────────────┘
Legend:
- '-': Not scanned
- '0': Clean (no security findings detected)


nginx:1.30-alpine (alpine 3.24.2)
=================================
Total: 3 (HIGH: 3, CRITICAL: 0)

┌──────────┬─────────────────┬──────────┬────────┬───────────────────┬───────────────┬─────────────────────────────────────────────────────────┐
│ Library  │  Vulnerability  │ Severity │ Status │ Installed Version │ Fixed Version │                          Title                          │
├──────────┼─────────────────┼──────────┼────────┼───────────────────┼───────────────┼─────────────────────────────────────────────────────────┤
│ libexpat │ CVE-2026-93990  │ HIGH     │ fixed  │ 2.8.4-r0          │ 2.8.5-r0      │ expat: Expat: XML Injection via Malformed UTF-16 Input  │
│          │                 │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2026-93990              │
├──────────┼─────────────────┤          │        ├───────────────────┼───────────────┼─────────────────────────────────────────────────────────┤
│ pcre2    │ CVE-2026-103111 │          │        │ 10.48-r0          │ 10.49-r0      │ pcre2: pcre2: Out-of-bounds write via crafted regular   │
│          │                 │          │        │                   │               │ expression                                              │
│          │                 │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2026-103111             │
├──────────┼─────────────────┤          │        ├───────────────────┼───────────────┼─────────────────────────────────────────────────────────┤
│ tiff     │ CVE-2026-4775   │          │        │ 4.7.1-r0          │ 4.7.2-r0      │ libtiff: libtiff: Arbitrary code execution or denial of │
│          │                 │          │        │                   │               │ service via signed integer...                           │
│          │                 │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2026-4775               │
└──────────┴─────────────────┴──────────┴────────┴───────────────────┴───────────────┴─────────────────────────────────────────────────────────┘
  • --severity HIGH,CRITICAL exibe apenas vulnerabilidades de alta gravidade ou superior; --quiet oculta o progresso do download do banco de dados de vulnerabilidades.
  • Montar docker.sock permite que o Trivy leia as imagens locais; o Volume trivy-cache guarda o banco de dados de vulnerabilidades, então não é preciso baixá-lo novamente na próxima análise.
  • O resultado mostra 3 vulnerabilidades de nível HIGH na Alpine base de nginx:1.30-alpine. Todas têm Status como fixed, e o relatório lista a Fixed Version: isso significa que a Alpine já publicou as correções, mas esta imagem ainda não as incluía quando foi construída.

Os resultados da análise mudam com o tempo: novas vulnerabilidades são descobertas todos os dias, e as imagens são atualizadas continuamente. É provável que os resultados sejam diferentes quando você executar a análise.

O Docker Desktop e as versões recentes da CLI também incluem o Docker Scout (docker scout quickview, docker scout cves), que você pode usar depois de entrar na sua conta Docker. Recomendamos automatizar a análise no fluxo de CI e impedir a implantação quando forem encontradas vulnerabilidades graves.

10. Atualizações regulares

As imagens base continuam recebendo correções de vulnerabilidades. Dando continuidade ao resultado da verificação acima, atualize os pacotes no Dockerfile e faça uma nova verificação:

$ cat Dockerfile
FROM nginx:1.30-alpine
RUN apk upgrade --no-cache

$ docker build -q -t nginx-patched .
sha256:c88ea56e7062e7703dccc5eb2fedea74c333f33982d9735753056a50858646f2

$ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v trivy-cache:/root/.cache aquasec/trivy:latest image --quiet --scanners vuln --severity HIGH,CRITICAL nginx-patched

Report Summary

┌───────────────────────────────┬────────┬─────────────────┐
│            Target             │  Type  │ Vulnerabilities │
├───────────────────────────────┼────────┼─────────────────┤
│ nginx-patched (alpine 3.24.2) │ alpine │        0        │
└───────────────────────────────┴────────┴─────────────────┘
Legend:
- '-': Not scanned
- '0': Clean (no security findings detected)

Depois que apk upgrade atualiza os pacotes vulneráveis para versões corrigidas, o número de vulnerabilidades HIGH e CRITICAL passa a ser 0.

Na prática, é melhor reconstruir a imagem periodicamente com docker build --pull (--pull baixa primeiro a versão mais recente da imagem base) e fazer um novo deploy. Você pode usar ferramentas como Dependabot e Renovate para receber alertas automáticos sobre novas versões das imagens base. Uma imagem construída uma única vez e deixada sem atualização por vários anos só acumula cada vez mais vulnerabilidades.

Lista de verificação

  • O Dockerfile define USER e não executa como root
  • A origem da imagem base é confiável e a versão está fixada
  • Use uma imagem enxuta ou um build em vários estágios
  • Não há senhas, chaves nem .env na imagem (verifique docker history e docker inspect)
  • Use RUN --mount=type=secret para segredos durante o build
  • Portas de serviços internos, como bancos de dados, não estão expostas externamente
  • Quando apropriado, use --read-only, --cap-drop ALL e no-new-privileges
  • Não use --privileged nem monte docker.sock sem necessidade
  • Defina limites de memória, CPU e PID
  • Faça a verificação de vulnerabilidades das imagens no CI
  • Reconstrua periodicamente com --pull para obter atualizações de segurança

Resumo

  • Adicione USER ao Dockerfile para executar como um usuário diferente de root.
  • ARG, ENV e arquivos excluídos continuam na imagem; use --secret para segredos durante o build.
  • Não publique serviços que não precisam de acesso externo com -p; -p ignora o ufw.
  • Ter acesso a docker.sock equivale a ser root no host.
  • Faça verificações regulares e reconstrua com --pull.

Exercícios

Exercício 1: use docker history --no-trunc para verificar se há alguma senha ou token na imagem que você construiu.

Dica

Confira o conteúdo dos comandos ARG, ENV e RUN na coluna CREATED BY, além de docker inspect -f "{{json .Config.Env}}" imagem. Se encontrar algo, passe a usar --secret ou variáveis de ambiente em tempo de execução e revogue as chaves que foram expostas.

Perguntas frequentes

O root dentro do contêiner é o mesmo root do host?

No modo geral, é o mesmo uid, mas fica limitado por namespaces, capabilities e seccomp. Para que não seja o root do host, use o [modo Rootless](/pt/docker/rootless-podman/).

O que faço se a varredura da imagem encontrar muitas vulnerabilidades?

Comece pelas vulnerabilidades de alto risco com Fixed Version: reconstrua a imagem (docker build --pull) ou atualize os pacotes. Para as que não têm versão corrigida, avalie se você realmente vai usar o pacote.

Qual é a diferença entre Docker Scout e Trivy?

Ambos são ferramentas de varredura de vulnerabilidades. O Scout é integrado ao Docker Desktop e exige login; o Trivy é uma ferramenta de código aberto que pode ser executada em um contêiner.

Atualizado 14 de outubro de 2026