Construindo Imagens de Container Seguras com Wolfi e Chainguard

Arquitetura da cadeia de build: fluxos e artefatos

Descreva mentalmente o fluxo antes de automatizar: repositório Git com código e apko.yaml, pipeline CI que gera artefatos OCI (imagem, SBOM, assinaturas), registro de imagens com políticas de acesso, scanner de segurança em pipeline e um runtime (Kubernetes) validando assinaturas/attestations. A implementação abaixo transforma esse fluxo em passos reprodutíveis com ferramentas open-source: apko (Wolfi builder), syft/grype (SBOM + scan), cosign (assinatura), oras/skopeo (push/pull) e Terraform para infra.

Instalando ferramentas essenciais no ambiente de build

Use um container de execução ou um runner de CI com as ferramentas instaladas via Go, package managers ou instaladores oficiais. Exemplo de bootstrap para runner Linux (bash):

# instalar apko (Wolfi builder), cosign, syft e grype no runner CI (ubuntu example)
sudo apt-get update && sudo apt-get install -y ca-certificates curl git build-essential
# apko (chainguard-dev/apko)
export GOPATH="$HOME/go"
export PATH="$GOPATH/bin:$PATH"
GO111MODULE=on go install github.com/chainguard-dev/apko/cmd/apko@latest
# cosign para assinatura (sigstore)
GO111MODULE=on go install github.com/sigstore/cosign/cmd/cosign@latest
# syft e grype via instalador oficial (anchore)
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
# oras para push/pull OCI
curl -sSfL https://github.com/oras-project/oras/releases/latest/download/oras_$(uname -s)_$(uname -m).tar.gz | tar -xz -C /usr/local/bin oras

Definindo o pacote Wolfi: apko.yaml de exemplo

Wolfi usa apko para montar um rootfs mínimo com pacotes APK. O arquivo apko.yaml descreve nome, versão, pacotes e variáveis de ambiente do rootfs que será transformado em imagem OCI.

name: myservice
version: "1.0.0"
summary: "Minimal runtime for myservice"
description: |
  Runtime image built on Wolfi with only required packages.
packages:
  - bash
  - ca-certificates
  - curl
  - openssl
files:
  - from: ./bin/myservice
    to: /usr/local/bin/myservice
env:
  PATH: /usr/local/bin:/usr/bin:/bin
entrypoint:
  - /usr/local/bin/myservice

Coloque binários estáticos (ou binários dinâmicos com dependências listadas) em ./bin e faça com que apko copie para o rootfs. Mantendo a imagem mínima reduzimos superfície de ataque.

Construção declarativa sem daemon: apko build e output OCI

Execute o build em runner CI. Apko produz um rootfs e pode empacotar como OCI layout. Usamos o formato OCI para empurrar com oras.

# no diretório do repo com apko.yaml
apko init   # opcional, cria estrutura de exemplo
apko build -o oci-layout .
# output: ./oci-layout/oci-layout/index.json e manifests

Se preferir uma imagem tipo tar (docker-archive) para compatibilidade local, use o passo de empacotamento com oras ou ferramentas de empacotamento que suportem OCI layout para tar/oci-archive.

Gerando SBOM e varredura de vulnerabilidades no pipeline

Gere SBOMs imutáveis e execute scan antes de publicar: syft cria SBOM; grype verifica CVEs contra o SBOM ou a imagem.

# analisar o layout OCI localmente
syft oci-layout:./oci-layout -o cyclonedx-json > sbom.cdx.json
# ou analisar a imagem no registry após push
syft registry.example.com/myteam/myservice:1.0 -o json > sbom.json
# varredura rápida com grype (usa o sbom ou a imagem)
grype oci-layout:./oci-layout --output json > grype-report.json

Automatize falhas do pipeline se o scanner encontrar severidade crítica definida pela sua política (ex: >= HIGH). Use o exit code do grype para decidir.

Assinatura de imagens e attestations (cosign)

Assinar imagens garante origem. Use cosign para assinar o artefato OCI antes do push final. Recomendo usar chaves armazenadas em KMS (AWS KMS, GCP KMS, Azure KeyVault) ou utilizar o fluxo de OIDC + ephemeral keys do Sigstore em runners federados.

# gerar chave local (somente para testes)
cosign generate-key-pair
# assinatura da imagem no registry (usando cosign key)
cosign sign --key cosign.key registry.example.com/myteam/myservice:1.0
# verificar assinatura
cosign verify registry.example.com/myteam/myservice:1.0

