12 de junho de 202611 min de leitura

Segurança em CI/CD para Projetos Open Source: Bloqueando Dependências

André Martins (Cilium maintainer and Software Engineer, Isovalent at Cisco) and Feroz Salam (Cilium Security Team and Security Engineer, Isovalent at Cisco)

Cloud Native Computing Foundation

Banner - Segurança em CI/CD para Projetos Open Source: Bloqueando Dependências

TL;DR: Este artigo analisa como o Cilium protege sua pipeline CI/CD contra ataques à cadeia de dependências. O ponto central é prático: fixar ações e imagens por SHA digest, automatizar atualizações com Renovate e versionar dependências Go. A conclusão principal é que o pinning SHA oferece a mesma garantia de imutabilidade que um fork, sem o custo operacional de mantê-lo. Para empresas brasileiras, o artigo oferece um roteiro concreto — e testado em produção — para reduzir riscos sem paralisar o time.

Como o Cilium protege sua pipeline CI/CD contra ataques à cadeia de dependências?

Este é o segundo artigo de uma série de três sobre como o Cilium — projeto graduado da CNCF — endurece sua pipeline CI/CD. A primeira parte cobriu controle de acesso: quem pode disparar builds e que código a CI está autorizada a executar. Agora entramos na camada de dependências: o código que esses builds puxam, e como garantir que ele não foi adulterado.

Fixando dependências: o que realmente funciona?

Depois de controlar quem dispara os builds, a prógunta é: que código esses builds puxam? Um workflow pinado que busca uma dependência comprometida ainda é um workflow comprometido. O Cilium adota uma abordagem prática: todas as diretivas uses: nos arquivos de workflow referenciam ações pelo SHA completo de 40 caracteres do commit, com a versão legível como comentário:

- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

Se alguém comprometer a tag v6 no actions/checkout e fizer force push de código malicioso, os workflows do Cilium não puxarão a nova versão. Eles estão pinados em um commit específico. O mesmo vale para containers usados diretamente em steps: são referenciados por @sha256: digest, garantindo que até as ferramentas executadas dentro da CI são endereçadas por conteúdo.

O ponto cego das dependências transitivas

O pinning tem um ponto cego importante: dependências transitivas. Quando o Cilium pina actions/checkout@de0fac2e…, sabe exatamente qual código roda para aquela ação. Mas se actions/checkout referencia outra ação por tag (uses: some-org/some-helper@v1), essa resolução acontece em tempo de execução e é invisível para o pipeline. Um atacante que comprometa a dependência aninhada ainda pode alcançar o pipeline.

Uma correção está a caminho: o bloqueio de dependências em nível de workflow foi anunciado no roteiro de segurança do GitHub Actions para 2026. Ele adicionaria uma seção dependencies: ao YAML do workflow que trava todas as dependências diretas e transitivas por SHA de commit, similar ao que go.mod + go.sum fazem para Go. O time do Cilium adotará assim que estiver disponível.

Automatizando atualizações com um limite de confiança

Manter pins SHA manualmente seria inviável, então o Cilium não faz isso. A configuração do Renovate estende o preset helpers:pinGitHubActionDigests e define pinDigests: true globalmente. Quando uma nova versão de ação é lançada, o Renovate abre um PR atualizando o SHA. O time se mantém atualizado sem nunca recorrer a uma referência mutável.

O Renovate roda como um bot auto-hospedado em schedule horário, usando um GitHub App dedicado com permissões granulares — não um token de acesso pessoal. A opção vulnerabilityAlerts está ativada, então CVEs conhecidas na árvore de dependências viram PRs automaticamente.

Um detalhe importante: o Cilium adicionou um cooldown ao Renovate para não aceitar novas versões no momento exato em que são publicadas. Dado o ritmo atual de ataques à cadeia de suprimentos, esses primeiros dias são a janela em que um pacote comprometido costuma ser descoberto e removido:

.github/renovate.json5

{
  // Dependency cooldown: skip versions published less than 5 days ago
  "matchUpdateTypes": ["major", "minor", "patch"],
  "minimumReleaseAge": "5 days"
},
{
  "matchPackageNames": [
    "actions/{/,}**",
    "docker/{/,}**",
    "cilium/{/,}**",
    "k8s.io/{/,}**",
    "sigs.k8s.io/{/,}**",
    "golang.org/x/{/,}**",
    "github.com/golang/{/,}**",
    "github.com/prometheus/{/,}**",
    "github.com/hashicorp/{/,}**",
    "go.etcd.io/etcd/{/,}**",
  ],
  "automerge": true,
  "automergeType": "pr",
  "groupName": "auto-merge-trusted-deps",
  "reviewers": ["ciliumbot"]
}

Atualizações que vêm dessa lista de confiança são mescladas automaticamente após a CI passar. Todo o resto precisa de revisão humana.

