Pular para o conteúdo
16 min de leitura

Segurança da cadeia de suprimentos de software: SBOM, assinatura e SLSA no seu pipeline

Por Equipe Nebular ·

Como blindar sua cadeia de suprimentos de software com SBOM, scan de dependencias, assinatura com cosign e proveniencia SLSA direto no CI/CD.

Neste artigo

Quando você faz o deploy de um serviço, você não está entregando apenas o código que escreveu. Está entregando uma árvore de dependências transitivas com centenas ou milhares de pacotes, uma imagem base que herda binários de sistema que ninguém do seu time revisou, ferramentas de build baixadas em tempo de compilação e camadas de artefatos que passaram por vários registries até chegar em produção. Cada um desses elos é um ponto de entrada, e a maior parte deles está fora do seu controle direto. A cadeia de suprimentos de software é justamente esse conjunto de tudo que entra no seu artefato final sem ter sido escrito por você.

O problema é que a superfície de ataque migrou. Durante anos a gente investiu em firewall, WAF e revisão do código da própria aplicação, enquanto a dependência que puxa outra dependência que puxa um script de pós-instalação passava batida. Casos como o comprometimento do build da SolarWinds — onde o atacante injetou código malicioso no processo de compilação, não no repositório — e o backdoor plantado no utilitário xz/liblzma através de anos de engenharia social sobre um mantenedor mostram, de forma conceitual, que atacar o elo mais fraco da cadeia é mais barato e mais eficaz do que atacar a aplicação diretamente. Cito esses dois apenas como ilustração do padrão: o inimigo não bate na porta da frente, ele entra pela dependência que você confia cegamente.

Este guia é sobre transformar essa confiança cega em confiança verificável. Vamos passar por SBOM, scan de vulnerabilidades em dependências e imagens, assinatura de artefatos com Sigstore/cosign, proveniência e os níveis do SLSA, pinning e lockfiles, imagens distroless e, no fim, como amarrar tudo isso em gates de CI/CD que quebram o build quando algo não bate. O objetivo não é ferramenta pela ferramenta, e sim uma postura: você deveria ser capaz de responder, a qualquer momento, "o que exatamente está rodando em produção, de onde veio, e quem garante isso".

Os três vetores clássicos#

Antes das ferramentas, vale nomear com precisão os três problemas que a gente está tentando resolver, porque cada defesa combate um vetor específico.

O primeiro são as dependências transitivas. Você declara vinte pacotes diretos, mas o grafo resolvido tem oitocentos. Você leu a licença e o histórico de commits dos vinte? Talvez. Dos outros setecentos e oitenta, com certeza não. Uma vulnerabilidade ou um pacote malicioso três níveis abaixo do que você declarou entra no seu artefato com o mesmo privilégio de qualquer código que você mesmo escreveu. A profundidade do grafo é justamente o que torna a auditoria manual impossível e o inventário automatizado obrigatório.

O segundo é a imagem base. Quando você escreve FROM node:20 ou FROM python:3.12, está herdando um sistema operacional inteiro — shell, gerenciador de pacotes, dezenas de bibliotecas de sistema, muitas delas sem relação nenhuma com o que sua aplicação precisa. Cada uma dessas bibliotecas pode ter CVEs conhecidos, e cada binário extra é uma ferramenta a mais na mão de quem eventualmente conseguir execução dentro do container. A tag latest piora tudo: ela é um alvo móvel, então "funcionou ontem" não diz nada sobre o que você está baixando hoje.

O terceiro é o typosquatting e o sequestro de pacotes. O atacante publica reqeusts no lugar de requests, ou python-dateutil com um hífen a mais, contando com um erro de digitação ou uma sugestão errada de um assistente. Também entram aqui o dependency confusion — quando um pacote interno com o mesmo nome de um público é resolvido do registry errado — e o comprometimento de conta de um mantenedor legítimo. Esses ataques exploram o fato de que o install roda scripts arbitrários com as permissões do seu usuário de build.

Repare que nenhum desses três é resolvido revisando melhor o seu próprio código. Eles exigem inventário, verificação de origem e barreiras automatizadas. É o que vem a seguir.

SBOM: o inventário que torna tudo auditável#

