CI/CD Zero-Trust: Implementando Segurança em Pipelines DevOps
Pipelines de CI/CD concentram credenciais poderosas e acesso direto a produção, o que os torna alvos atrativos. Entenda como aplicar Zero Trust nesse contexto: identidades efêmeras, isolamento de runners, gestão de segredos e integridade da cadeia de entrega.
Pipelines de CI/CD têm permissão para fazer deploy em produção, escrever em registros de imagens e assumir roles com acesso a dados sensíveis. Ainda assim, costumam concentrar chaves de longa duração armazenadas como variáveis de ambiente e recebem menos atenção de segurança do que os sistemas que alimentam. Comprometer um pacote de dependência amplamente usado ou injetar código em um runner é suficiente para atingir dezenas de organizações de uma vez, sem precisar atacar cada uma diretamente. O risco interno é igualmente concreto: tokens com escopo excessivo reutilizados entre jobs, segredos vazando por má configuração e runners self-hosted em repositórios públicos sendo explorados são incidentes que acontecem por falhas de design, não só por ataques sofisticados.
Zero Trust, aplicado ao CI/CD, significa não confiar implicitamente em nenhum componente, identidade ou rede, tratando cada stage e runner como potencialmente hostil até que o contrário seja verificado.
Por que pipelines são alvos
O problema mais comum começa na gestão de credenciais. Um segredo de longa duração (chave de acesso AWS, token de registry, senha de banco de staging) é colocado como variável de ambiente no sistema de CI e permanece lá indefinidamente. Qualquer job naquele pipeline, em qualquer branch, tem acesso a essas credenciais, mesmo que não precise delas. Runners self-hosted adicionam outra superfície: se a máquina é persistente e o repositório é público, um pull request externo pode executar código nesse ambiente e exfiltrar os segredos disponíveis.
Identidades efêmeras no lugar de credenciais estáticas
A mudança mais impactante em termos de segurança é eliminar chaves de longa duração do pipeline. A alternativa já está disponível nos provedores de nuvem mais usados: federação via OIDC (OpenID Connect).
O mecanismo é direto. O sistema de CI (GitHub Actions, GitLab CI) emite um token OIDC assinado para cada execução de job, atestando a identidade do workflow, o repositório e a branch. Esse token é apresentado ao provedor de nuvem, que o valida e retorna credenciais temporárias com duração limitada. Nenhuma chave estática precisa existir.
No GitHub Actions com AWS, a configuração de confiança no lado da IAM define exatamente quais workflows podem assumir qual role:
{
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:sub": "repo:minha-org/meu-repo:ref:refs/heads/main"
}
}
}
Esse trecho diz que somente jobs rodando na branch main do repositório especificado podem assumir a role. Um job disparado de um fork ou de uma branch de feature recebe negação. As credenciais emitidas têm validade curta (tipicamente 15 minutos a 1 hora dependendo da configuração) e não existem fora da execução daquele job.
GitLab CI oferece mecanismo equivalente, e outros provedores de nuvem (GCP, Azure) têm suporte similar para workload identity federation.
Least privilege para jobs e runners
Com identidades efêmeras no lugar, o próximo passo é garantir que cada identidade tenha o mínimo de permissão necessário. Um job de testes unitários não precisa de acesso a S3. Um job de build de imagem não precisa escrever em produção. Um job de deploy em staging não precisa das mesmas permissões que o deploy em produção.
Isso implica definir roles separadas por stage e por propósito, em vez de uma única role "CI" que acumula permissões ao longo do tempo. O mesmo raciocínio se aplica a tokens internos do sistema de CI: devem ter escopo explícito (leitura, escrita, pull requests), não o scope padrão mais amplo que a plataforma oferece.
Isolamento de runners
Runners efêmeros, criados para um job e descartados ao fim dele, eliminam o estado persistente que pode ser explorado entre execuções. Um runner que existia antes do job e continuará existindo depois é uma superfície potencial: arquivos temporários, cache de credenciais, configurações de ferramentas que um job anterior deixou para trás.
Runners gerenciados pelos provedores (GitHub Actions hosted runners, GitLab CI SaaS runners) já funcionam assim por padrão. Para runners self-hosted, a prática recomendada é descartar o ambiente após cada job e isolar em rede separada sem acesso direto à infraestrutura de produção. Em repositórios públicos, a combinação com runners self-hosted é especialmente perigosa: qualquer pessoa pode abrir um pull request e acionar código nessa máquina.
Gestão de segredos com cofre dedicado
Segredos armazenados como variáveis de ambiente no sistema de CI têm algumas limitações: ficam disponíveis para qualquer job do pipeline, raramente têm rotação automática e são difíceis de auditar. Um cofre dedicado (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault) permite escopar segredos por serviço, rotacionar automaticamente e registrar cada acesso com contexto.
Com Vault, a integração com pipelines pode usar segredos dinâmicos: em vez de uma senha de banco fixa, o Vault gera credenciais temporárias com TTL curto para aquela execução específica. As credenciais expiram automaticamente, e nenhum humano precisa conhecê-las.
Antes de adotar um cofre externo, o mínimo é garantir que segredos nunca apareçam em texto no repositório nem sejam impressos em logs. Ferramentas como truffleHog ou git-secrets detectam vazamentos no pre-commit e na CI.
Integridade da cadeia: assinatura e proveniência
Saber que o código passou pelo pipeline não é suficiente se não houver como verificar que o artefato que chegou ao deploy é exatamente o que foi construído, sem modificações no caminho. É o que o conceito de supply chain integrity trata.
Sigstore e sua ferramenta cosign permitem assinar imagens de contêiner e outros artefatos com identidades OIDC, sem necessidade de gerenciar chaves de assinatura manualmente. A assinatura fica registrada em um log de transparência público (Rekor), o que torna adulterações detectáveis.
SLSA (Supply-chain Levels for Software Artifacts) define níveis de maturidade para proveniência: do nível 1 (build automatizado com proveniência gerada) até o nível 3 (build em ambiente isolado com proveniência assinada). O nível 2, que exige builder hospedado por serviço confiável e proveniência assinada, é atingível com GitHub Actions ou GitLab CI sem infraestrutura adicional. Verificar a proveniência antes do deploy fecha o ciclo: o sistema rejeita o artefato se a verificação falhar.
Pinning de dependências e de actions
Ataques de supply chain via dependências comprometidas são facilitados quando o pipeline busca a versão mais recente de uma dependência a cada execução. Fixar versões (pinning) no manifesto de dependências é uma prática básica. Para GitHub Actions especificamente, referenciar uma action por hash de commit em vez de por tag evita que uma tag seja movida para apontar a um commit diferente:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
Tags podem ser sobrescritas; hashes de commit, não.
Trade-offs e como começar
Zero Trust aplicado ao pipeline adiciona complexidade operacional real. Configurar federação OIDC, manter runners efêmeros, integrar um cofre de segredos e implementar assinatura de artefatos são passos que levam tempo e exigem conhecimento específico. Tentar fazer tudo de uma vez costuma resultar em projetos parados no meio.
A abordagem incremental funciona melhor. Um ponto de partida razoável é eliminar credenciais estáticas de longa duração: migrar para OIDC onde o provedor de nuvem suporta e rotacionar qualquer chave que ainda precise existir. É a mudança de maior impacto por menor custo operacional, e o ponto a partir do qual as outras práticas se constroem com mais segurança.