Um job de GitHub Actions é um bom sítio para roubar segredos. Contém chaves de implementação, tokens de registos e credenciais cloud, e executa código que outra pessoa escreveu há um minuto: actions de terceiros, pacotes e os respetivos scripts de instalação. O tratamento de segredos do GitHub protege contra acidentes. Não foi concebido para travar código que já está a ser executado dentro do job.

Este artigo explica como funcionam os segredos no GitHub Actions, as cinco formas como vazam na prática e os controlos que fecham cada uma.

Como funcionam os segredos do GitHub Actions

Os segredos existem a três níveis: organização, repositório e ambiente. Quando o mesmo nome existe em mais do que um nível, prevalece o mais específico: o ambiente sobre o repositório, o repositório sobre a organização.

Alguns limites e regras que vale a pena conhecer:

  • Tamanho e quantidade. Até 1000 segredos de organização, 100 segredos de repositório e 100 segredos de ambiente, cada um com um máximo de 48 KB. Um workflow vê todos os segredos de repositório e de ambiente, mas apenas os primeiros 100 segredos de organização por ordem alfabética.
  • Os forks não recebem nada. À exceção de GITHUB_TOKEN, os segredos não são passados a workflows desencadeados por um pull request de um fork.
  • O Dependabot é tratado como um fork. Os workflows desencadeados pelo Dependabot recebem um token só de leitura e apenas segredos do Dependabot, não segredos do Actions.
  • Os workflows reutilizáveis precisam que os segredos lhes sejam passados. secrets: inherit passa-os todos implicitamente, dentro da mesma organização ou empresa. Os segredos de ambiente não podem ser passados através de workflow_call.
  • Os ambientes controlam o acesso. Com revisores obrigatórios, um job não pode ler os segredos de um ambiente até alguém o aprovar. As regras de branches de implementação limitam os branches que podem sequer usar o ambiente.

O que a ocultação faz, e o que não faz

O GitHub oculta os valores dos segredos nos registos, mas a ocultação depende de encontrar uma correspondência exata. A própria documentação do GitHub enumera onde falha:

  • um segredo dentro de dados estruturados, como JSON ou YAML
  • um valor derivado de um segredo, como um token trocado por outro token, a menos que o registe com ::add-mask::
  • um segredo que foi transformado, por exemplo codificado em base64

A ocultação protege contra a impressão acidental de um segredo. Não faz nada contra código que codifique primeiro o segredo.

Como os segredos vazam dos workflows

1. Uma action comprometida imprime-os

Em março de 2025, um atacante comprometeu tj-actions/changed-files (CVE-2025-30066), uma action usada por mais de 23 000 repositórios. Moveu as etiquetas de versão existentes da action para um commit malicioso, pelo que todos os workflows que referenciavam uma etiqueta como @v45 executaram o código do atacante. Apenas os workflows fixados num SHA de commit completo ficaram a salvo.

O payload lia os segredos da memória do runner e imprimia-os no registo do job, codificados duas vezes em base64 para que a ocultação não os apanhasse. Nos repositórios públicos, esses registos podem ser lidos por qualquer pessoa. O comprometimento começou noutra action, reviewdog/action-setup (CVE-2025-30154), e antes disso num token de acesso pessoal roubado através de um workflow pull_request_target.

2. Um workflow pull_request_target executa código não fiável

pull_request_target é executado no contexto do repositório base, com os respetivos segredos e um token com permissão de escrita, mesmo quando o pull request vem de um fork. Isso só é seguro enquanto o workflow nunca executar código do pull request. Quando um workflow faz checkout da head do pull request e a compila, qualquer pessoa que possa abrir um pull request consegue ler todos os segredos do workflow. O GitHub Security Lab chama-lhes "pwn requests".

O GitHub tem vindo a restringir isto. Desde dezembro de 2025, pull_request_target usa sempre o ficheiro de workflow do branch predefinido. Desde julho de 2026, actions/checkout recusa fazer checkout de código de forks em workflows pull_request_target que usem padrões reconhecidamente perigosos, e as proteções de execução de workflows, disponíveis de forma geral desde setembro de 2026, vão desativar pull_request_target por predefinição em repositórios públicos sem política de eventos a partir de 2 de novembro de 2026.

3. Uma entrada não fiável é injetada num script

As expressões de workflow são expandidas antes de a shell ser executada. Um passo como este:

- run: echo "Title: ${{ github.event.issue.title }}"

executa o que quer que o título do issue contenha. Um atacante abre um issue com um comando de shell como título e o job executa-o, com os segredos do job ao alcance. Em alternativa, passe os valores não fiáveis por uma variável de ambiente, para que a shell os trate como dados:

- env:
    TITLE: ${{ github.event.issue.title }}
  run: echo "Title: $TITLE"