SBOM significa Software Bill of Materials — a lista de materiais do seu software. É um documento legível por máquina que enumera cada componente presente no artefato: nome, versão, hash, licença e, idealmente, a relação entre eles. Sem SBOM, quando uma vulnerabilidade nova aparece numa biblioteca popular, a pergunta "eu estou afetado?" vira uma caçada manual de horas ou dias entre repositórios. Com SBOM versionado, é uma consulta.

Existem dois formatos padronizados que dominam o mercado. O SPDX, mantido pela Linux Foundation e formalizado como padrão ISO, tem forte tradição na parte de licenciamento e compliance. O CycloneDX, mantido pela OWASP, nasceu com foco em segurança e é muito prático para descrever relações de dependência e vulnerabilidades. Os dois são amplamente suportados; escolha um, seja consistente, e prefira gerar em ambos se seus consumidores downstream pedirem formatos diferentes.

A ferramenta mais direta para gerar SBOM hoje é o Syft, do projeto Anchore. Ela examina tanto o sistema de arquivos de um projeto quanto uma imagem de container e produz o inventário no formato que você pedir.

```bash # Gera SBOM de uma imagem em CycloneDX (JSON) syft registry.exemplo.com/api:1.4.2 \ -o cyclonedx-json=sbom.cdx.json

# Gera SBOM do código-fonte local em SPDX syft dir:. -o spdx-json=sbom.spdx.json

# Inspeciona rápido em tabela, sem gravar arquivo syft registry.exemplo.com/api:1.4.2 -o table ```

O ponto importante: o SBOM só tem valor se for gerado no pipeline, a partir do artefato real que vai para produção, e não à mão num momento qualquer. Um SBOM desatualizado é pior que nenhum, porque dá falsa sensação de cobertura. Gere no mesmo job que constrói a imagem, anexe o SBOM ao artefato (voltaremos a isso na parte de atestações) e versione. Assim o inventário e o binário nascem juntos e ninguém pode divergir sem que apareça.

Scan de vulnerabilidades e SCA#

Ter o inventário é metade do caminho; a outra metade é cruzar esse inventário com bases de vulnerabilidades conhecidas. Isso é o SCA — Software Composition Analysis. A ideia é olhar cada componente que você importou (não o código que você escreveu, que é trabalho de SAST) e perguntar se alguma versão presente tem CVE publicado.

O Trivy, da Aqua Security, e o Grype, da Anchore, são as duas ferramentas open source mais usadas para isso. Ambas escaneiam imagens de container, sistemas de arquivos e — este é o detalhe elegante — o próprio SBOM que o Syft gerou. Escanear a partir do SBOM em vez de reescanear a imagem economiza tempo e garante que você está avaliando exatamente o inventário que declarou.

```bash # Trivy escaneando uma imagem, falhando em severidade alta/crítica trivy image \ --severity HIGH,CRITICAL \ --exit-code 1 \ --ignore-unfixed \ registry.exemplo.com/api:1.4.2

# Grype consumindo o SBOM já gerado pelo Syft grype sbom:sbom.cdx.json \ --fail-on high ```

Duas decisões práticas fazem diferença aqui. A primeira é a flag --ignore-unfixed (ou equivalente): faz sentido, no gate que quebra o build, focar nas vulnerabilidades que já têm correção disponível — cobrar do time que atualize o que dá para atualizar, sem travar por CVE que ainda não tem patch de ninguém. As vulnerabilidades sem correção você registra e monitora, mas não bloqueia por elas eternamente. A segunda é o processo de exceção auditável: vai existir o falso positivo e o CVE que comprovadamente não te afeta (o código vulnerável não está no caminho de execução). Trate isso com um arquivo de exceções versionado, com justificativa e prazo de revisão, nunca desligando o scan inteiro. Silenciar o scanner para o build passar é exatamente o anti-padrão que a gente combate — o problema continua lá, só ficou invisível.

Rode o scan em dois momentos distintos: nas dependências, o quanto antes (idealmente já no pull request, para dar feedback rápido a quem abriu), e na imagem final, logo antes de publicar. São camadas diferentes: o primeiro pega o pacote de aplicação, o segundo pega também a biblioteca de sistema que veio na imagem base.