O workflow de auto-approve adiciona mais uma verificação: ele confirma que o PR foi criado pelo bot cilium-renovate[bot] e que a solicitação de revisão foi realmente acionada pelo bot, não por um humano fingindo ser ele:

if: ${{ github.event.pull_request.user.login == 'cilium-renovate[bot]' && (github.triggering_actor == 'cilium-renovate[bot]' || github.triggering_actor == 'auto-committer[bot]') }}

Se essas condições não forem satisfeitas, não há auto-approval.

E se adotássemos uma abordagem mais radical?

Em teoria, forkar todas as ações de terceiros para a organização cilium/ e pinar pelo SHA do fork seria ainda mais seguro — um comprometimento upstream não alcançaria o pipeline. Alguns projetos de alta segurança fazem exatamente isso. O time do Cilium decidiu não seguir esse caminho, principalmente porque o custo operacional é real e o ganho de segurança é menor do que parece:

  • Carga de manutenção. O time usa dezenas de ações de terceiros. Manter forks sincronizados com patches de segurança upstream se torna um trabalho de meio período, e um fork desatualizado com vulnerabilidades não corrigidas é ele próprio um problema de segurança.
  • Melhorias perdidas. Ações upstream corrigem bugs e entregam funcionalidades de segurança regularmente. Forks adicionam atrito para adotar essas melhorias.
  • Complexidade do Renovate. O pipeline de atualização teria que rastrear lançamentos upstream, abrir PRs contra cada fork e depois atualizar os workflows consumidores. A corrente dobra de comprimento.

O pinning SHA oferece a garantia de imutabilidade que realmente importa: um commit específico é um commit específico, independentemente de qual organização o hospeda. Combinado com o Renovate propondo atualizações conforme novas versões surgem, o time obtém o benefício de segurança sem o imposto operacional. Se um provedor importante for repetidamente comprometido, forkar as ações de alto risco é uma escalada razoável, mas o Cilium ainda não foi levado a esse ponto.

O mesmo trade-off se aplica às dependências Go

A pergunta "devemos forká-la?" se aplica tanto à árvore de dependências Go do Cilium quanto às ações. O Cilium puxa centenas de módulos Go: bibliotecas cliente do Kubernetes, gRPC, etcd, Prometheus, entre outros. Forkar e manter todos eles não é realista.

Go está em uma posição ligeiramente melhor que npm ou PyPI porque os caminhos de importação incluem explicitamente a fonte (github.com/stretchr/testify), o que elimina completamente a classe de ataque Dependency Confusion. Typosquatting ainda é uma ameaça real, no entanto. A pesquisa de Michael Henriksen encontrou pacotes Go com typosquatting na natureza, incluindo um fork do urfave/cli registrado como utfave (uma letra trocada) que enviava hostname, SO e arquitetura para um servidor remoto. Trocar esse callback por um reverse shell seria uma alteração de uma linha.

E typosquatting não é o pior caso. O caso SolarWinds mostrou que um fornecedor legítimo e amplamente confiável pode ter sua pipeline de build comprometida e depois distribuir malware por meio de atualizações normais. O mesmo pode acontecer com qualquer módulo Go: um atacante que obtém acesso à conta de um mantenedor publica uma versão maliciosa, o proxy a armazena em cache, e qualquer um que execute go get a puxa. É por isso que o Cilium versiona dependências: isso move a decisão de confiança do momento do build, onde é invisível, para o momento da revisão, onde um humano pode ver o diff.

O vendoring é a principal defesa aqui. Um caminho de importação com typosquatting aparece como um diff no diretório vendor/ durante a revisão de código, em vez de ser resolvido silenciosamente de um proxy de módulos. Ele não detecta o typo no momento em que é introduzido (depende de um revisor notar o caminho desconhecido no PR), mas combinado com o mecanismo CODEOWNERS, tem se mostrado eficaz até agora.

O time também é deliberado sobre quais dependências incorpora. A configuração do Renovate tem uma lista explícita de dependências desabilitadas que são gerenciadas manualmente, seja porque precisam de atualizações coordenadas (como sigs.k8s.io/gateway-api junto com testes de conformidade), porque o time mantém um fork com patches específicos do projeto (como github.com/cilium/dns), ou porque a dependência é desenvolvida internamente e requer bump intencional (como github.com/cilium/ebpf). Mudanças em vendor/ são revisadas pelo time dedicado @cilium/vendor via o mesmo mecanismo CODEOWNERS.

Há um provérbio Go que vale citar: "Um pouco de cópia é melhor que um pouco de dependência." O time audita periodicamente as bibliotecas de terceiros e ativamente reduz a árvore. Se uma dependência existe apenas para fornecer uma pequena função utilitária, ela é substituída por algumas linhas copiadas inline. Cada dependência removida é uma que nunca poderá ser comprometida, e revisar mudanças futuras de dependências se torna mais fácil.