4. Caches e artefactos levam-nos para fora

  • Artefactos. Em 2024, a investigação ArtiPACKED da Unit 42 mostrou que actions/checkout escreve GITHUB_TOKEN em .git/config por predefinição, pelo que carregar o workspace como artefacto expõe o token a quem o puder descarregar. upload-artifact exclui ficheiros ocultos por predefinição desde setembro de 2024, e actions/checkout v6 guarda a credencial fora do diretório de checkout, mas as versões fixadas mais antigas e os passos de carregamento personalizados continuam a expô-lo.
  • Caches. Um atacante que possa executar código em qualquer workflow de um repositório pode encher a respetiva cache, expulsar as entradas reais e plantar uma envenenada com a mesma chave. Um workflow privilegiado posterior restaura-a. Em maio de 2026, esta cadeia, que começou em pull_request_target e terminou com um token OIDC lido da memória do runner, permitiu a um atacante publicar 84 versões maliciosas de 42 pacotes TanStack.

5. Uma dependência envia-os para um servidor

Os scripts de instalação são executados com os mesmos acessos que o resto do job. O worm Shai-Hulud, detetado pela primeira vez no npm em setembro de 2025, era executado durante npm install, recolhia tokens npm, tokens GitHub e chaves cloud do ambiente e publicava-os em repositórios públicos do GitHub. A sua vaga de novembro de 2025 era executada no preinstall e registava a máquina infetada como runner self-hosted, para que o atacante pudesse voltar mais tarde.

Esta via não precisa de nenhum erro no seu workflow. Basta uma única versão comprometida em qualquer ponto da árvore de dependências, e os segredos saem pela rede.

Como os proteger

Substitua as chaves cloud armazenadas por OIDC. Em vez de guardar uma chave da AWS, do Azure ou do Google Cloud como segredo, deixe o job pedir um token de curta duração ao emissor OIDC do GitHub e trocá-lo junto do fornecedor de cloud. A política de confiança decide que repositório, branch ou ambiente pode assumir a função, e a credencial expira com o job. O job precisa de permissions: id-token: write. Os tokens OIDC ainda podem ser roubados de um job em execução, como mostrou o caso TanStack, mas um token roubado vale muito menos do que uma chave que nunca expira.

Fixe as actions num SHA de commit completo. A documentação do GitHub chama-lhe a única forma de usar uma action como versão imutável. As etiquetas podem ser movidas; um SHA não. Desde agosto de 2025, a política de actions permitidas pode impor a fixação por SHA e bloquear actions específicas em toda uma organização. O Dependabot e o Renovate podem manter atualizados os SHA fixados.

Dê ao GITHUB_TOKEN o mínimo de que precisa. Defina a predefinição como só de leitura e conceda âmbitos de escrita por job. As novas organizações e repositórios têm a predefinição só de leitura desde fevereiro de 2023, mas os mais antigos mantiveram a predefinição permissiva, salvo se alguém a alterou. Consulte permissões de GITHUB_TOKEN: privilégio mínimo para cada workflow.

Limite os segredos ao passo que precisa deles. Um segredo no env: de um passo só está no ambiente desse passo. Um segredo definido ao nível do job é visível para todos os passos, incluindo todas as actions de terceiros.

Coloque os segredos de produção atrás de ambientes. Com revisores obrigatórios e regras de branches de implementação, um workflow num branch de funcionalidade, ou um desencadeado por um pull request, não os pode ler.

Trate pull_request_target e as entradas não fiáveis como execução de código. Evite pull_request_target se não precisar dele, nunca compile nele código de pull requests e passe os dados de eventos por variáveis de ambiente. O CodeQL pode verificar nos workflows padrões de injeção e de pwn requests; está disponível de forma geral para o GitHub Actions desde abril de 2025.

Ative o secret scanning e o push protection. Apanham segredos introduzidos no repositório, uma fuga diferente das anteriores, mas igualmente comum.

Restrinja os destinos a que o job se pode ligar. A maioria dos jobs precisa de uma lista curta de destinos: registos de pacotes, registos de contentores e algumas API. Se o job só alcançar esses, um segredo lido por código malicioso não tem para onde ir. Consulte como uma política de egress trava a exfiltração a partir do GitHub Actions.

Faça a rotação após qualquer exposição. Se um workflow executou uma action ou um pacote comprometido, parta do princípio de que todos os segredos que este podia ler foram levados. Revogue-os e volte a emiti-los todos, não apenas os que julga terem sido usados.

O papel do CRACI

O CRACI executa os seus jobs de GitHub Actions em runners próprios: altera runs-on para craci, e os workflows, os segredos e o histórico de execuções permanecem no GitHub. Há duas coisas que faz e que visam as fugas acima.

  • Uma política de egress que falha em modo fechado. Define as origens de pacotes, os registos e os hosts que um job pode alcançar. A política é validada antes de o job começar e aplicada no runner, pelo que uma ligação a qualquer outro destino é bloqueada, e recebe um alerta quando a política é violada.
  • Um registo do que o job fez. O CRACI regista cada pacote que cada job realmente descarregou, incluindo pacotes restaurados de caches, e um traço das ligações de rede do job. Quando for anunciado o próximo pacote comprometido, pode ver que builds o obtiveram e a que se ligaram, em vez de tentar adivinhar a partir de lockfiles.

Cada job é executado numa máquina virtual própria, pelo que nada do que um job deixa fica disponível para o seguinte. O CRACI não procura segredos no seu código; mantenha o secret scanning do GitHub para isso.

Marque uma demo para ver o registo de um dos seus builds.