Assinatura e verificação com Sigstore#

Inventariar e escanear responde "o que tem dentro". A assinatura responde outra pergunta, igualmente crítica: "esse artefato é mesmo o que meu pipeline produziu, ou alguém trocou pelo caminho?". Sem assinatura, quem controla o registry controla o que você faz deploy — e o registry é justamente um dos pontos que você não gerencia sozinho.

O projeto Sigstore resolveu o maior atrito histórico da assinatura, que era a gestão de chaves privadas. A ferramenta cosign permite o modo keyless: em vez de guardar uma chave de longa duração (que vaza, que precisa ser rotacionada, que alguém copia para a máquina errada), o cosign usa a identidade OIDC do próprio job de CI para obter um certificado de curtíssima duração da autoridade Fulcio, assina com ele, e registra a assinatura num log de transparência público e imutável chamado Rekor. A chave existe por segundos e some. Fica registrado que aquela identidade (por exemplo, o workflow tal do repositório tal) assinou aquele digest naquele momento.

```bash # Assinatura keyless no CI: sem chave privada, usa OIDC do runner COSIGN_EXPERIMENTAL=1 cosign sign \ registry.exemplo.com/api@sha256:9f2b...c1

# Verificação em qualquer ponto: exige a identidade e o emissor esperados cosign verify \ --certificate-identity-regexp 'https://github.com/minhaorg/.+' \ --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ registry.exemplo.com/api@sha256:9f2b...c1 ```

Note dois detalhes que fazem a assinatura ser real e não teatro. Primeiro: você assina o digest (@sha256:...), nunca a tag. Tag é mutável; digest é o conteúdo. Assinar tag é assinar um ponteiro que pode mudar embaixo de você. Segundo: na verificação, você precisa especificar a identidade esperada (certificate-identity) e o emissor (oidc-issuer). Verificar só que "existe alguma assinatura" não protege de nada — o atacante também sabe assinar. O que protege é exigir que a assinatura tenha vindo da identidade certa, do pipeline certo. Essa é a diferença entre autenticidade e um carimbo qualquer.

Proveniência e SLSA#

Assinar prova quem publicou. Proveniência prova como o artefato foi construído — um registro verificável de qual código-fonte, com qual commit, em qual builder, produziu aquele binário. É o que teria detectado o padrão do ataque à SolarWinds: mesmo com o repositório íntegro, a proveniência mostraria que o binário publicado não correspondia bit a bit ao que o código-fonte deveria gerar.

O SLSA (Supply-chain Levels for Software Artifacts, pronuncia-se "salsa") é o framework que organiza isso em níveis crescentes de garantia. Pensando de forma prática, os níveis progridem mais ou menos assim:

  • Nível 0 — sem nenhuma garantia; o estado padrão de quem nunca pensou no assunto.
  • Nível 1 — o processo de build é automatizado e gera um documento de proveniência descrevendo como o artefato foi feito. Ainda não é resistente a adulteração, mas já existe o registro.
  • Nível 2 — a proveniência é assinada e gerada por um serviço de build hospedado, não pela máquina do desenvolvedor. Fica mais difícil forjar.
  • Nível 3 — o build acontece em ambiente hermético e isolado, com proveniência inforjável. Hermético significa que o build declara todas as suas entradas e roda sem acesso arbitrário à rede, então ninguém injeta nada de fora no meio da compilação. Este é o alvo realista para times que levam supply chain a sério.

O ponto do build hermético merece ênfase porque é o que fecha a brecha mais insidiosa. Se durante a compilação seu build baixa uma ferramenta de uma URL qualquer, ou resolve uma dependência sem versão fixa, o resultado deixa de ser reproduzível e passa a depender do que estava disponível na internet naquele segundo. Um build hermético recebe todas as entradas de forma declarada e verificada; rodar duas vezes produz o mesmo bit. Reprodutibilidade não é preciosismo acadêmico — é a propriedade que permite a alguém conferir, de forma independente, que o binário corresponde ao código.