Para permitir validação no cluster Kubernetes, armazene a chave pública em um Secret e configure admission controller (ex: cosign-verifier ou policies via Gatekeeper + OPA) para recusar imagens não assinadas ou com assinatura inválida.

Pipeline CI/CD completo: GitHub Actions como referência

Exemplo de workflow que integra build, SBOM, scan, assinatura e push. Este YAML assume um runner com go e privilégios para autenticar no registry e no KMS.

name: ci-build-sign-push
on:
  push:
    branches: [ main ]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install tools
        run: |
          sudo apt-get update && sudo apt-get install -y ca-certificates curl
          GO111MODULE=on go install github.com/chainguard-dev/apko/cmd/apko@latest
          GO111MODULE=on go install github.com/sigstore/cosign/cmd/cosign@latest
          curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
          curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
          curl -sSfL https://github.com/oras-project/oras/releases/latest/download/oras_$(uname -s)_$(uname -m).tar.gz | tar -xz -C /usr/local/bin oras
      - name: Build OCI layout
        run: apko build -o oci-layout .
      - name: Generate SBOM
        run: syft oci-layout:./oci-layout -o cyclonedx-json > sbom.cdx.json
      - name: Scan with grype
        run: grype oci-layout:./oci-layout --output json > grype-report.json
      - name: Fail on high severity
        run: |
          jq '.matches[].vulnerability.severity' grype-report.json | grep -q 'CRITICAL|HIGH' && exit 1 || true
      - name: Push image to registry
        env:
          REGISTRY: ghcr.io
        run: |
          oras push $REGISTRY/myorg/myservice:1.0 --manifest-config ./oci-layout/manifest.json:application/vnd.oci.image.config.v1+json ./oci-layout
      - name: Sign image
        env:
          COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
        run: |
          cosign sign --key ${{ secrets.COSIGN_KEY }} $REGISTRY/myorg/myservice:1.0

Ajuste a política de falha conforme o SLA da sua equipe (por exemplo: bloquear PRs se encontrar CVEs em produção).

Provisionamento da infra de registro (Terraform) e políticas

Crie um repositório ECR com Terraform se estiver em AWS; habilite lifecycle e políticas de retenção. Exemplo mínimo:

resource "aws_ecr_repository" "myservice" {
  name                 = "myorg/myservice"
  image_tag_mutability = "IMMUTABLE"
  image_scanning_configuration {
    scan_on_push = true
  }
  lifecycle {
    prevent_destroy = true
  }
}

Habilite políticas IAM para o runner CI com permissão de putImage/InitiateLayerUpload. Combine isso a um policy-as-code que valida assinaturas antes do deploy no cluster.

Validação no runtime: admission e Supply Chain enforcement

Proteja o cluster Kubernetes validando imagens e SBOMs via admission controller. Use Gatekeeper + OPA ou uma solução de imagem policy (ex: Kyverno). Política exemplo (conceitual): negar deploys cujo image.signature.verify != true ou cujo SBOM contenha CVEs >= HIGH.

# snippet conceitual de ConstraintTemplate (Gatekeeper) - valida assinatura
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: k8simagesigned
spec:
  crd:
    spec:
      names:
        kind: K8sImageSigned
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8simagesigned
        violation[{
          "msg": "image is not cosign-signed",
        }] {
          input.review.object.spec.containers[_].image
          # chamar um webhook/sidecar que verifica a assinatura no registry
        }

Implemente um webhook que consulta cosign verify API ou o repositório de chaves públicas para validar automaticamente.

Boas práticas operacionais e observabilidade

Mantenha imagens imutáveis (tags sem reescrita), retenha SBOMs e relatórios de scan como artefatos no S3/Blob storage. Audite o uso das chaves de assinatura usando logs centralizados (CloudTrail) e rotacione chaves de KMS periodicamente. Automatize o processo de patch: crie pipeline que dispara rebuild ao detectar CVE fix publicada upstream (integre webhooks do feed CVE ou use API de scanner).

Checklist mínimo para pipeline de produção

  • Build declarativo (apko) gerando rootfs/OCI
  • SBOM gerado (CycloneDX/Spdx) e armazenado
  • Scan de vulnerabilidades com política de falha
  • Assinatura de imagem com cosign e chave em KMS
  • Push para registry com tags imutáveis
  • Admission controller no cluster validando assinatura e políticas
  • Auditoria e rotação de chaves

Com essas peças você monta uma pipeline que transforma Wolfi (imagens Wolfi minimalistas) em artefatos assinados, escaneados e auditáveis, prontos para deployment seguro em Kubernetes. Foque em shift-left: SBOM e scan no CI, assinatura automática e verificação no runtime.