Container Security no Pipeline: Trivy, Grype e Signing com Cosign na Prática Já leu

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ê

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:

  1. Build: Constrói a imagem Docker
  2. Security: Executa Trivy e Grype em paralelo, gera relatórios e SBOM
  3. Publish: Só publica se os scans passarem, depois assina com Cosign
  4. 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.


Referências


Artigos relacionados