Pular para o conteúdo
9 min de leitura

Segurança de containers: Docker sem furos do build ao runtime

Por Lucas Andrade ·

Container não é uma VM. Veja como fechar os furos de imagem, privilégio, segredo e supply chain que transformam um Docker mal configurado em root no host.

Neste artigo

Um time subiu um serviço em container, seguindo o tutorial padrão: uma imagem base popular, o Dockerfile que copia o código, um docker run e pronto. Funcionou. O que ninguém percebeu é que a aplicação rodava como root dentro do container, a imagem base carregava mais de duzentas vulnerabilidades conhecidas em pacotes que o serviço nem usava, a chave de API do banco estava embutida numa camada da imagem via ARG, e o socket do Docker fora montado no container para uma tarefa de conveniência. Cada uma dessas decisões, isolada, parecia inofensiva. Juntas, elas significavam que comprometer aquele serviço web equivalia a comprometer o host inteiro e todos os outros containers nele. O container dava uma falsa sensação de isolamento que não existia.

Container é uma ferramenta excelente de empacotamento e reprodutibilidade, mas a segurança dele não vem de fábrica — vem de uma série de decisões deliberadas no build e no runtime. Este artigo percorre essas decisões.

O container não é uma máquina virtual#

O erro conceitual que origina quase todos os outros é tratar container como se fosse VM. Uma máquina virtual tem seu próprio kernel e um hypervisor que a isola do host por hardware; escapar dela é difícil. Um container compartilha o kernel do host — o isolamento é feito por recursos do próprio kernel Linux (namespaces, que dão a ilusão de processos, rede e filesystem próprios, e cgroups, que limitam recursos). Isso é leve e rápido, mas significa que a superfície entre o container e o host é o kernel inteiro. Uma vulnerabilidade de escape de container, um privilégio mal configurado, uma capability perigosa: qualquer um pode transformar o comprometimento de um container no comprometimento do host.

Entender isso reorienta a mentalidade. Você não confia no isolamento como confiaria numa VM; você o reforça em camadas, assumindo que cada barreira pode falhar e que a próxima precisa segurar. Defense-in-depth aplicada a container significa imagem mínima, privilégio mínimo, e supply chain verificada — nas três frentes ao mesmo tempo.

A imagem: menos coisa, menos risco#

Toda vulnerabilidade num pacote da imagem é uma vulnerabilidade que você carrega, mesmo que a aplicação nunca chame aquele pacote. Uma imagem base "cheia", com uma distribuição completa e ferramentas de conveniência, traz centenas de bibliotecas — cada uma um CVE potencial e um utilitário que o atacante usa depois de entrar (um curl, um bash, um compilador para montar o próximo estágio do ataque). A defesa é a imagem mínima: bases enxutas reduzem drasticamente a contagem de pacotes, e imagens distroless levam isso ao extremo, contendo apenas o runtime da aplicação e nada de shell ou gerenciador de pacotes. Um atacante que cai num container distroless não encontra nem um sh para executar o próximo comando.

O multi-stage build é o mecanismo que torna isso prático sem sacrificar a ergonomia. Você usa uma imagem completa, com todas as ferramentas de compilação, no estágio de build, e copia apenas o artefato final — o binário, os assets — para uma imagem final mínima. As ferramentas de build, as dependências de desenvolvimento e o código-fonte não vão para produção. A imagem que roda é pequena, tem pouca superfície e sobe mais rápido.

Escaneie a imagem e conheça o que está dentro#

Não dá para proteger o que você não enxerga. Ferramentas de scan de imagem analisam cada camada, identificam os pacotes e cruzam com bases de vulnerabilidade, apontando os CVEs presentes. Isso entra no pipeline: uma imagem com vulnerabilidade crítica conhecida não deveria chegar a produção. Complementarmente, gerar um SBOM (a lista de materiais de software) da imagem dá a você um inventário — quando um novo CVE crítico for divulgado numa biblioteca, você consulta o SBOM e sabe em minutos quais imagens estão afetadas, em vez de auditar tudo à mão.

Privilégio: nada roda como root sem motivo#

O padrão histórico do Docker é rodar o processo como root dentro do container, e root no container é, por padrão, perigosamente próximo de root no host. A primeira e mais importante correção é criar um usuário sem privilégios na imagem e declarar USER para que a aplicação rode como ele. A maioria dos serviços web não precisa de root para escutar numa porta alta e processar requisições; rodar como root é conveniência que vira vetor. Indo além, o modo rootless do Docker executa o próprio daemon sem privilégios de root no host, reduzindo o impacto de um escape.

Depois do usuário, vêm as capabilities. O kernel Linux fragmenta os poderes de root em capabilities individuais, e um container recebe um conjunto por padrão que é mais amplo do que quase qualquer aplicação precisa. A prática correta é dropar todas (--cap-drop ALL) e adicionar de volta apenas as estritamente necessárias, que na maioria dos serviços são nenhuma ou quase nenhuma. Combine com --security-opt no-new-privileges, que impede o processo de ganhar privilégios novos via binários setuid, e você fechou uma classe inteira de escalonamento.

