Como estamos construindo imagens com zero CVEs #1: reduzindo a superfície de ataque (Alpine e distroless)

Como estamos construindo imagens com zero CVEs #1: reduzindo a superfície de ataque (Alpine e distroless)

Esta é a primeira publicação de uma série técnica sobre as decisões de engenharia por trás das imagens do Quor. Cada artigo vai explorar uma camada específica da nossa abordagem para entregar imagens de container com CVEs próximas de zero. Começamos pelo mais fundamental: a escolha da imagem base.

Esta é a primeira publicação de uma série técnica sobre as decisões de engenharia por trás das imagens do Quor. Cada artigo vai explorar uma camada específica da nossa abordagem para entregar imagens de container com CVEs próximas de zero. Começamos pelo mais fundamental: a escolha da imagem base.

Security Researcher

Heitor Gouvêa

Quando uma equipe faz docker pull node:lts e usa essa imagem como base, ela herda, silenciosamente, mais do que o runtime do Node.js. A imagem node:22 (alias de node:lts) é construída sobre buildpack-deps, uma imagem de propósito geral que inclui ferramentas de build para múltiplos ecossistemas de linguagem. O resultado: 661 pacotes instalados, incluindo Python completo, GCC, Make e dezenas de bibliotecas de desenvolvimento. Nenhuma delas necessária para executar uma aplicação Node.js em produção.

Isso não é um detalhe. É a causa estrutural de boa parte das CVEs que aparecem nos scanners de segurança das equipes de engenharia.

Superfície de ataque em containers: o que isso significa na prática

Superfície de ataque é o conjunto de pontos de entrada que um atacante pode explorar para comprometer um sistema. Em containers, cada componente presente na imagem é um ponto de entrada potencial:

  • Pacotes de sistema operacional com vulnerabilidades conhecidas (CVEs);

  • Shells interativos (bash, sh) que facilitam execução de comandos arbitrários após uma exploração inicial;

  • Package managers (apt, apk, pip) que podem ser usados para instalar ferramentas adicionais dentro do container comprometido;

  • Utilitários de rede (curl, wget, netcat) que facilitam exfiltração de dados ou download de payloads;

  • Compiladores e ferramentas de build que permitem compilar código malicioso dentro do ambiente.

A lógica é: cada componente a mais é um vetor a mais. Um container que roda apenas o binário da aplicação e as bibliotecas estritamente necessárias tem uma superfície de ataque ordens de magnitude menor do que um container baseado em uma distribuição completa.

Comparando variantes de imagens Node.js 22 (Active LTS):

Imagem

Pacotes

CVEs reportadas

Tamanho

node:22 (fat)

~413

~36

~1.64GB

registry.quor.dev/default/node:20-alpine

~ 50

~0 reportadas

~54 MB

A diferença entre node:22 e uma imagem distroless não é marginal. É uma ordem de grandeza em número de pacotes e CVEs reportadas.

Quor Newsletter

Updates on software supply chain security.

Updates on software supply chain security.

O que é Alpine

Alpine Linux é uma distribuição construída com um objetivo específico: ser pequena, simples e segura. Ao contrário de Debian e Ubuntu, que foram projetadas para uso geral em servidores e desktops, Alpine nasceu para ambientes embarcados e containers.

Seus pilares técnicos:

  • musl libc em vez de glibc: implementação alternativa e mais enxuta da biblioteca C padrão;

  • BusyBox: substituição de centenas de utilitários GNU por um único binário modular;

  • apk: gerenciador de pacotes próprio, significativamente mais simples que apt/yum;

  • Base mínima: a imagem base oficial tem menos de 8 MB e inclui apenas o essencial para um shell funcional.

Por que Alpine reduz CVEs estruturalmente

Quando uma imagem é baseada em Debian, ela herda o repositório de pacotes do Debian. Esse repositório é enorme e mantido para cobrir uma variedade enorme de casos de uso. Muitos pacotes têm CVEs abertas que simplesmente demoram para ser corrigidas no ciclo de release da distribuição. Alpine, por ser menor e mais focada, tem menos pacotes expostos. Além disso, sua base musl/BusyBox tem historicamente menos vulnerabilidades do que os equivalentes GNU. Na prática, uma imagem node:22-alpine parte de uma base que normalmente tem zero CVEs reportadas, contra dezenas ou centenas na variante Debian.

Trade-offs que você precisa conhecer

Alpine não é uma solução universal. Há trade-offs reais:

  1. musl vs glibc: Algumas aplicações têm comportamentos diferentes quando compiladas contra musl. Em especial, aplicações que dependem de comportamentos específicos de glibc (locale handling, DNS resolver, comportamento de certas chamadas de sistema) podem apresentar inconsistências. Para a maioria das aplicações web backend, isso não é um problema prático.

  2. Módulos nativos Node.js: Se sua aplicação depende de módulos que precisam ser compilados a partir de C++ (como bcrypt, node-sqlite3, sharp), Alpine pode complicar o processo de build. 

A solução padrão é usar multi-stage build: Alpine (ou uma imagem mais completa) no estágio de build, Alpine mínima no estágio de runtime.

Suporte oficial: Builds Node.js para Alpine ainda têm status experimental upstream. Para uso em produção crítica, isso é um ponto a considerar.

O que é distroless

Distroless é um conceito mais radical que Alpine. Em vez de partir de uma distribuição Linux enxuta, a abordagem distroless remove a própria noção de distribuição da imagem final. Uma imagem distroless contém:

  • O runtime da linguagem (Node.js, JVM, Python, Go binary);

  • As bibliotecas de sistema estritamente necessárias (libc, libssl, libgcc);

  • Certificados TLS;

  • Fuso horário e locale básicos.

