Container Security no Pipeline: Trivy, Grype e Signing com Cosign
O que é Container Security e por que importa
Container security é o conjunto de práticas e ferramentas que garantem a segurança das imagens Docker (ou OCI) durante todo seu ciclo de vida no pipeline CI/CD. Diferentemente de máquinas virtuais, containers compartilham o kernel do host, o que significa que uma vulnerabilidade em uma imagem pode comprometer toda a infraestrutura. Por isso, é essencial incorporar verificações de segurança logo nas primeiras etapas do pipeline, antes que uma imagem chegue à produção.
O problema é que a maioria dos desenvolvedores ainda trata segurança como algo que vem depois. Você cria a imagem, faz deploy e torça para que nada ruim aconteça. Isso é reativo. Nós vamos trabalhar de forma proativa: escanear vulnerabilidades, verificar integridade das imagens e garantir que apenas imagens confiáveis sejam deployed. As três ferramentas que você vai aprender aqui (Trivy, Grype e Cosign) são exatamente o que você precisa para isso.
Scanning de Vulnerabilidades com Trivy
Entendendo Trivy
Trivy é um scanner de vulnerabilidades de código aberto desenvolvido pela Aqua Security. Ele não apenas escaneia imagens Docker, mas também filesystems, repositórios Git e até artefatos Kubernetes. Para nossos propósitos, vamos focar em imagens de container. Trivy funciona comparando os pacotes instalados na imagem contra bancos de dados de vulnerabilidades conhecidas (CVE - Common Vulnerabilities and Exposures).
A beleza do Trivy é sua velocidade e baixo overhead. Ele não precisa de credenciais especiais, não demanda muito poder computacional e integra facilmente em qualquer pipeline. O Trivy mantém seus bancos de dados de vulnerabilidades atualizados automaticamente, então você sempre está verificando contra as ameaças mais recentes.
Instalação e Uso Básico
Para instalar Trivy em um sistema Linux baseado em Debian:
sudo apt-get install wget apt-transport-https gnupg lsb-release
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee -a /etc/apt/sources.list.d/trivy.list
sudo apt-get update
sudo apt-get install trivy
Uma vez instalado, você pode escanear uma imagem com um simples comando:
trivy image nginx:latest
Isso vai gerar um relatório mostrando vulnerabilidades encontradas, classificadas por severidade (CRITICAL, HIGH, MEDIUM, LOW). Se você quiser um relatório mais legível em JSON:
trivy image --format json --output report.json nginx:latest
Integração em Pipeline CI/CD
No seu pipeline GitLab CI, a integração fica assim:
stages:
- build
- scan
- deploy
build_image:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker build -t myregistry.com/myapp:$CI_COMMIT_SHA .
- docker push myregistry.com/myapp:$CI_COMMIT_SHA
scan_trivy:
stage: scan
image: aquasec/trivy:latest
script:
- trivy image --severity HIGH,CRITICAL --exit-code 1 myregistry.com/myapp:$CI_COMMIT_SHA
allow_failure: false
O parâmetro --exit-code 1 faz o pipeline falhar se encontrar vulnerabilidades de alta severidade ou crítica. Isso impede que imagens vulneráveis avancem para o próximo estágio. Se você quer ser mais agressivo e falhar também com MEDIUM:
trivy image --severity MEDIUM,HIGH,CRITICAL --exit-code 1 myregistry.com/myapp:$CI_COMMIT_SHA
Ignorando Vulnerabilidades Conhecidas
Às vezes você tem uma vulnerabilidade que não afeta sua aplicação ou já está mitigada. Nesse caso, você pode criar um arquivo .trivyignore:
# .trivyignore
AVD-2021-123456 exp:2025-01-01
CVE-2021-12345 exp:2025-06-30
E usar no comando:
trivy image --ignorefile .trivyignore myregistry.com/myapp:latest
Scanning com Grype
Por que usar Grype além de Trivy?
Grype é outra ferramenta de código aberto, dessa vez mantida pela Anchore. Enquanto Trivy é mais focado em velocidade e simplicidade, Grype oferece recursos mais avançados como análise de artefatos software (SBOM - Software Bill of Materials) e suporte a múltiplos formatos de banco de dados. Em um ambiente profissional, é comum usar as duas em conjunto: Trivy como primeira linha rápida e Grype como validação mais profunda.
Grype também permite exportar SBOMs em padrões como CycloneDX e SPDX, o que é útil para compliance e auditoria. Se sua organização precisa fornecer relatórios detalhados sobre componentes de software, Grype é essencial.
Instalação e Uso Básico
Para instalar Grype em um sistema Linux:
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
Para escanear uma imagem:
grype myregistry.com/myapp:latest
Isso vai produzir uma saída em formato tabular mostrando vulnerabilidades encontradas. Se você quer JSON:
grype myregistry.com/myapp:latest -o json > grype-report.json
Gerando SBOM com Grype
Uma das funcionalidades mais poderosas do Grype é gerar um SBOM (Software Bill of Materials). Um SBOM é basicamente uma lista completa de todos os componentes (packages, dependências, versões) que fazem parte de sua imagem:
grype myregistry.com/myapp:latest -o cyclonedx > sbom.xml
Você pode usar diferentes formatos:
grype myregistry.com/myapp:latest -o spdx > sbom.spdx
grype myregistry.com/myapp:latest -o syft-json > sbom.syft.json
SBOMs são críticos para gerenciamento de supply chain security. Com um SBOM, você tem um registro permanente de exatamente quais versões de quais pacotes existiam na imagem em um determinado momento. Isso ajuda em investigações de segurança later.
Integração em Pipeline
No GitHub Actions, a integração fica assim:
name: Grype Scan
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Run Grype scan
uses: anchore/scan-action@v3
with:
image: myapp:${{ github.sha }}
fail-build: true
severity-cutoff: high
- name: Upload SBOM
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: ${{ steps.scan.outputs.sarif }}
O parâmetro fail-build: true faz a ação falhar se encontrar vulnerabilidades com severidade high ou acima (definido por severity-cutoff). O upload do SBOM como SARIF integra os resultados diretamente na interface do GitHub Security.
Assinatura de Imagens com Cosign
O que é Signing e por que você precisa
Signing de imagens é a prática de assinar criptograficamente uma imagem para garantir sua autenticidade e integridade. Pense assim: qualquer um pode fazer docker pull de uma imagem do seu registry e modificá-la antes de fazer push novamente. Sem assinatura, não há forma de saber se a imagem que você está rodando é realmente aquela que você buildou ou se foi comprometida no meio do caminho.
Cosign (mantido pela Linux Foundation via Sigstore) é uma ferramenta que simplifica esse processo. Ela não apenas assina imagens, mas também pode verificar assinaturas, gerenciar chaves e até integrar com Kubernetes para garantir que apenas imagens assinadas sejam deployadas. É o passo final na tríade de segurança: você escaneia vulnerabilidades, depois assina para garantir integridade.
Instalação e Setup Inicial
Para instalar Cosign:
wget https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64
chmod +x cosign-linux-amd64
sudo mv cosign-linux-amd64 /usr/local/bin/cosign
Você precisa de um par de chaves para assinar. Cosign pode gerar isso para você:
cosign generate-key-pair
Isso vai criar dois arquivos: cosign.key (privada) e cosign.pub (pública). A chave privada é o que você usa para assinar, a pública é o que outras pessoas usam para verificar. Nunca compartilhe ou commite sua chave privada no repositório. Use secrets do seu CI/CD provider.
Assinando uma Imagem
Assumindo que você já fez build e push de uma imagem, assinar é simples:
cosign sign --key cosign.key myregistry.com/myapp:v1.0.0
Isso vai pedir a password da chave privada (que você setou ao gerar) e então assinar a imagem. A assinatura é armazenada como um artefato separado no registry, vinculado à imagem original.
Verificando Assinaturas
Para verificar que uma imagem foi realmente assinada:
cosign verify --key cosign.pub myregistry.com/myapp:v1.0.0
Se a assinatura for válida, você vai ver um JSON com os dados da assinatura. Se não for ou não existir, o comando vai falhar.
Integração Completa no Pipeline
Aqui está um exemplo completo em GitHub Actions que cobre build, scan e signing:
name: Secure Build Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-scan-sign:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
security-events: write
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Container Registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
- name: Run Trivy vulnerability scan
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'HIGH,CRITICAL'
- name: Upload Trivy results to GitHub Security
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
- name: Install Cosign
uses: sigstore/cosign-installer@v3
- name: Sign image with Cosign
env:
COSIGN_EXPERIMENTAL: 1
run: |
cosign sign --yes ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
- name: Verify image signature
env:
COSIGN_EXPERIMENTAL: 1
run: |
cosign verify ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
Note o uso de COSIGN_EXPERIMENTAL: 1. Isso ativa o Keyless Signing do Cosign, que usa OpenID Connect (OIDC) para assinar sem precisar gerenciar chaves manualmente. Essa é a abordagem recomendada para CI/CD porque não requer armazenar secrets de chaves privadas.
Policy Enforcement com Cosign e Kubernetes
Uma vez que todas as imagens estão assinadas, você pode configurar o Kubernetes para aceitar apenas imagens assinadas usando ClusterImagePolicy (via Kyverno ou similar):
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-image-signature
spec:
validationFailureAction: enforce
webhookTimeoutSeconds: 30
failurePolicy: fail
rules:
- name: check-cosign-signature
match:
resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "myregistry.com/myapp:*"
attestations:
- name: check-signature
predicateType: https://cosign.sigstore.dev/attestation/v1
required: true
signatureSecret:
name: cosign-pubkey
namespace: kyverno
Isso garante que nenhum pod possa rodar uma imagem de myregistry.com/myapp a menos que esteja devidamente assinada com a chave pública que você configurou. Qualquer tentativa de fazer push de uma imagem modificada ou unsigned vai ser bloqueada no deploy.
Orquestrando Tudo: Um Pipeline Profissional Completo
Cenário Real
Vamos imaginar um pipeline real em GitLab CI que implementa todos os conceitos que você aprendeu. Sua organização tem uma aplicação Node.js que precisa ser securizada desde o build até o deploy em produção:
stages:
- build
- security
- publish
- deploy
variables:
REGISTRY: registry.example.com
IMAGE_NAME: $REGISTRY/$CI_PROJECT_PATH
IMAGE_TAG: $CI_COMMIT_SHA
build_application:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker build -t $IMAGE_NAME:$IMAGE_TAG .
- docker save $IMAGE_NAME:$IMAGE_TAG > image.tar
artifacts:
paths:
- image.tar
expire_in: 1 hour
trivy_scan:
stage: security
image: aquasec/trivy:latest
dependencies:
- build_application
script:
- trivy image --input image.tar --severity HIGH,CRITICAL --exit-code 1
- trivy image --input image.tar --format json --output trivy-report.json
artifacts:
reports:
container_scanning: trivy-report.json
paths:
- trivy-report.json
expire_in: 30 days
allow_failure: false
grype_scan:
stage: security
image: anchore/grype:latest
dependencies:
- build_application
script:
- grype oci-archive:image.tar -o json > grype-report.json
- grype oci-archive:image.tar -o cyclonedx > sbom.xml
artifacts:
paths:
- grype-report.json
- sbom.xml
expire_in: 30 days
allow_failure: true
publish_image:
stage: publish
image: docker:latest
services:
- docker:dind
dependencies:
- build_application
- trivy_scan
- grype_scan
script:
- docker login -u $REGISTRY_USER -p $REGISTRY_PASSWORD $REGISTRY
- docker load < image.tar
- docker push $IMAGE_NAME:$IMAGE_TAG
- docker tag $IMAGE_NAME:$IMAGE_TAG $IMAGE_NAME:latest
- docker push $IMAGE_NAME:latest
only:
- main
cosign_sign:
stage: publish
image: gcr.io/projectsigstore/cosign:latest
script:
- cosign sign --key $COSIGN_KEY $IMAGE_NAME:$IMAGE_TAG
- cosign verify --key $COSIGN_PUB $IMAGE_NAME:$IMAGE_TAG
env:
COSIGN_PASSWORD: $COSIGN_PASSWORD
only:
- main
allow_failure: false
deploy_production:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl set image deployment/myapp myapp=$IMAGE_NAME:$IMAGE_TAG --record
- kubectl rollout status deployment/myapp
environment:
name: production
url: https://myapp.example.com
only:
- main
when: manual
Neste pipeline, você tem uma separação clara:
- Build: Constrói a imagem Docker
- Security: Executa Trivy e Grype em paralelo, gera relatórios e SBOM
- Publish: Só publica se os scans passarem, depois assina com Cosign
- Deploy: Deploy manual (manual gate) para produção com a imagem assinada
Os artefatos de segurança (relatórios e SBOM) são preservados por 30 dias para auditoria.
Tratando Falsos Positivos
Nenhum scanner é perfeito. Você vai encontrar vulnerabilidades que são falsos positivos ou que não se aplicam ao seu caso. Para isso, crie um arquivo .trivyignore e .grypeignore no repositório:
# .trivyignore
AVD-2024-123456 exp:2025-12-31 js-serialize
CVE-2024-54321 exp:2025-06-30
# .grypeignore
CVE-2024-11111|pkg:npm/lodash@4.17.15|
CVE-2024-22222||
Adicione esses arquivos ao repositório e atualize os comandos:
trivy image --ignorefile .trivyignore $IMAGE_NAME:$IMAGE_TAG
grype oci-archive:image.tar --ignore-file .grypeignore
A chave é documentar por que você está ignorando cada vulnerabilidade. Adicione comentários explicando se é um falso positivo, se está mitigado de outra forma ou se tem uma data de resolução planejada.
Conclusão
Você aprendeu três coisas fundamentais que transformam container security de um checkmark teórico em prática real. Primeiro, entendeu que scanning de vulnerabilidades não é opcional — Trivy é rápido o suficiente para rodar em todo commit e Grype oferece profundidade quando você precisa de SBOMs e análises mais sofisticadas. Segundo, assinar imagens com Cosign não é paranoia, é como você garante que o que você fez deploy é realmente aquilo que você buildou, não algo modificado no caminho. Terceiro, e mais importante, essas ferramentas só funcionam quando integradas num pipeline automatizado — você não escaneia e assina manualmente, você faz isso acontecer automaticamente para cada build.