Tornando o hardening de containers e dependências uma prática permanente, não uma resposta a auditoria

Transformei o hardening de containers e dependências em uma prática trimestral permanente em dois serviços de backend ao longo de quatro trimestres, cobrindo runtimes non-root, 135 CVEs sinalizados e todos os 7 classificados como críticos remediados na primeira passagem, depois uma revisão de imagem base e dependências que levou a auditoria de 69 achados para 16, CVEs de SO sem correção de 160 para 0, e a imagem final de 1,64 GB para 463 MB, com risco aceito baseado em explorabilidade documentado em vez de números de versão perseguidos, e uma regressão de rollout autoinfligida com causa raiz identificada e corrigida no mesmo dia.

impactoPrimeira passagem (2025): 135 CVEs sinalizados em dois serviços, todos os 7 classificados como críticos remediados; runtimes trocados para um usuário non-root; imagem base modernizada.Última passagem (2026-08): auditoria de dependências 69 → 16 achados; CVEs de nível de SO sem correção upstream 160 → 0; imagem final 1,64 GB → 463 MB (−72%); quatro críticos removidos diretamente ao expurgar um pacote base não utilizado.Seis advisories conscientemente deixados abertos com justificativa escrita, baseada em explorabilidade, que é a parte do trabalho que mantém o relatório confiável.O hardening de segurança se tornou parte permanente de como serviços de backend são lançados, ao longo de quatro épicos trimestrais consecutivos de "redução de vulnerabilidade" a partir de 2025Q3, uma das seis áreas citadas na minha promoção a pleno, e, na última passagem, automatizado com uma skill de IA feita sob medida.Uma indisponibilidade autoinfligida em staging, com causa raiz identificada e corrigida no mesmo dia; nada chegou a produção.
01

Contexto

Esta é minha evidência de segurança como prática de engenharia em vez de simulação de incêndio. Segurança foi uma das seis áreas de competência no meu dossiê de promoção, e este é o trabalho concreto por trás dela, mas a parte que eu de fato defenderia em uma entrevista é o que aconteceu *depois* da promoção: a mesma prática rodou por mais quatro trimestres, e a última passagem é onde o julgamento aparece. Nem todo advisory merece um upgrade, nem todo CVE é alcançável no seu código, e uma imagem base que elimina 160 vulnerabilidades sem correção ainda pode derrubar seu serviço se você não checar do que seu entrypoint depende.

02

Problema

Serviços de backend rodavam em containers no Kubernetes sem uma prática formal de hardening ou remediação de CVEs. Exposição a vulnerabilidades em imagens base e dependências é o tipo de dívida mais fácil de adiar: não quebra um build, um teste, ou um deploy, só se acumula até que uma auditoria ou um incidente force atenção.

Concretamente, no início: as imagens rodavam como root por padrão, a saída do scan de containers nunca tinha sido triada, e ninguém era dono do trabalho recorrente.

03

Restrições

A exposição estava espalhada por dois serviços e suas árvores completas de dependências, não uma única linha corrigível: 135 CVEs sinalizados no primeiro scan, de severidade variada, e 69 achados de auditoria ainda abertos um ano depois, quando o toolchain já tinha avançado.
Containerização non-root toca todo o runtime, não só o Dockerfile: permissões de arquivo, ownership de processo, e tudo que assumia root precisava ser reverificado.
Remediar um CVE muitas vezes não é um bump de versão. Alguns advisories não têm correção na major em que você está fixado; alguns são corrigidos só em uma linha de framework para a qual você não pode migrar; alguns residem em pacotes de sistema operacional que a distribuição nunca corrigiu. Perseguir o número não é o mesmo que reduzir risco.
Priorização importava. Com 135 sinalizados, tratar todos como igualmente urgentes teria significado tratar nenhum como urgente.
Uma imagem base é um contrato de runtime. Trocar a distribuição muda quais binários existem, e as coisas que quebram não estão no seu código, estão no manifesto de deploy.
04

Decisão

2025, estabelecer o piso. Mapeei a saída do scanner de containers para os dois serviços em trabalho rastreado em vez de deixá-la como um relatório, troquei o runtime para um usuário non-root, migrei para uma base mais enxuta, e trabalhei os achados críticos primeiro: um cluster de CVEs da biblioteca Kerberos, um CVE do Berkeley DB, um advisory de form-data em ambos os serviços, um advisory do test runner. Sete críticos, todos fechados. Depois transformei isso em um épico trimestral recorrente ("redução de vulnerabilidade") em vez de uma tarefa concluída, que é o único motivo pelo qual a prática sobreviveu além da auditoria que a motivou.

2026, atacar as causas, não a contagem. Na última passagem, os achados interessantes não eram dependências de aplicação de jeito nenhum:

Dependências transitivas foram fixadas, não atualizadas. Overrides do gerenciador de pacotes corrigiram as dependências de produção alcançáveis sem uma mudança que quebrasse compatibilidade, e separadamente fixaram um lote de pacotes de toolchain que nunca chegam ao runtime.
O sistema operacional era o problema real. A base Debian-slim carregava 160 CVEs de nível de SO sem correção upstream disponível, inacionáveis por definição. Migrar para uma base Alpine levou isso a zero, porque todo advisory restante tinha uma correção. Também expurguei um pacote base que carregava quatro críticos que a Debian nunca tinha corrigido.
A imagem estava carregando sua própria build. Camadas do Docker são cumulativas, então devDependencies e uma mudança recursiva de ownership permaneciam na imagem mesmo depois de removidas. Dividir o Dockerfile em estágios de build e runtime levou a imagem final de 1,64 GB para 463 MB.
Escrevi o que não estava corrigindo, e por quê. Quatro pacotes de toolchain não tinham correção na major fixada e um upgrade seria desproporcional. Dois pacotes de framework tinham correções só em uma linha major para a qual o serviço não pode migrar, e o caminho de código vulnerável é o transporte SSE/TCP, enquanto este serviço só usa o transporte Kafka, então o caminho nunca é exercitado. Esse raciocínio está no pull request, como risco aceito com uma justificativa declarada, não omitido para fazer um número parecer melhor.
05