Uma imagem distroless não contém:

  • Shell (bash, sh, busybox);

  • Package manager (apt, apk, pip);

  • Utilitários de sistema (curl, wget, ls, ps, top);

  • Qualquer outra ferramenta que não seja necessária para executar a aplicação.

Google Distroless (gcr.io/distroless é o projeto original (alguns fabricantes distribuem forks), mantido pelo Google. Imagens construídas com Bazel diretamente a partir de pacotes Debian, sem incluir o sistema de pacotes em si. Produz imagens extremamente pequenas sem nenhum shell.

Por que distroless reduz a superfície de ataque mais do que Alpine

A ausência de shell é a diferença fundamental. Em termos de segurança:

Sem shell = sem execução interativa pós-exploração. A maioria dos ataques contra containers assume que, após explorar uma vulnerabilidade na aplicação (RCE, command injection), o atacante pode executar comandos no sistema subjacente. Sem shell disponível, essa etapa falha. O atacante pode ter acesso ao processo da aplicação, mas não pode facilmente pivotar para execução arbitrária de comandos.

Sem package manager = sem instalação de ferramentas. Técnicas de lateral movement e persistência frequentemente envolvem instalar ferramentas dentro do container comprometido (curl para baixar um backdoor, apt install para instalar um exploit). Distroless elimina essa possibilidade.

Menos pacotes = menos CVEs estruturalmente. Com menos de 10 pacotes totais, há simplesmente menos código com potencial de vulnerabilidade.

Trade-offs de distroless

Debug é mais difícil. Sem shell, você não pode fazer $ kubectl exec -it e navegar pelo container. Isso requer mudança de prática operacional: usar distroless/debug em ambientes de desenvolvimento (que inclui busybox), e ter observabilidade suficiente (logs estruturados, traces) para não precisar de acesso interativo em produção.

child_process.exec() não funciona. Em Node.js distroless, chamadas que dependem de shell (child_process.exec, child_process.spawn com shell: true) falham porque não há shell disponível. Aplicações que precisam executar subprocessos precisam usar child_process.spawn com o binário diretamente.

A instalação de dependências requer multi-stage build. Não há npm install dentro de uma imagem distroless. As dependências precisam ser copiadas de um estágio anterior. Isso é um requisito arquitetural, não uma limitação operacional, multi-stage build deve ser a prática padrão de qualquer forma.

Como o Quor usa as duas abordagens

No Quor, a escolha entre Alpine e distroless depende do tipo de imagem e das necessidades do runtime.

Alpine é usada quando:

  • A aplicação precisa de alguma interatividade controlada com o sistema (execução de subprocessos via shell);

  • O runtime não tem variante distroless estável disponível;

  • É necessário instalar pacotes adicionais de sistema durante o build;

  • A imagem é usada em contextos onde compatibilidade operacional com ferramentas de debug é um requisito explícito.

Distroless é usada quando:

  • O runtime tem suporte estável (Node.js LTS, JVM, Python, Go binaries);

  • A aplicação não depende de execução de shell em runtime;

  • A prioridade é minimização máxima de superfície de ataque;

  • O ambiente tem observabilidade suficiente para dispensar acesso interativo ao container.

Além da escolha da base, todas as imagens do Quor são construídas diretamente a partir do código fonte dos projetos, em vez de depender de pacotes pré-compilados de distribuições. Isso permite aplicar patches de segurança de forma antecipada, sem depender do ciclo de release de Debian, Ubuntu ou Alpine, e evitar vulnerabilidades herdadas de cadeias de distribuição mais amplas. Iremos abordar esse tópico em um próximo artigo de forma mais detalhada.

Benefícios técnicos concretos

A consequência mais direta é a eliminação estrutural de CVEs. Uma imagem com 8 pacotes tem uma superfície de ataque ordens de magnitude menor do que uma imagem com 661 pacotes. Não porque os CVEs foram "resolvidos", mas porque a maioria dos componentes vulneráveis simplesmente não existe na imagem.

Imagens menores impactam o pipeline de forma mensurável:

  • Menor tempo de pull durante deploys e cold starts em Kubernetes;

  • Menor consumo de storage em registries e nós do cluster;

  • Menor banda durante distribuição em ambientes multi-região;

  • Inicialização mais rápida de containers, especialmente relevante em ambientes com escalabilidade horizontal frequente.

Em ambientes onde centenas de pods são iniciados diariamente, a diferença entre 1 GB e 160 MB por imagem tem impacto real em custo e latência de escalonamento.

Redução de ruído em pipelines de segurança

Um efeito frequentemente subestimado: imagens com CVEs próximas de zero eliminam o ruído dos scanners de segurança integrados ao CI/CD. Pipelines configurados para bloquear builds com CVEs de alta ou crítica severidade param de ser interrompidos por vulnerabilidades em pacotes que nem existem no runtime da aplicação. Equipes deixam de gastar horas em triagem manual de vulnerabilidades irrelevantes. Isso impacta diretamente a frequência de deploy e lead time, duas das quatro DORA Metrics.

Segurança pós-exploração

Mesmo que uma vulnerabilidade na aplicação seja explorada com sucesso, a ausência de shell, package manager e ferramentas de sistema em imagens distroless limita drasticamente o que o atacante pode fazer após o comprometimento inicial. Isso não elimina o risco, mas reduz significativamente o raio de explosão de um incidente.

  1. https://github.com/docker-library/buildpack-deps

Shrink your attack surface.

Cut remediation costs.

Reduce your attack surface and the cost of remediation.

With Quor, security becomes your competitive edge. See how in a personalized demo.

Documentation

sales@quor.dev

Powered by Getup