Pular para o conteúdo

Cadeia de suprimentos de software

Avançado20 min de leituradevsecops

Você revisa o seu código. Mas o que roda em produção inclui centenas de dependências, uma imagem base, ferramentas do build e o próprio runner do CI. Cada um desses pontos é uma forma de colocar código na sua produção sem passar por revisão nenhuma.

Os episódios conhecidos — pacote popular tomado por quem herdou a manutenção, atualização assinada de um fornecedor que distribuiu backdoor, dependência de build comprometida — têm o mesmo formato: o atacante não invadiu o alvo, ele entrou junto com uma atualização legítima. As práticas abaixo atacam esse formato.

Um SBOM (Software Bill of Materials) lista o que existe dentro do artefato. Ele não protege por si só — ele responde, em minutos, a pergunta que aparece quando sai uma CVE grave: “a gente usa isso? onde?”.

Janela do terminal
# gere durante o build, quando as versões já estão resolvidas
syft registry.exemplo.com/loja:1.8.3 -o spdx-json > sbom.json
# ou direto no build da imagem
docker buildx build --sbom=true --provenance=true -t registry.exemplo.com/loja:1.8.3 --push .
# no dia da CVE, a resposta sai do inventário, não de uma força-tarefa
grep -i "log4j" sbom.json

Os dois formatos usados na prática são SPDX e CycloneDX; qualquer ferramenta séria lê os dois. Guarde o SBOM junto do artefato, no registry como anexo, e não em um wiki que envelhece.

SLSA descreve o quanto se pode confiar em como o artefato foi produzido. O que muda de um nível para o outro, em linguagem prática:

  • Nível 1 — o build é automatizado e gera procedência (metadados de como foi feito).
  • Nível 2 — o build roda em serviço hospedado, e a procedência é assinada.
  • Nível 3 — o build é isolado, as fontes de entrada são identificadas e ninguém consegue injetar passo ou variável fora do que está versionado.

Para a maior parte dos times, o alvo realista é o nível 2 com pipeline hospedado (GitHub Actions, GitLab CI) e assinatura via OIDC — sem chave de longa duração no CI.

Assinar responde “quem produziu isto”; a procedência responde “a partir de quê e como”. O Sigstore torna isso viável sem gerenciar chave privada: a identidade do workflow vira o assinante, com certificado de curta duração e registro público em log transparente.

# GitHub Actions — assinatura sem segredo armazenado
permissions:
id-token: write # OIDC: a identidade vem daqui
packages: write
attestations: write
steps:
- uses: sigstore/cosign-installer@v3
- run: cosign sign --yes registry.exemplo.com/loja@${{ steps.build.outputs.digest }}
- run: |
cosign attest --yes --predicate sbom.json --type spdxjson \
registry.exemplo.com/loja@${{ steps.build.outputs.digest }}

Assine sempre pelo digest (@sha256:…), nunca pela tag. Tag é um ponteiro mutável: assinar :1.8.3 garante pouco se alguém puder reapontá-la depois.

Janela do terminal
# verificação: quem assinou e a partir de qual workflow
cosign verify registry.exemplo.com/loja@sha256:abc… \
--certificate-identity-regexp '^https://github.com/empresa/loja/\.github/workflows/.+' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com

A verificação só tem valor com as duas restrições de identidade. cosign verify sem --certificate-identity aceita assinatura de qualquer pessoa do mundo — é o erro mais comum na adoção.

Assinar sem verificar é teatro. A verificação precisa acontecer onde o artefato é usado:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata: { name: exigir-imagem-assinada }
spec:
validationFailureAction: Enforce # comece em Audit por algumas semanas
rules:
- name: verificar-assinatura
match: { any: [{ resources: { kinds: [Pod], namespaces: [prod] } }] }
verifyImages:
- imageReferences: ["registry.exemplo.com/*"]
attestors:
- entries:
- keyless:
issuer: https://token.actions.githubusercontent.com
subject: "https://github.com/empresa/*"

Kyverno também substitui a tag pelo digest ao admitir o Pod, o que fecha a janela entre verificar e executar.

O resto do trabalho é reduzir o que entra sem revisão:

  • Fixe versões com lockfile e commit dele; instale com npm ci, pip install -r com hashes, go.sum verificado.
  • Fixe também as actions do CI por commit SHA, não por tag móvel — o CI é código com acesso a segredos.
  • Espelhe dependências em um registry interno (proxy), para não depender de um pacote público continuar existindo.
  • Imagem base mínima e atualizada reduz a maior fatia de CVEs sem esforço de código.
  • Revise atualização de dependência com a mesma atenção de código próprio; automatize a abertura do PR, não o merge.

Próximo passo: a credencial que o pipeline usa para publicar é o próximo alvo. Continue em Gestão de segredos.