Trade-offs

Alpine sobre Debian-slim, escolhido porque moveu 160 CVEs de SO *sem correção* para zero, que é uma redução de risco real em vez de um número menor. Custo aceito: uma libc diferente, o que significou verificar o motor nativo do cliente de banco de dados e, como se viu, um shell ausente no caminho de deploy.
Fixar dependências transitivas via overrides em vez de atualizar os dependentes diretos, sem mudanças que quebrem compatibilidade e com cobertura imediata, ao custo de um lockfile que agora codifica decisões que alguém precisa revisitar.
Risco aceito documentado em vez de perseguir um relatório limpo. Deixar seis advisories abertos com justificativa escrita (sem correção na major fixada; transporte vulnerável nunca usado) é mais honesto e mais útil do que um upgrade que desestabiliza o serviço para deixar um dashboard verde.
Imagens non-root em vez de root (o padrão), para um raio de impacto menor se um container for comprometido, ao custo de reverificar tudo que assumia root.
Remediar por severidade em vez de por volume, atacando os 7 críticos primeiro mesmo que isso deixasse itens de menor severidade abertos por mais tempo.
Um épico trimestral recorrente em vez de uma limpeza única. Mais overhead de processo, mas é a diferença entre uma prática e uma anedota. Quatro trimestres depois ainda está rodando, mais recentemente executado com uma skill de IA construída para esse fim.
06

Impacto

Primeira passagem (2025): 135 CVEs sinalizados em dois serviços, todos os 7 classificados como críticos remediados; runtimes trocados para um usuário non-root; imagem base modernizada.
Última passagem (2026-08): auditoria de dependências 69 → 16 achados; CVEs de nível de SO sem correção upstream 160 → 0; imagem final 1,64 GB → 463 MB (−72%); quatro críticos removidos diretamente ao expurgar um pacote base não utilizado.
Seis advisories conscientemente deixados abertos com justificativa escrita, baseada em explorabilidade, que é a parte do trabalho que mantém o relatório confiável.
O hardening de segurança se tornou parte permanente de como serviços de backend são lançados, ao longo de quatro épicos trimestrais consecutivos de "redução de vulnerabilidade" a partir de 2025Q3, uma das seis áreas citadas na minha promoção a pleno, e, na última passagem, automatizado com uma skill de IA feita sob medida.
Uma indisponibilidade autoinfligida em staging, com causa raiz identificada e corrigida no mesmo dia; nada chegou a produção.
07

Lições Aprendidas

Conhecimento de engenharia reutilizável que levo adiante disso:

Exposição a vulnerabilidades se acumula silenciosamente. Nada em um ciclo normal de build/teste/deploy força atenção a isso, então precisa da sua própria prática permanente com um horário recorrente em vez de uma resposta a uma auditoria.
Um CVE sem correção upstream não é uma tarefa, é uma decisão de imagem base. 160 advisories inacionáveis não são um backlog a trabalhar; são um sinal de que a distribuição é o problema.
Argumente explorabilidade, não números de versão. "Corrigido só na próxima major, e o transporte vulnerável nunca é usado neste serviço" é uma posição mais forte do que um upgrade disruptivo, mas só se você escrever isso onde um revisor possa contestar.
Camadas do Docker são cumulativas, então limpar dentro de uma camada não limpa nada. Multi-stage é como você de fato remove peso de build-time do que você entrega.
Uma imagem base é um contrato de runtime. O que quebra quando você a troca vive no manifesto de deploy, não no seu código, e só quebra quando um rollout substitui os pods, que é o pior momento possível para descobrir.
Non-root por padrão é uma decisão de runtime, não uma linha de Dockerfile. Precisa ser verificada em todo o serviço, não só declarada.
08

Evidência

Passagem de 2025: achados do scanner de containers mapeados para trabalho rastreado nos dois serviços; runtime non-root; os 7 CVEs críticos fechados como itens rastreados individualmente (cluster da biblioteca Kerberos, Berkeley DB, form-data em ambos os serviços, test runner), todos com pull requests mesclados.
Passagem de 2026-08, verificada a partir do próprio pull request: auditoria 69 → 16; CVEs de SO sem correção 160 → 0; imagem 1,64 GB → 463 MB; saída de scanner antes/depois anexada; validada com um build e lint completos, 1.870 testes unitários com zero falhas, a suite e2e em 328/330, um build de container real, e um boot de API ao vivo dentro da nova imagem.
Risco aceito documentado no pull request, não omitido: seis advisories com justificativa escrita, incluindo o argumento de alcançabilidade para os dois pacotes de framework.
Regressão assumida: a troca para Alpine causou crash-loop em três deploys de staging porque os entrypoints do Kubernetes invocam `bash`; causa raiz identificada a partir dos eventos do pod e corrigida em um follow-up no mesmo dia, com a mudança lançada concorrentemente explicitamente descartada como causa.
Prática, não projeto: quatro épicos trimestrais consecutivos de "redução de vulnerabilidade" a partir de 2025Q3, o mais recente (2026-08) executado rodando uma skill de IA feita sob medida contra o serviço.
Citada como uma das seis áreas de competência (segurança) no meu dossiê de promoção (ver companies/dynamox.md).
Fonte (privada): épicos e tarefas do Jira no domínio de inspeção, 2025-07 a 2026-08; os pull requests correspondentes em ambos os repositórios de serviço de backend; base de conhecimento de carreira consolidada.