Detectando erros com análise estática

Mesmo com as políticas certas em vigor, erros acontecem. Um contribuidor bem-intencionado pode adicionar um workflow sem permissions:, ou usar ubuntu-latest em vez de um runner pinado. O Cilium usa análise estática para capturar esses problemas antes da revisão.

Onde os workflows precisam de acesso de escrita (release signing, OIDC para Cosign), eles declaram apenas o escopo específico necessário, como id-token: write ou contents: write. Onde não precisam, declaram permissions: read-all ou permissions: {} para optar por não usar os padrões mais amplos. O time não confia na memória para isso: o CodeQL roda em todo push e PR com a regra actions/missing-workflow-permissions ativada, e o workflow falha qualquer arquivo de workflow modificado que não defina permissões explicitamente.

Além disso, o actionlint verifica estaticamente cada arquivo de workflow em busca de erros de sintaxe, padrões inseguros e configurações incorretas. O mesmo pipeline de lint também aplica convenções do projeto: cada job e step tem um nome, nenhum job usa a tag flutuante ubuntu-latest (o time pina para ubuntu-24.04), e não há espaços em branco à direita em arquivos de workflow.

Uma classe de vulnerabilidade merece destaque: a injeção de expressão do GitHub Actions. A sintaxe ${{ }} no YAML de workflow é uma substituição de texto que acontece antes de o Bash ver a linha. Se um atacante controla o valor sendo substituído (um título de PR, um nome de branch), ele pode injetar comandos arbitrários via ;, $(…) ou backticks. O Bash não tem ideia de onde o valor veio. A correção é atribuir o valor a uma variável de ambiente primeiro e referenciá-la como "$MINHA_VAR" no bloco run:, para que o Bash a trate como uma única variável, independentemente do conteúdo. A equipe de segurança do GitHub relatou isso ao time do Cilium há algum tempo, e todas as instâncias foram corrigidas. É um bug sutil, fácil de introduzir e difícil de identificar em revisão — exatamente por que a análise estática é importante: tanto o actionlint quanto o CodeQL sinalizam o uso de ${{ }} em blocos run: onde dados não confiáveis entram.

A parte 3 cobrirá a camada final: manter credenciais de CI e produção isoladas, assinar e atestar cada release, e as lacunas que o time ainda está trabalhando para fechar.

Perguntas Frequentes

  • Por que o Cilium prefere pinning por SHA em vez de forkar todas as ações de terceiros?
    O fork de todas as ações traria um custo operacional alto com manutenção de forks sincronizados, riscos de patches de segurança atrasados e complexidade adicional no pipeline de atualização. O pinning por SHA oferece a mesma garantia de imutabilidade — um commit específico é um commit específico — sem o ônus operacional. Se um provedor importante for repetidamente comprometido, forkar as ações de alto risco é uma escalada razoável, mas o time do Cilium ainda não precisou chegar a esse ponto.

  • Qual é o impacto prático de versionar dependências Go no repositório?
    Versionar (vendoring) move a decisão de confiança do momento do build — onde é invisível — para o momento da revisão, onde um humano pode ver o diff. Um pacote com typosquatting aparece como uma alteração suspeita no diretório vendor/ durante o code review. Além disso, builds se tornam reprodutíveis e não dependem de proxies externos no momento da compilação, eliminando o risco de um módulo adulterado em um proxy público.

  • Como o Renovate é configurado para evitar atualizações automáticas de dependências recém-publicadas?
    O Renovate usa a configuração 'minimumReleaseAge': '5 days', que impede que versões publicadas há menos de cinco dias sejam automaticamente propostas. Essa janela de espera é intencional: dado o ritmo atual de ataques à cadeia de suprimentos, esses primeiros dias são justamente o período em que um pacote comprometido costuma ser descoberto e removido (yanked).

  • Qual é a principal vulnerabilidade de injeção em GitHub Actions e como o Cilium a mitiga?
    A vulnerabilidade é a injeção de expressão do GitHub Actions (${{ }}), que faz substituição de texto antes do Bash interpretar o comando. Se um atacante controla o valor substituído (ex.: título de PR, nome de branch), pode injetar comandos arbitrários via ;, $(…) ou backticks. A mitigação do Cilium é atribuir o valor a uma variável de ambiente primeiro e referenciá-la como "$MINHA_VAR" no bloco run:, de modo que o Bash trate o conteúdo como uma única variável. Tanto o actionlint quanto o CodeQL detectam esse padrão perigoso automaticamente.


Artigo originalmente publicado por André Martins (Cilium maintainer and Software Engineer, Isovalent at Cisco) and Feroz Salam (Cilium Security Team and Security Engineer, Isovalent at Cisco) em Cloud Native Computing Foundation.

Gostou? Compartilhe:
Precisa de ajuda?Fale com nossos especialistas 👋
Avatar Walcew - Headset