Ainda no runtime: um filesystem read-only (--read-only, com volumes graváveis explícitos só onde a aplicação de fato escreve) impede o atacante de plantar um binário ou modificar a aplicação em execução. Perfis de seccomp restringem quais chamadas de sistema o container pode fazer ao kernel — reduzindo a superfície de escape — e AppArmor ou SELinux adicionam controle de acesso mandatório por cima. Nenhuma dessas medidas é exótica; todas são flags e perfis que levam minutos para aplicar e mudam materialmente o custo de um comprometimento.

Limites de recurso contra negação de serviço#

Um container sem limites de CPU e memória pode consumir tudo o que o host tem, derrubando os vizinhos — seja por bug, seja por um atacante que induz uma carga de trabalho cara. Definir limites via cgroups (memória, CPU, número de processos) contém o estrago de um container a ele mesmo e é parte da higiene de isolamento, não um detalhe de performance.

Segredo nunca mora na imagem#

Este é o erro que mais vaza credencial em container. As camadas de uma imagem são imutáveis e persistem no histórico: se você passou um segredo via ARG no build, ou fez COPY de um arquivo .env, ou setou uma variável com a chave e depois "removeu" numa camada seguinte, o segredo continua lá, recuperável por qualquer um que baixe a imagem e inspecione o histórico das camadas. docker history e a extração das camadas revelam tudo. Remover numa camada posterior não apaga a anterior.

Segredo entra em runtime, nunca em build-time e nunca na imagem. As formas corretas são montar o segredo como arquivo em tempo de execução a partir de um gerenciador de segredos, ou injetá-lo pelo orquestrador no momento em que o container sobe, mantendo-o fora do sistema de arquivos da imagem. Variável de ambiente é aceitável para injeção em runtime, mas com cuidado: ela é visível para o processo e frequentemente vaza em logs e em endpoints de introspecção, então segredo de alto valor prefere o arquivo montado. O princípio é simples e inegociável: a imagem é um artefato distribuível e público por natureza; nada que seja secreto pode estar dentro dela.

A supply chain da imagem#

Você quase nunca constrói a partir do zero — parte de uma imagem base que alguém publicou. Isso é uma dependência com todos os riscos de supply chain. Puxar uma imagem por tag mutável (:latest, :1.2) significa que o conteúdo por trás daquele nome pode mudar sem aviso, inclusive para uma versão comprometida; referenciar a imagem por digest (o hash imutável do conteúdo) garante que você roda exatamente o que auditou. Imagens de origem não confiável podem trazer malware embutido, mineradores ou backdoors — verifique a procedência e prefira fontes oficiais ou espelhos internos que você controla.

Assinatura de imagem fecha o ciclo: ferramentas de assinatura permitem que o produtor da imagem a assine e que o cluster verifique a assinatura antes de rodar, recusando qualquer imagem não assinada ou adulterada em trânsito. Isso transforma "confio que essa imagem é a que eu construí" de esperança em garantia criptográfica.

O socket do Docker é a porta dos fundos#

Um padrão perigoso e comum: montar /var/run/docker.sock dentro de um container para que ele possa gerenciar outros containers. Quem controla o socket do Docker controla o daemon, e o daemon roda como root no host — então qualquer processo com acesso ao socket pode subir um novo container privilegiado que monta o filesystem do host e faz o que quiser. Montar o socket do Docker num container equivale a dar root no host para aquele container. Se você precisa de orquestração, use a API do orquestrador com credenciais de escopo mínimo, nunca o socket cru.

Containers que rodam IA#

Serviços que carregam e servem modelos de IA merecem atenção redobrada nas mesmas frentes. O peso do modelo é uma dependência de supply chain — baixar um modelo de origem não verificada pode significar executar código malicioso durante o carregamento, dependendo do formato de serialização. O container que serve inferência costuma ter acesso a dados sensíveis e a recursos caros de computação, o que o torna alvo tanto de exfiltração quanto de abuso de custo. Aplique o mesmo hardening: imagem mínima, usuário não-root, capabilities dropadas, filesystem read-only onde possível, limites de recurso, e verificação de procedência tanto do código quanto dos pesos.

O checklist mental#

Segurança de container se resolve com disciplina em três eixos que você percorre em toda imagem que sobe. No build: base mínima, multi-stage, nenhum segredo em camada, imagem escaneada e com SBOM. No runtime: usuário não-root, cap-drop ALL com adição mínima, no-new-privileges, filesystem read-only, seccomp/AppArmor, limites de recurso. Na supply chain: imagem por digest e não por tag mutável, procedência verificada, assinatura conferida, e o socket do Docker jamais montado sem entender que isso é root no host. Nenhum desses passos é caro; o que é caro é descobrir, depois do incidente, que o "isolamento" do container era uma ilusão que ninguém tinha reforçado.

Leituras relacionadas

Nenhum comentário ainda

Seja o primeiro a comentar.

Deixe seu comentário

Entre com sua conta Canverly para comentar. Você pode usar a mesma conta em qualquer site da rede.

Entrar com Canverly