Na prática, você não implementa SLSA do zero. Geradores de proveniência para as principais plataformas de CI produzem a atestação no formato in-toto e a assinam com o mesmo Sigstore. O que você precisa entender é que proveniência é um artefato de primeira classe: nasce no build, é assinada, e é verificada na entrada do ambiente que vai executar.

Atestações: colando tudo ao artefato#

Você reparou que juntou três documentos em volta da imagem — SBOM, resultado de scan e proveniência. Uma atestação é o mecanismo que amarra qualquer um desses documentos ao digest do artefato, de forma assinada. Em vez de o SBOM ser um arquivo solto que pode se perder ou ser trocado, ele vira uma afirmação assinada: "eu, esta identidade, atesto que este SBOM descreve esta imagem, identificada por este digest".

```bash # Anexa o SBOM como atestação assinada, ligada ao digest COSIGN_EXPERIMENTAL=1 cosign attest \ --predicate sbom.cdx.json \ --type cyclonedx \ registry.exemplo.com/api@sha256:9f2b...c1

# Downstream: verifica que a atestação de SBOM existe e é da identidade certa cosign verify-attestation \ --type cyclonedx \ --certificate-identity-regexp 'https://github.com/minhaorg/.+' \ --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ registry.exemplo.com/api@sha256:9f2b...c1 ```

A elegância aqui é que a atestação segue o artefato pelo registry. Quem for consumir a imagem — outro time, um cluster, um cliente — pode verificar, sem confiar na sua palavra, que o SBOM e a proveniência daquele digest existem, são assinados pela identidade esperada e não foram adulterados. A confiança deixa de ser social e passa a ser criptográfica.

Pinning, lockfiles e imagens distroless#

Duas práticas de higiene reduzem drasticamente a superfície antes mesmo de qualquer scanner rodar, e são baratas de adotar.

A primeira é o pinning rigoroso. No nível de pacotes, isso significa comitar o lockfile (package-lock.json, poetry.lock, Cargo.lock, go.sum) e instalar em modo estrito, que falha se a árvore resolvida divergir do lock em vez de silenciosamente resolver versões novas. O lockfile fixa não só a versão mas o hash de integridade de cada pacote transitivo — é ele que impede que um pacote seja trocado por outro conteúdo sob o mesmo número de versão. No nível de imagem, o pinning significa referenciar a base pelo digest, não pela tag:

```dockerfile # Ruim: alvo móvel, muda sem você saber FROM node:20

# Bom: conteúdo fixo e verificável FROM node:20-slim@sha256:a1b2c3d4e5f6...

# Melhor ainda para runtime: distroless por digest FROM gcr.io/distroless/nodejs20-debian12@sha256:7a8b9c... ```

A segunda prática é a imagem distroless. A ideia é que a imagem de runtime contenha apenas a sua aplicação e as bibliotecas estritamente necessárias para ela rodar — sem shell, sem gerenciador de pacotes, sem os cem utilitários que vêm numa distribuição completa. Isso ataca dois dos três vetores de uma vez: reduz o número de componentes que o scanner encontra (menos CVEs de sistema, menos ruído) e reduz o que um atacante tem à disposição caso consiga executar código dentro do container — sem sh, sem curl, muitas técnicas de pós-exploração simplesmente não têm ferramenta. O caminho natural é o multi-stage build: um estágio pesado compila com todas as ferramentas, e o estágio final distroless copia só o binário pronto. A imagem que vai para produção fica pequena, com pouquíssima superfície, e o SBOM dela é curto o suficiente para ser realmente auditável.

Verificação em admission: a última barreira#

Todo o trabalho de assinar e atestar só vale se alguém verificar antes de executar. É aqui que entra o controle de admission no cluster: uma política que intercepta cada requisição para rodar uma imagem e a rejeita se não houver assinatura válida e atestações da identidade esperada. É a fronteira onde a confiança criptográfica vira decisão de sim ou não.

Ferramentas de policy como as que integram com o Sigstore (por exemplo, controllers de admission que consultam a política de verificação) permitem declarar isso como código. O cluster passa a recusar qualquer imagem que não passe pela verificação — independentemente de quem tentou fazer o deploy ou de qual pipeline mandou.

