Segurança no GitHub Actions: 3 defesas essenciais aprendidas com o caso tj-actions

Em março de 2025, um GitHub Advisory mudou meu limite de percepção sobre segurança. O tj-actions/changed-files, uma Action que eu usava havia três anos, foi marcado como CVE-2025-30066. Mais de 23.000 repositórios ficaram, de uma hora para outra, expostos ao risco de vazamento de Secrets.
O que mais me deu um frio na espinha foi o método do ataque. Primeiro, os atacantes roubaram o PAT de um projeto. Depois, adulteraram as tags de versão do tj-actions e fizeram o código que antes era seguro apontar para um script malicioso. Aqueles Secrets, como chaves da AWS, senhas de banco de dados e API Tokens, simplesmente escorreram em silêncio para logs públicos.
Na hora eu travei um pouco. O fluxo de CI/CD que eu mesmo tinha configurado virou uma ponte para atacantes. Saí revisando todos os repositórios, olhando referências de versão das Actions, permissões do GITHUB_TOKEN e configurações de log de auditoria. Depois de um dia inteiro mexendo nisso, percebi quantos detalhes eu já tinha deixado passar.
Este artigo é a checklist de proteção que organizei depois daquele momento de susto. Vamos falar sobre o padrão de ataques à cadeia de suprimentos, a forma correta de lidar com Secrets, os detalhes do controle de permissões do GITHUB_TOKEN e os novos recursos do roadmap de segurança do GitHub para 2026 que já vale a pena acompanhar.
O ponto principal é: tudo isso pode ser configurado agora. Você não precisa esperar nenhum recurso novo entrar no ar.
O que o caso tj-actions ensina sobre ataques à cadeia de suprimentos em CI/CD
Como o ataque aconteceu
Vamos começar pelo tj-actions/changed-files. Essa Action é muito popular no GitHub: ela detecta quais arquivos mudaram em um PR, e muitos fluxos de CI/CD dependem dela. Alguns dos meus projetos também a usavam para decidir implantações incrementais.
A cadeia do ataque foi mais ou menos assim:
Os atacantes primeiro roubaram um PAT, ou token de acesso pessoal, do projeto reviewdog/action-setup. Esse PAT tinha permissão de escrita no repositório. Com o token em mãos, eles adulteraram as tags de versão do repositório tj-actions: a tag v45, que antes apontava para código seguro, passou discretamente a apontar para um novo commit com lógica maliciosa.
Projetos que ainda usavam uses: tj-actions/changed-files@v45 baixaram o código malicioso sem perceber. O script malicioso fazia algo simples: imprimia todos os Secrets do ambiente de CI/CD nos logs. Como os logs eram públicos, os Secrets vazaram desse jeito.
Segundo o GitHub Advisory, mais de 23.000 repositórios foram afetados. A equipe de segurança da Coinbase divulgou depois que o ataque atingiu dados de mais de 70.000 clientes.
O erro que eu também cometia: referência por tag vs pinning por SHA
Ao revisar meus próprios repositórios, encontrei muitos lugares usando referências por tag:
# Exemplo incorreto: uso de tag mutável
- name: Check changed files
uses: tj-actions/changed-files@v45
Tags são vivas. O mantenedor do repositório, ou um atacante que consiga permissão de escrita, pode apontar a tag para um novo commit a qualquer momento. Você acha que está usando a v45, mas o que roda de fato pode ser código malicioso adulterado.
A prática correta é fixar a referência pelo SHA completo:
# Prática correta: usar SHA completo
- name: Check changed files
uses: tj-actions/changed-files@6cbf527e7a7b6d61c4e7f25e5ce5f7b7c8f3c72a
SHA é imóvel. Enquanto você não atualizar a referência manualmente, o workflow executa exatamente aquele trecho de código, e ele não muda.
Na época, enquanto eu corrigia tudo, fiquei me perguntando: isso é conhecimento básico de segurança. Como passei tanto tempo sem prestar atenção?
O custo de confiar em Actions de terceiros
O caso tj-actions também expôs outro problema: muitas vezes, confiamos barato demais em Actions de terceiros.
Uma Action tem um pouco mais de stars, parece ser usada por bastante gente, e já colocamos direto no fluxo de CI/CD de produção. Mas quem garante o nível de consciência de segurança dos mantenedores? Quem garante que o PAT deles não será roubado?
O roadmap de segurança do GitHub para 2026 menciona uma solução: bloqueio de dependências no nível do workflow. Seria algo parecido com package-lock.json, fixando o SHA de todas as Actions em um arquivo de lock. Esse recurso ainda não está no ar, mas podemos fazer isso manualmente desde já: trocar cada referência de Action por SHA e auditar periodicamente.
Outra recomendação é reduzir o número de Actions de terceiros. Se uma Action oficial resolve o problema, evite a de terceiros. Para detecção de arquivos, por exemplo, muitas vezes dá para usar a Action oficial actions/checkout junto com um script shell, sem depender do tj-actions.
Os 3 níveis de GitHub Secrets e práticas avançadas
Os três níveis de armazenamento de GitHub Secrets
Os Secrets nativos do GitHub existem em três níveis: organização, repositório e ambiente.
Secrets no nível da organização podem ser compartilhados por vários repositórios. Eles são adequados para credenciais comuns, como chaves da AWS e Tokens de serviços em nuvem. Secrets no nível do repositório só ficam disponíveis naquele repositório, então combinam com senhas de banco de dados específicas de um projeto. Secrets no nível do ambiente são mais granulares e podem ser combinados com regras de proteção de ambiente, como exigir aprovação em PR antes de permitir acesso aos Secrets de produção.
Na parte de armazenamento, o GitHub usa criptografia com libsodium sealed box. Depois que um Secret é gravado, não dá para lê-lo novamente; ele só pode ser acessado durante a execução do workflow por meio do contexto secrets:
steps:
- name: Deploy to AWS
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Um erro comum é interpolar Secrets diretamente em comandos shell:
# Perigoso: Secrets podem ser impressos nos logs
- name: Configure AWS
run: aws configure set aws_access_key_id ${{ secrets.AWS_ACCESS_KEY_ID }}
Se o valor do Secret tiver caracteres especiais, ou se o comando falhar, os logs podem expor esse Secret. A prática correta é passar o valor por variável de ambiente:
# Seguro: passar por variável de ambiente
- name: Configure AWS
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
run: aws configure set aws_access_key_id "$AWS_ACCESS_KEY_ID"
Mascaramento dinâmico: ::add-mask::
Alguns dados sensíveis não são Secrets cadastrados previamente, mas valores gerados em tempo de execução. Por exemplo, um Token temporário produzido por um script. Nesses casos, você pode usar ::add-mask:: para mascaramento dinâmico:
- name: Generate temporary token
run: |
token=$(generate-token.sh)
echo "::add-mask::$token"
echo "TOKEN=$token" >> $GITHUB_ENV
Depois de mascarado, o valor aparece como *** nos logs. Mas há um detalhe importante: o mascaramento precisa acontecer antes de o valor ser impresso. Se você imprimir primeiro e mascarar depois, o valor ainda ficará exposto no log.
Caminho avançado: integração HashiCorp Vault com OIDC
Se o seu projeto tem requisitos de segurança mais altos, talvez os Secrets nativos do GitHub não sejam suficientes. Credenciais de longa duração armazenadas no GitHub significam que, se o repositório for comprometido, os Secrets podem ir junto.
Uma solução melhor é usar HashiCorp Vault com OIDC, ou OpenID Connect, para acesso sem credenciais permanentes. A ideia é fazer o GitHub Actions provar ao Vault que ele é quem diz ser. Depois da validação, o Vault emite um Token temporário. Assim, nenhuma credencial de longa duração fica armazenada no GitHub.
O processo de configuração fica mais ou menos assim:
Primeiro passo: configurar uma função OIDC no Vault
O Vault precisa confiar no provedor OIDC do GitHub. Configure uma função que determine quais repositórios podem acessar quais Secrets:
resource "vault_jwt_auth_backend_role" "github_actions" {
backend = "jwt"
role_name = "github-actions-role"
bound_audiences = ["https://github.com/your-org"]
user_claim = "repository"
role_type = "jwt"
token_policies = ["ci-policy"]
token_ttl = "1h"
}
Segundo passo: usar vault-action no GitHub Actions
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Import Secrets from Vault
uses: hashicorp/[email protected]
id: vault
with:
url: https://vault.example.com:8200
role: github-actions-role
method: jwt
secrets: |
secret/data/ci/aws accessKey | AWS_ACCESS_KEY_ID ;
secret/data/ci/aws secretKey | AWS_SECRET_ACCESS_KEY
- name: Deploy with AWS credentials
run: |
echo "Accessing AWS with Vault-provided credentials"
aws s3 ls
Nesse fluxo, a vault-action usa automaticamente o OIDC Token fornecido pelo GitHub para se autenticar no Vault. Depois da validação, o Vault retorna os Secrets e os injeta em variáveis de ambiente. Após 1 hora, o Token expira automaticamente; na próxima execução, um novo Token é obtido.
O Azure Key Vault também oferece uma integração OIDC parecida. A configuração é semelhante, usando a Action azure/login junto com Azure Key Vault Secrets.
Controle de permissões do GITHUB_TOKEN na prática
O que é GITHUB_TOKEN
Sempre que um workflow do GitHub Actions roda, ele recebe automaticamente um GITHUB_TOKEN. Esse é um OAuth Token temporário usado para operar o repositório atual: criar Releases, enviar código, comentar em PRs e assim por diante.
O problema é que, por padrão, o GITHUB_TOKEN pode ter permissões amplas demais.
Em versões antigas do GitHub Actions, o GITHUB_TOKEN vinha com permissões de leitura e escrita quase totalmente abertas. O workflow podia modificar conteúdo do repositório, criar branches e fazer push de código. Se um workflow fosse explorado por um atacante, por exemplo com um script malicioso acionado por PR, o GITHUB_TOKEN virava uma arma.
Configuração completa da chave permissions
Em abril de 2021, o GitHub introduziu a chave permissions, permitindo controlar com precisão o escopo de permissões do GITHUB_TOKEN.
A sintaxe básica é esta:
permissions:
actions: read|write|none # Gerenciar Actions
contents: read|write|none # Conteúdo do repositório
issues: read|write|none # Operações com Issues
packages: read|write|none # GitHub Packages
pull-requests: read|write|none # Operações com PR
security-events: read|write|none # Envio de eventos de segurança
deployments: read|write|none # Status de implantação
statuses: read|write|none # Status de Commit
Existe um mecanismo importante: assim que você define a chave permissions em um workflow, todas as permissões não especificadas passam automaticamente para none. É isso que cria a fronteira de segurança do menor privilégio.
Permissões no nível do workflow vs no nível do job
permissions pode ser definido em dois níveis: no workflow, de forma global, e no job, de forma local.
Permissões no nível do workflow valem para todos os jobs:
name: CI Pipeline
permissions:
contents: read # Todos os jobs leem conteúdo do repositório por padrão
issues: write # Todos os jobs podem escrever em Issues
jobs:
lint:
runs-on: ubuntu-latest
steps:
- run: echo "Lint with read-only access"
test:
runs-on: ubuntu-latest
steps:
- run: echo "Test with read-only access"
Permissões no nível do job podem sobrescrever a configuração do workflow:
name: Release Pipeline
permissions:
contents: read # Somente leitura por padrão
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read # Mantém somente leitura
steps:
- run: echo "Build needs read only"
release:
runs-on: ubuntu-latest
permissions:
contents: write # Sobrescreve para escrita, usada na criação de Release
packages: write # Publica em Packages
steps:
- name: Create Release
uses: actions/create-release@v1
Um caso real: eu tenho um projeto cujo workflow de Release possui um job de build que só precisa ler o código, enquanto o job de release precisa criar Release e enviar imagem Docker. Com a separação de permissões por job, mesmo que o job de build seja comprometido, ele não consegue escrever no repositório.
Modo de permissões no nível do repositório
Além da configuração no workflow, as configurações do repositório no GitHub também têm dois modos de permissão:
- Permissive (permissivo): o GITHUB_TOKEN tem, por padrão, permissões de leitura e escrita amplas. Esse modo se aplica quando o workflow não especifica
permissions. - Restricted (restrito): o GITHUB_TOKEN tem, por padrão, apenas leitura de contents e packages. O workflow precisa declarar permissões extras para obter escrita.
A recomendação é colocar todos os repositórios em modo Restricted. Em Settings > Actions > General do repositório, encontre “Workflow permissions” e selecione “Read repository contents and packages permissions”.
Assim, mesmo que um workflow esqueça de definir permissions, ele não terá permissões excessivas. A fronteira de segurança já fica travada no nível do repositório.
Logs de auditoria e checagens de conformidade
Eventos de execução de workflows entram nos logs de auditoria
Desde fevereiro de 2021, o GitHub inclui eventos de execução de workflows do Actions nos logs de auditoria da organização. Isso significa que você pode rastrear quem acionou qual workflow, quais permissões foram usadas e quais Secrets foram acessados.
O caminho para o log de auditoria é: Organization Settings > Security > Audit log.
Campos importantes incluem:
action: tipo de evento, comoworkflow_run.createeworkflow_run.completeactor: quem acionou o evento, que pode ser um nome de usuário, um App ougithub-actions[bot]repo: caminho do repositóriotoken_scopes: escopos de permissão do Token usadorequest_id: ID de rastreamento da requisição, útil para correlacionar logs
Um uso prático: quando você encontra uma execução anormal de workflow, use o log de auditoria para voltar à origem. Por exemplo, se um Secret foi acessado de forma suspeita, procure eventos workflow_run para encontrar o responsável pelo acionamento.
API de auditoria para empresas
Se a sua organização usa GitHub Enterprise, é possível consultar todas as operações com a Audit Log API:
curl -H "Authorization: Bearer YOUR_TOKEN" \
"https://api.github.com/enterprises/YOUR_ENTERPRISE/audit-log?phrase=workflow_run"
A resposta JSON contém registros detalhados dos eventos. Você pode exportá-la para um sistema SIEM, como Splunk ou Datadog, e manter monitoramento contínuo.
Resumo dos requisitos de conformidade
Se o seu projeto precisa atender a SOC 2 ou ISO 27001, segurança em CI/CD é um item obrigatório. Logs de auditoria são a evidência de conformidade mais direta: eles provam que você consegue rastrear atividades de CI/CD, detectar anomalias e responder a incidentes.
Uma configuração recomendada é exportar os logs de auditoria para um sistema externo e criar regras de alerta para eventos críticos. Por exemplo:
- Aumento súbito na taxa de falhas de execução de workflows
- Acesso anormal a Secrets, como muitas leituras em pouco tempo
- Workflow acionado por IP desconhecido
Visão antecipada do roadmap de segurança do GitHub para 2026
Em março de 2026, o GitHub publicou um roadmap de segurança para Actions, planejando 6 novos recursos importantes. Alguns já estão disponíveis, enquanto outros ainda estão em desenvolvimento.
Bloqueio de dependências no nível do workflow
Essa é a solução oficial para ataques do tipo tj-actions. Parecido com package-lock.json, o recurso fixa todas as referências de Actions em SHA. No workflow, você ainda escreveria uses: tj-actions/changed-files@v45, mas o arquivo de lock registraria o SHA correspondente. Quando o mantenedor atualizasse a Action, o arquivo de lock não mudaria automaticamente; você precisaria revisar e atualizar de forma ativa.
Esse recurso ainda não está no ar, mas já podemos fazer o pinning por SHA manualmente.
Firewall de saída nativo em Layer 7
O recurso oferece suporte nativo para controlar o acesso externo de fluxos de CI/CD. Por exemplo, limitar um workflow para que ele só acesse sua API da AWS, sem permitir acesso livre à internet.
Hoje, para implementar isso, é necessário usar Runner auto-hospedado e políticas de rede customizadas. Quando o recurso entrar em 2026, GitHub-hosted Runners também poderão configurar firewall de saída.
Scoped Secrets
Scoped Secrets traz controle mais granular do escopo de Secrets. Por exemplo, um Secret poderia ser acessado apenas por uma branch específica ou por um job específico. Atualmente, o escopo de Secrets fica no nível do repositório ou do ambiente, com granularidade ainda insuficiente.
Controle de execução orientado por políticas
Esse recurso permite definir fronteiras de confiança, aprovações e gates de comprovação. Por exemplo, exigir que todos os PRs vindos de forks passem por aprovação manual antes de acionar workflows. Ou exigir que o workflow passe por uma varredura de segurança antes de rodar.
Ele se parece com regras de proteção de ambiente, mas com granularidade maior e lógica mais complexa.
Actions Data Stream
Esse recurso oferece visibilidade em tempo real das atividades de CI/CD. É parecido com logs de auditoria, mas com envio em tempo real, não consulta posterior. Pode ser integrado a um sistema SIEM para monitoramento contínuo.
Declarações de atributos customizados em OIDC
Esse recurso fortalece a verificação de identidade por provedores de nuvem. O OIDC Token poderá carregar mais atributos customizados, como tags do repositório e informações da branch. Vault ou AWS poderão usar esses atributos para tomar decisões de autorização mais granulares.
Conclusão
Do caso tj-actions até agora, as principais lições que aprendi são três:
Primeira: pinning por SHA. Não use referência por tag para Actions de terceiros; use SHA completo. Essa é uma experiência que custou caro a 23.000 repositórios.
Segunda: menor privilégio. O GITHUB_TOKEN pode ter permissões amplas demais por padrão. Use a chave permissions para limitar o escopo e coloque o repositório em modo Restricted.
Terceira: logs de auditoria. Eventos de Actions já entram na auditoria. Verifique execuções anormais periodicamente e exporte os dados para um sistema de monitoramento.
Essas três coisas podem ser configuradas agora. Você não precisa esperar os novos recursos do GitHub 2026 nem trocar de ferramenta. Abra seu repositório, revise referências de Actions, permissões e logs de auditoria. Em meia hora, dá para concluir a proteção básica.
No fim, segurança em CI/CD não é uma técnica misteriosa; é um hábito de atenção aos detalhes. Cada SHA fixado e cada permissão excessiva removida reduzem um pouco o risco. O caso tj-actions nos lembrou que atacantes não precisam de truques muito sofisticados: às vezes basta que a gente ignore um detalhe pequeno.
Fluxo de configuração para proteger GitHub Actions
Do pinning por SHA ao controle de permissões, conclua os três passos centrais para reforçar a segurança de CI/CD.
⏱️ Estimated time: 30 min
- 1
Step 1: Fixar referências de Actions por SHA
Verifique todos os arquivos de workflow e troque `uses: action@tag` por `uses: action@full-sha` para evitar adulteração de tags. Use ferramentas como `pinact` para converter referências existentes em lote. - 2
Step 2: Configurar a chave permissions
Adicione a chave permissions no nível do workflow ou do job e conceda apenas as permissões necessárias. Por exemplo: `permissions: contents: read` para operações somente leitura e `permissions: contents: write` para criar Releases. Use o modo Restricted no repositório como camada de proteção. - 3
Step 3: Ativar monitoramento de logs de auditoria
Em Organization Settings > Security > Audit log, consulte eventos de execução de workflows do Actions. Configure alertas para eventos críticos: aumento súbito na taxa de falhas de workflows, acesso anormal a Secrets e workflows acionados por IPs desconhecidos. Exporte os logs para um sistema SIEM para monitoramento contínuo.
FAQ
Por que fixar por SHA em vez de usar referência por tag?
Como controlar as permissões do GITHUB_TOKEN?
Como detectar se um workflow foi atacado?
Qual é a vantagem de OIDC + Vault em relação aos Secrets nativos?
Ainda dá para usar Actions de terceiros?
14 min de leitura · Publicado em: 16 mai 2026 · Atualizado em: 14 jul 2026
Guia completo GitHub Actions
Se você chegou pela busca, o caminho mais rápido é ir para o post anterior ou próximo desta série.
Anterior
Desenvolvendo Composite Actions no GitHub Actions: do action.yml ao Marketplace
Guia completo para desenvolver Composite Actions no GitHub Actions, com estrutura do action.yml, inputs/outputs, passagem de secrets, versionamento e publicação no Marketplace.
Parte 9 de 10
Próximo
Este é o post mais recente da série até agora.



Comentários
Entre com GitHub para comentar