Defendendo o Kubernetes contra ataques à cadeia de suprimentos
Uma dependência comprometida pode contrabandear um minerador de criptomoedas para dentro do seu build, passar pela revisão de código e consumir CPU silenciosamente em um node de produção sem nenhum sintoma na aplicação. Veja como esse ataque funciona e as defesas de pipeline e runtime que o impedem.
Combine verificações de pipeline com detecção de anomalias em nível de node e um procedimento de contenção explícito.
Contribuição em resumo
- Minha responsabilidade
- Defesa em camadas da cadeia de suprimentos e técnicas operacionais de contenção.
- Escopo
- Um relato técnico de controles de dependência/build e padrões de resposta em runtime.
- Colaboração & limites
- O caso explica a abordagem de engenharia; ele não atribui cada controle nem a ausência de incidentes futuros a uma única pessoa.
- Resultado
- Verificações de pipeline, sinais de runtime e um caminho de contenção explícito tratam de diferentes estágios de um comprometimento.
A classe mais assustadora de comprometimento é a silenciosa. Nenhuma indisponibilidade, nenhum pico de erros, nenhum alerta da aplicação. Apenas um node rodando mais quente do que deveria, com um processo como o xmrig (um minerador de Monero) consumindo CPU dentro de um container que, de resto, parece normal. Este é um relato de como isso acontece através da cadeia de suprimentos de software e das defesas em camadas que o tornam tanto difícil de executar quanto rápido de detectar.
Como um minerador entra sem tocar em um pull request
A parte perigosa de um ataque à cadeia de suprimentos é que o código malicioso nunca está em um diff que alguém leia. Um pacote nas profundezas da árvore de dependências (uma dependência transitiva de uma dependência) é comprometido na origem. Quando o frontend é compilado, o código ruim é puxado e montado no bundle. A revisão de código, estruturalmente, não consegue pegar isso, porque os revisores olham o conjunto de alterações, não cada linha de cada pacote transitivo. O resultado vai para produção parecendo completamente normal.
Por que a revisão de código é a camada errada para isso
A revisão humana inspeciona o que mudou no pull request. Uma dependência transitiva envenenada não muda nada no diff; ela muda o que é baixado e compilado. É por isso que a defesa da cadeia de suprimentos precisa viver no pipeline e no runtime, não na revisão.
Detecção: observe o node, não apenas a aplicação
Como a aplicação se comporta normalmente, o único sinal precoce está na camada de infraestrutura. O propósito inteiro de um minerador de criptomoedas é consumir CPU, então a detecção de anomalias de CPU em nível de node é o gatilho. Concretamente:
- Alerte sobre CPU anômala em qualquer node, com uma janela de limiar curta (por exemplo, cinco minutos), direcionado ao canal de plantão.
- Quando ele disparar e as métricas da aplicação parecerem normais, suspeite do node, não do serviço. Inspecione os processos rodando dentro dos containers naquele node.
- Um processo que não pertence a nenhuma das suas imagens (um minerador, um shell, um binário inesperado) é a prova definitiva.
Contenção, se um entrar
A resposta é a higiene padrão do Kubernetes, feita rapidamente:
- Faça cordon no node afetado para que nenhum novo workload seja alocado nele.
- Faça drain dele e deixe os workloads serem reagendados em nodes limpos.
- Remedie a origem: atualize cada biblioteca vulnerável e fixe as versões transitivas para que o pacote envenenado não possa voltar no próximo build.
As defesas que realmente o impedem
Detecção e limpeza são o último recurso. Estas são as camadas que o previnem, ou o neutralizam se ele ainda assim passar.
- Audite no CI. Faça cada build passar por
npm audit --audit-level=highpara que dependências com vulnerabilidades conhecidas quebrem o build. - Escaneie a imagem. Rode um scanner de imagens como o Trivy em cada build pré-deploy, para que um payload malicioso embutido na imagem seja pego antes de ir ao ar.
- Exija lockfiles. Exija um lockfile commitado em cada repositório e faça o CI falhar se ele estiver ausente ou tiver sido alterado sem revisão, para que as versões das dependências sejam determinísticas e auditáveis.
- Fixe as versões transitivas. Versões flutuantes são a forma como um pacote envenenado ganha uma segunda chance; a fixação fecha essa porta.
- Restrinja o runtime. Aplique Pod Security Standards (restricted) em todos os namespaces para que um container comprometido não consiga escalar privilégios.
- Negue o egress por padrão. Network policies que bloqueiam conexões de saída para pools de mineração e endpoints desconhecidos transformam uma intrusão bem-sucedida em um beco sem saída: o minerador não consegue alcançar seu pool nem se comunicar com o exterior.
Defesa em profundidade, em uma frase
A auditoria no CI e o escaneamento de imagens tornam difícil a entrada, lockfiles e fixação tornam difícil o retorno, os Pod Security Standards tornam difícil a escalada, negar egress por padrão torna o ataque inútil caso ele entre, e os alertas de anomalia de CPU no node tornam rápida a detecção. Nenhuma camada isolada precisa ser perfeita.
O que eu diria a quem está fortalecendo um cluster
- Presuma que uma dependência será comprometida em algum momento e projete de modo que isso não importe muito quando acontecer.
- Coloque as verificações da cadeia de suprimentos no pipeline. Elas pegam o que a revisão de código estruturalmente não consegue.
- Negar egress por padrão é o controle de maior alavancagem: ele transforma a maioria das intrusões em becos sem saída com quase nenhuma desvantagem.
- Observe os sinais de infraestrutura, não apenas os da aplicação. Os comprometimentos silenciosos só aparecem no node.
Decisões e próximos passos
Uma visão retrospectiva da abordagem e dos seus limites. A próxima melhoria é uma proposta, não um resultado já entregue.
Alternativa e justificativa
A revisão de pull request continua útil, mas os controles de dependência e de runtime cobrem código e comportamento fora do conjunto de alterações revisado.
Trade-off
Qualquer detector isolado tem pontos cegos. Detecção, contenção e rebuilds limpos precisam de validação independente.
O que eu melhoraria a seguir
Próxima melhoria: ensaiar um comprometimento simulado e seguro e verificar o tempo da detecção até a contenção e a recuperação a partir de artefatos confiáveis.