```yaml # Política de admission (conceitual): só admite imagem assinada # pela identidade do pipeline oficial, com SBOM atestado. apiVersion: policy.sigstore.dev/v1beta1 kind: ClusterImagePolicy metadata: name: exige-assinatura-e-sbom spec: images:

  • glob: "registry.exemplo.com/**"

authorities:

  • keyless:

identities:

  • issuer: "https://token.actions.githubusercontent.com"

subjectRegExp: "https://github.com/minhaorg/.+" attestations:

  • name: sbom-obrigatorio

predicateType: "https://cyclonedx.org/bom" ```

Esse é o fecho lógico da cadeia. Você pode ter o pipeline mais rigoroso do mundo, mas se o cluster aceita qualquer imagem, basta alguém com acesso ao registry fazer um push lateral para contornar tudo. A admission garante que o único caminho para produção passa pela porta que você fortificou — a que exige assinatura da identidade certa e as atestações que você definiu como mínimas.

Encaixando no pipeline: gates que quebram o build#

Nada disso funciona como recomendação opcional. A diferença entre segurança de cadeia de suprimentos real e um relatório bonito que ninguém lê é o gate: o passo do CI que retorna código de saída diferente de zero e trava a promoção do artefato quando algo não bate. Um pipeline maduro encadeia os controles nesta ordem, cada um sendo um portão:

```yaml # Esboço de pipeline: cada passo é um gate que pode quebrar o build steps:

  • name: instalar-deps-do-lockfile

run: npm ci # falha se a árvore divergir do lockfile

  • name: gerar-sbom

run: syft dir:. -o cyclonedx-json=sbom.cdx.json

  • name: scan-sca

run: grype sbom:sbom.cdx.json --fail-on high # quebra em HIGH+

  • name: build-imagem

run: docker build -t registry.exemplo.com/api:${SHA} .

  • name: scan-imagem

run: trivy image --severity HIGH,CRITICAL --ignore-unfixed \ --exit-code 1 registry.exemplo.com/api:${SHA}

  • name: assinar-e-atestar

run: | cosign sign registry.exemplo.com/api@${DIGEST} cosign attest --predicate sbom.cdx.json \ --type cyclonedx registry.exemplo.com/api@${DIGEST} ```

A sequência importa. Você instala do lockfile (portão contra troca de pacote), gera o SBOM, escaneia as dependências antes de gastar tempo construindo, constrói, escaneia a imagem final com as bibliotecas de sistema incluídas, e só então assina e atesta o que passou por tudo. Cada passo que falha impede o próximo. O artefato que sai no fim é, por construção, um artefato que tem inventário conhecido, sem CVE alto sem correção, assinado pela identidade do pipeline e com SBOM atestado colado a ele.

Comece pequeno e evolua a rigidez. É perfeitamente aceitável rodar os primeiros scans em modo de aviso por algumas semanas, para o time enxergar o tamanho do backlog sem travar entregas — mas com data marcada para virar o gate obrigatório. O que não pode acontecer é o modo aviso virar permanente, porque aí o controle é decorativo. E a regra de ouro em qualquer desses passos: quando o gate acusa, você conserta a cadeia — atualiza a dependência, troca a base, registra a exceção justificada — nunca desliga o gate para o build passar. Desligar o portão não resolve o problema; só transfere a descoberta dele para o atacante.

Fechando: segurança de cadeia de suprimentos não é um produto que você compra, é uma propriedade que você constrói etapa por etapa. Se você sair daqui e fizer só três coisas, faça estas: comite e imponha o lockfile, gere o SBOM no pipeline e adote uma imagem distroless por digest. Esses três já cortam boa parte da superfície com esforço baixo. Assinatura keyless, proveniência SLSA e admission são o próximo degrau, e cada um deles transforma um pouco mais da confiança cega que você deposita hoje em confiança que você consegue verificar amanhã. No fim, o objetivo é simples de enunciar e trabalhoso de garantir: saber, com prova, exatamente o que roda em produção e de onde veio cada pedaço.

Leituras relacionadas

Nenhum comentário ainda

Seja o primeiro a comentar.

Deixe seu comentário

Entre com sua conta Canverly para comentar. Você pode usar a mesma conta em qualquer site da rede.

Entrar com Canverly