Pular para o conteúdo
Development

Um ticket de hardening que precisou ser re-derivado antes de eu poder implementá-lo

Por Victor Da Luz
railscisecuritykamaldev-logblog-manager

Esse foi um issue de hardening de segurança de CI/CD com três tarefas escritas semanas atrás, vindas de uma investigação exploratória que auditou o repositório inteiro. Quando cheguei nele, o chão tinha mudado debaixo dele. Duas das três tarefas continuavam certas. A terceira me ensinou mais do que eu esperava de um diff de workflow de duas linhas.

A parte que precisou ser re-derivada

A primeira tarefa dizia: parar de rodar builds de pull request no runner self-hosted, porque um bump de dependência comprometido poderia executar código arbitrário ali. Razoável, quando foi escrita. Só que, no meio do caminho, eu tinha migrado o pipeline de CI inteiro para esse mesmo runner self-hosted, justamente para que pull requests voltassem a ter feedback de teste real depois que o GitHub cortou o billing de runners hospedados. Fazer o que o ticket antigo dizia teria desfeito isso silenciosamente.

Então, em vez de implementar, fui checar o que é verdade de fato hoje. O GitHub já trata pull requests autorados pelo Dependabot como se viessem de um fork - token somente leitura, zero acesso a secrets - automaticamente, sem precisar de configuração. Isso está documentado, e li a documentação em vez de presumir. O risco residual (o código de uma dependência ruim ainda roda no host do runner durante o install e o test, mesmo sem secrets) é real, mas não existe correção via arquivo de workflow para isso enquanto a situação de billing continuar assim - rotear esses PRs para runners hospedados só faz eles voltarem a falhar ao iniciar, que é exatamente o problema que eu já tinha resolvido. Expliquei isso e perguntei antes de descartar a tarefa, em vez de decidir sozinho e silenciosamente que um ticket antigo não se aplicava mais.

O que eu construí

As outras duas tarefas se sustentaram bem: permissões explícitas somente leitura nos workflows, e não deixar mais uma master key do Rails descriptografada parada no disco do runner depois de um deploy.

A segunda é onde ficou interessante. Minha primeira passada escrevia a chave num arquivo, rodava o deploy, depois apagava o arquivo. Direto ao ponto, e funcionava. O code review apontou que isso só encolhe a janela de exposição - não a fecha. Se o processo do runner morresse entre a escrita e a exclusão, o que pode acontecer numa máquina persistente que não é desmontada depois de cada job, a chave simplesmente ficaria ali. E então veio uma pergunta mais difícil: por que eu estava escrevendo isso num arquivo, se o workflow já tinha isso como variável de ambiente?

Boa pergunta. Fui investigar, e descobri que a escrita do arquivo existia puramente porque a configuração de secrets da ferramenta de deploy tinha hardcoded a leitura a partir de um caminho de arquivo em vez do ambiente. Nada no Rails ou na ferramenta de deploy exigia de fato um arquivo - consegui provar isso, porque um job completamente diferente no mesmo pipeline já passava o mesmo secret como uma variável de ambiente simples, sem nenhum arquivo envolvido.

O que me surpreendeu

Corrigir o arquivo de secrets não foi tão simples quanto trocar por uma expressão de fallback estilo bash - “use a variável de ambiente se estiver definida, senão volte para o arquivo.” Escrevi isso, e parecia razoável. Depois li de fato o código-fonte da ferramenta que faz o parsing desse arquivo, em vez de presumir que ela se comporta como um script shell só porque parece um. Não se comporta. É parseada por uma biblioteca estilo dotenv com uma pequena extensão customizada plugada para substituição de comando, e essa biblioteca não tem nenhum conceito de sintaxe de fallback. Alimente ela com o que eu escrevi e ela silenciosamente mantém só a primeira metade, descarta o resto como texto lixo, e devolve um valor corrompido sem nenhum erro. Esse é o tipo de bug que parece bem num diff e só se anuncia quando um deploy quebra silenciosamente em produção - um que eu especificamente quero sempre evitar. (O comportamento de secrets dotenv do Kamal já me mordeu antes.)

A correção que sobreviveu ao contato com o parser de verdade: a ferramenta suporta empilhar um arquivo de secrets específico de um destino sobre um compartilhado, com valores posteriores sobrescrevendo os anteriores para a mesma chave. Então dei ao destino de staging seu próprio arquivinho que lê a chave direto do ambiente, enquanto deixava o arquivo compartilhado - o que a produção ainda usa para deploys manuais locais - completamente intocado. Testei isso diretamente antes de confiar: instanciei o próprio resolvedor de secrets da ferramenta num script descartável, confirmei que o staging pegava um valor de ambiente falso enquanto a produção continuava lendo a chave real do disco exatamente como antes. Depois fiz o merge, vi o deploy real rodar no CI, e entrei via SSH no runner como root depois para confirmar - não presumir - que nenhum arquivo de chave existe em lugar nenhum no seu workspace.

O que vem a seguir

Nada de follow-up aqui. Mas o formato desse é digno de lembrar: um ticket antigo de hardening, uma correção “deveria ser simples” que tinha uma resposta errada com cara de plausível, e um comentário de review que se provou certo sobre profundidade, não só sobre estilo. Vale a pena checar o parser de verdade antes de confiar numa sintaxe que só parece com algo que você já escreveu cem vezes antes.

Leitura relacionada

Development

Três linhas de config, uma tarde de verificação

Descomentar flags de SSL do Rails levou dez minutos. Ler o código-fonte do framework, testar dois achados de revisão plausíveis mas errados, e provar o cookie em produção levou o resto do tempo.

Ler

Você também pode achar útil

AdGuard

AdGuard para iOS

Bloqueio de anúncios e rastreadores em todo o sistema no iOS, sem necessidade de um servidor DNS separado.

Como afiliado da AdGuard, ganho com compras qualificadas.

Saiba mais
Proton

Proton VPN

VPN comercial com filtragem NetShield e interruptor de desligamento automático.

Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).

Saiba mais
Proton

Proton Pass

Gerenciador de senhas focado em privacidade, da equipe por trás do Proton Mail.

Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).

Saiba mais