Supply Chain Security: Protegendo sua Pipeline de Software
A segurança da cadeia de suprimentos de software tornou-se uma das maiores preocupações da indústria. Ataques sofisticados como o SolarWinds (2020) e Codecov (2021) demonstraram que comprometer dependências ou componentes intermediários é tão efetivo quanto atacar o alvo final. Neste artigo, exploraremos três pilares essenciais: SBOM (Software Bill of Materials), Cosign para assinatura e verificação, e as práticas de verificação de imagens Docker. Você aprenderá não apenas o quê fazer, mas por quê cada camada importa na defesa em profundidade da sua aplicação.
SBOM: Inventariando sua Stack de Software
O que é SBOM e por que você precisa
Um SBOM (Software Bill of Materials) é um documento estruturado que lista todos os componentes, dependências, versões e licenças utilizadas em um projeto. Pense nele como um "inventário completo" da sua aplicação — semelhante a uma lista de ingredientes em um produto alimentício. Sem essa visibilidade, você não consegue responder perguntas críticas: "Minha aplicação usa a versão vulnerável da Log4j?" ou "Qual é a cadeia de dependências que leva ao OpenSSL 1.0.2?"
Governos, reguladores (como a NIST nos EUA) e grandes empresas agora exigem SBOMs em contratos de software. Mais importante: você não consegue defender o que não consegue ver. Um SBOM bem feito permite detecção automática de vulnerabilidades conhecidas (CVEs) em toda sua stack, não apenas no código principal.
Formatos: SPDX e CycloneDX
Existem dois padrões principais: SPDX (ISO/IEC 5230) focado em conformidade de licenças, e CycloneDX otimizado para segurança de dependências. Para fins práticos de supply chain security, recomendo CycloneDX. Ambos são estruturados em XML ou JSON.
Vamos gerar um SBOM usando Syft, a ferramenta mais prática atualmente:
# Instalar Syft (para macOS/Linux)
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
# Gerar SBOM em formato CycloneDX para uma imagem Docker
syft nginx:latest -o cyclonedx-json > nginx-sbom.json
# Gerar SBOM para um repositório local (Go, Python, Node.js, etc)
syft /caminho/para/seu/projeto -o spdx > project-sbom.spdx
Um exemplo simplificado de SBOM em CycloneDX JSON:
{
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"version": 1,
"components": [
{
"type": "library",
"name": "requests",
"version": "2.28.1",
"purl": "pkg:pypi/requests@2.28.1",
"licenses": [
{
"license": {
"name": "Apache-2.0"
}
}
]
},
{
"type": "library",
"name": "urllib3",
"version": "1.26.12",
"purl": "pkg:pypi/urllib3@1.26.12",
"licenses": [
{
"license": {
"name": "MIT"
}
}
]
}
]
}
Integrando SBOM na Pipeline CI/CD
A geração de SBOM deve ser automática e obrigatória em cada build. Aqui está um exemplo com GitHub Actions:
name: Build e SBOM
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build imagem Docker
run: docker build -t myapp:${{ github.sha }} .
- name: Instalar Syft
run: curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
- name: Gerar SBOM
run: |
syft myapp:${{ github.sha }} \
-o cyclonedx-json \
> sbom-${{ github.sha }}.json
- name: Fazer upload do SBOM
uses: actions/upload-artifact@v3
with:
name: sbom
path: sbom-*.json
- name: Verificar vulnerabilidades conhecidas
run: |
syft myapp:${{ github.sha }} | grype
Note que usamos Grype (também da Anchore) para verificar vulnerabilidades no SBOM — isso é segurança automatizada.
Cosign: Assinando e Verificando Imagens Docker
Criptografia de Confiança: Por que assinar imagens
Toda imagem Docker publicada em um registro (Docker Hub, ECR, Harbor) pode ser baixada por qualquer pessoa e executada. Sem assinatura criptográfica, não há garantia de autenticidade. Um ataque Man-in-the-Middle (MITM) poderia interceptar sua imagem e substituir por uma maliciosa. Cosign resolve isso usando criptografia assimétrica: você assina a imagem com sua chave privada, e qualquer pessoa verifica com sua chave pública.
Cosign é a ferramenta recomendada pela Linux Foundation e funciona perfeitamente com registros OCI (Docker, ECR, GCR, etc).
Instalação e Configuração de Chaves
# Instalar Cosign (requer Go 1.16+)
wget https://github.com/sigstore/cosign/releases/download/v2.0.0/cosign-linux-amd64
chmod +x cosign-linux-amd64
sudo mv cosign-linux-amd64 /usr/local/bin/cosign
# Gerar par de chaves (será solicitada uma senha)
cosign generate-key-pair
# Cria: cosign.key (privada) e cosign.pub (pública)
# Exportar a chave pública em formato PEM para compartilhar
cat cosign.pub
Assinando uma Imagem Docker
# Fazer login no registro (Docker Hub, ECR, etc)
docker login -u seu_usuario
# Build e push da imagem
docker build -t seu_usuario/myapp:v1.0.0 .
docker push seu_usuario/myapp:v1.0.0
# Assinar a imagem (será solicitada a senha da chave privada)
cosign sign --key cosign.key seu_usuario/myapp:v1.0.0
A assinatura é armazenada como uma imagem separada no registro (exemplo: seu_usuario/myapp:v1.0.0.sig). Você pode verificar os metadados assinados:
# Verificar a assinatura (usa a chave pública automaticamente)
cosign verify --key cosign.pub seu_usuario/myapp:v1.0.0
Saída esperada:
[{"critical":{"identity":{"docker-reference":"seu_usuario/myapp"},...}}]
Assinando com Keyless (Sigstore)
A forma mais moderna é usar Keyless Signing via Sigstore, eliminando a necessidade de gerenciar chaves privadas localmente:
# Login com sua conta do GitHub/Google
cosign login ghcr.io
# Assinar sem chave local (usa OIDC do GitHub)
COSIGN_EXPERIMENTAL=1 cosign sign ghcr.io/seu_usuario/myapp:v1.0.0
# Verificar sem chave pública explícita
COSIGN_EXPERIMENTAL=1 cosign verify ghcr.io/seu_usuario/myapp:v1.0.0
Internamente, Cosign usa certificados OIDC do provedor (GitHub) e Rekor (um registro de transparência) para auditoria imutável.
Verificação de Imagens: Policy-as-Code com Kyverno
Por que apenas assinar não é suficiente
Você pode assinar todas as imagens, mas se seu cluster Kubernetes aceitasse uma imagem não assinada, o atacante poderia simplesmente fazer bypass da assinatura. Você precisa de policy enforcement automático — um sistema que rejeite qualquer pod que use uma imagem não assinada ou não confiável.
Kyverno é um admission controller Kubernetes que funciona como um firewall de imagens. Ele verifica políticas antes de qualquer container ser executado.
Instalando Kyverno
# Instalar Kyverno no cluster
helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update
helm install kyverno kyverno/kyverno --namespace kyverno --create-namespace
# Aguardar o deployment
kubectl rollout status deployment/kyverno-admission-controller -n kyverno
Policy: Requerer Imagens Assinadas
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-signed-images
namespace: kyverno
spec:
validationFailureAction: enforce # audit = logging apenas, enforce = bloqueia
rules:
- name: verify-image-signature
match:
resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "seu_usuario/*"
- "gcr.io/seu-projeto/*"
attestors:
- count: 1
entries:
- keys:
publicKeys: |
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----
signatureAlgorithm: sha256
Testando a Policy
# Criar um pod com imagem assinada (vai passar)
kubectl run test-signed --image=seu_usuario/myapp:v1.0.0 \
--dry-run=server
# Tentar criar pod com imagem não assinada (vai falhar)
kubectl run test-unsigned --image=nginx:latest \
--dry-run=server
# Error: admission webhook "mutate.kyverno.io" denied the request
Policy Avançada: Verificar SBOM Anexado
Cosign pode anexar SBOMs à imagem e Kyverno pode verificá-los:
# Anexar SBOM à imagem
cosign attach sbom --sbom sbom-v1.0.0.json seu_usuario/myapp:v1.0.0
# Verificar o SBOM anexado
cosign download sbom seu_usuario/myapp:v1.0.0
Uma policy que verifica presença de SBOM:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-sbom
spec:
validationFailureAction: enforce
rules:
- name: verify-sbom-exists
match:
resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "*"
attestations:
- name: sbom
predicateType: https://cyclonedx.org/schema/ext/vulnerability/4.0/vulnerability.json
conditions:
- all:
- key: "{{ components[0].name }}"
operator: Pattern
value: "?*" # verifica se existe ao menos 1 componente
Integrando Tudo: Pipeline End-to-End
Uma pipeline real deve:
1. Gerar SBOM automaticamente no build
2. Verificar vulnerabilidades do SBOM contra bancos de CVE
3. Assinar a imagem com Cosign
4. Anexar SBOM e atestações à imagem
5. Verificar assinatura no cluster com Kyverno
Aqui está um exemplo completo com GitHub Actions + Kyverno:
name: Secure Build Pipeline
on:
push:
tags:
- 'v*'
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
secure-build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
steps:
- uses: actions/checkout@v3
- name: Setup Cosign
uses: sigstore/cosign-installer@v3
with:
cosign-release: 'v2.0.0'
- name: Setup Syft
run: |
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | \
sh -s -- -b /usr/local/bin
- name: Build imagem Docker
run: |
docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} .
- name: Gerar SBOM
run: |
syft ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} \
-o cyclonedx-json \
> sbom.json
- name: Verificar vulnerabilidades
run: |
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | \
sh -s -- -b /usr/local/bin
grype ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} \
--fail-on high
- name: Fazer login no GHCR
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push imagem
run: |
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
- name: Assinar imagem com Cosign (Keyless)
run: |
cosign sign --yes ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
env:
COSIGN_EXPERIMENTAL: 1
- name: Anexar SBOM à imagem
run: |
cosign attach sbom --sbom sbom.json \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
env:
COSIGN_EXPERIMENTAL: 1
- name: Gerar atestação de build (provenance)
uses: actions/attest-build-provenance@v1
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
No cluster Kubernetes, a policy Kyverno automaticamente bloqueará qualquer imagem não assinada ou vulnerável.
Conclusão
Os três pilares da segurança supply chain que aprendemos — SBOM para visibilidade, Cosign para autenticidade e Kyverno para enforcement — formam uma defesa em profundidade que detém ataques em múltiplas camadas. Primeiro, você conhece exatamente o que está rodando (SBOM). Segundo, você garante que ninguém alterou sua imagem no caminho (Cosign). Terceiro, você rejeita qualquer coisa suspeita automaticamente no cluster (Kyverno). Essa abordagem transforma segurança de um problema reativo para proativo — você não espera ser hacked, você previne que seja hacked. A automatização completa dessa pipeline via CI/CD é o que separa equipes defensoras de equipes vulneráveis no mundo atual.