Validando assinaturas de imagens Docker no Kubernetes com cosign e OPA Gatekeeper
Por John Pires · 19 de março de 2026

Validando assinaturas de imagens Docker no Kubernetes com cosign e OPA Gatekeeper
Motivacao e trade-offs
Por que assinar imagens importa
A cadeia de suprimento de software (software supply chain) é um vetor crítico de ataques: imagens comprometidas podem chegar ao cluster através de pushs maliciosos, CI comprometido ou cadastros de terceiros. Assinar imagens garante autenticidade e integridade entre build e deploy. Verificar assinaturas antes do agendamento de pods reduz risco operacional e facilita auditoria.
Trade-offs práticos
Existem escolhas a fazer: chave gerenciada vs chaveless (Fulcio), verificação no CI vs verificação em runtime, e implementação de políticas via Gatekeeper vs webhooks dedicados. Cada opção tem custo/benefício:
- Chave privada (armazenada em CI/CD): maior controle, necessidade de rotação e proteção.
- Keyless (Fulcio): conveniência, menor responsabilidade pelo gerenciamento de chaves, depende de terceiros (trust).
- Verificação no CI: impede push de imagens não assinadas, mas não protege contra imagens assinadas por actor malicioso.
- Verificação no admission (Gatekeeper): garante que apenas imagens verificadas rodem no cluster — é aquela defesa em profundidade que recomendamos.
Tip: combine checks em CI (filtro precoce) com verificação em admission (controle definitivo). Assim você reduz latência de deploy e mantém segurança no cluster.
Fundamentos tecnicos
Como o cosign funciona (visão prática)
cosign assina artefatos OCI (imagens) e armazena assinaturas em registros ou em um repositório de confiança (Rekor). Assinaturas podem ser feitas com chave pública/privada ou keyless via Fulcio/Transparent Log. A verificação consiste em: recuperar assinatura do registro, validar cadeia de confiança e checar que o digest do artefato corresponde à assinatura.
Como o Gatekeeper/Opa funciona
OPA Gatekeeper é um Admission Controller que aplica políticas Rego declarativas. Permite criar ConstraintTemplates (com rego embutido) e Constraints que declaram regras específicas. Gatekeeper é altamente reutilizável para validação de recursos Kubernetes na etapa de admission.
Arquitetura recomendada
Arquitetura prática e implementável sem depender de execuções remotas do Rego:
- CI constrói e assina imagem com cosign (key/keyless) e publica assinatura no registro.
- Um componente verificador (CronJob/controller) em Kubernetes ou um processo externo verifica assinaturas e atualiza uma lista de digests aprovados em um Constraint Gatekeeper (ou em uma ConfigMap/CRD que você usa como fonte de verdade para as políticas).
- Gatekeeper valida pods usando essa lista: imagens devem usar digest (sha256:...) e estar na lista de aprovados.
Esse padrão separa responsabilidades de verificação (ferramenta que tem cosign disponível) e validação (Gatekeeper), permitindo políticas expressivas sem exigir que Rego execute verificações externas.
Exemplo 1: caso simples (assinatura com chave e validação via Gatekeeper)
Contexto e objetivo
Objetivo: forçar que apenas imagens com assinaturas verificadas sejam agendadas. Fluxo simples: CI assina imagem com cosign (chave RSA), um script verifica e atualiza um Constraint com digests aprovados, Gatekeeper bloqueia pods que referenciam imagens não aprovadas.
Assinar imagem com cosign (com chave)
O comando abaixo gera uma chave e assina uma imagem. Ele deve rodar na pipeline de CI antes do push ou logo após o push.
O que faz: gera par de chaves e cria assinatura da imagem referenciada por TAG. A assinatura é armazenada no registro (padrão cosign: o ramo "signature" do manifest).
# gera chave (local, guardada no CI secrets vault melhor que workspace do runner)
cosign generate-key-pair && ls -l cosign.key cosign.pub
# assina a imagem (tag existente):
cosign sign --key cosign.key registry.example.com/myapp:1.2.3
# verifica localmente:
cosign verify --key cosign.pub registry.example.com/myapp:1.2.3
O que acontece: após o sign, a imagem no registry tem associação de assinatura; verify retorna 0 se a assinatura bater com a chave pública.
ConstraintTemplate e Constraint Gatekeeper (Rego)
Contexto: vamos criar um ConstraintTemplate que lê uma lista de digests no campo "parameters.approvedDigests" do Constraint. Um processo externo vai atualizar esse campo. O Rego valida que todo container do pod usa digest e que o digest está na lista.
O que faz: define a política que será aplicada pelo Gatekeeper durante admission.
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8simageverified
spec:
crd:
spec:
names:
kind: K8sImageVerified
validation:
openAPIV3Schema:
properties:
approvedDigests:
type: array
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8simageverified
violation[{
"msg": msg
}] {
input.review.kind.kind == "Pod"
# pega todos os images dos containers
some i
image := input.review.object.spec.containers[i].image
not image_is_allowed(image, input.parameters.approvedDigests)
msg := sprintf("image %v is not an approved signed image", [image])
}
image_is_allowed(image, approved) {
# força uso de digest: registry/path@sha256:...
contains(image, "@sha256:")
parts := split(image, "@")
digest := parts[1]
digest == _
approved[_] == digest
}
O que acontece: crie esse ConstraintTemplate no cluster; ele define a regra que depois será usada pelo Constraint.
Constraint exemplo
Contexto: Constraint com campo approvedDigests vazio inicialmente. Uma etapa de verificação vai preencher esse campo com os digests verificados.
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sImageVerified
metadata:
name: approved-image-digests
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
approvedDigests: []
O que acontece: o Constraint existe e bloqueia imagens que não usem digest (ou que usem digest não listado). Ainda não bloqueia nada até que approvedDigests seja preenchido.
Script simples para verificar e atualizar Constraint
Contexto: processo externo mínimo (pode ser job CI, pipeline ou CronJob) que usa cosign para verificar e atualiza o Constraint via kubectl patch. Esse script assume que cosign.verify pode retornar digest e que kubectl tem permissão para patch.
# script: verify-and-patch.sh
# entrada: imagem (registry/path:tag), key pública, nome do constraint
IMAGE="$1" # ex: registry.example.com/myapp:1.2.3
PUBKEY="$2" # arquivo cosign.pub
CONSTRAINT_NAME="$3" # approved-image-digests
# verifica e extrai digest
DIGEST=$(cosign verify --key "$PUBKEY" -json "$IMAGE" | jq -r '.[0].critical.identity.docker-reference // empty')
# Observação: dependendo da versão do cosign, ajuste a extração do digest. Aqui usamos um fallback simples:
if [ -z "$DIGEST" ]; then
# fallback: resolve digest via skopeo inspect
DIGEST=$(skopeo inspect --format '{{.Digest}}' docker://$IMAGE)
fi
# constrói payload para patch (adiciona digest ao array se não existir)
PATCH=$(cat <<EOF
{"spec": {"parameters": {"approvedDigests": ["$DIGEST"]}}}
EOF
)
kubectl patch K8sImageVerified $CONSTRAINT_NAME --type merge -p "$PATCH"
O que acontece: o script verifica a assinatura, resolve o digest e faz um patch no Constraint. Em produção, você vai querer lógica para adicionar/mesclar múltiplos digests e tratar concorrência.
Exemplo 2: caso avancado do mundo real (GitHub Actions, attest e validação por claim)
Objetivo e cenários
Caso real: pipeline GitHub Actions que produz uma imagem e gera uma attestation (provenance) contendo metadata do builder (SHA do repo, runner, workflow). Queremos validar não só assinatura, mas também atributos do build — por exemplo, que a imagem veio da branch main e foi construída por GitHub Actions do nosso org.
GitHub Actions: build, sign e attest
Contexto: workflow que faz build, push, sign (keyless) e cria uma attestation com cosign attest usando predicate de in-toto.
O que faz: compila imagem, faz push, gera assinatura keyless (fulcio) e grava uma attestation com claims que descrevem a build.
name: build-and-sign
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build image
run: |
IMAGE=registry.example.com/myapp:${{ github.sha }}
docker build -t $IMAGE .
echo "IMAGE=$IMAGE" >> $GITHUB_ENV
- name: Push image
run: |
docker push $IMAGE
- name: Sign image keyless (fulcio)
env:
COSIGN_EXPERIMENTAL: "" # dependendo da versão
run: |
cosign sign --keyless $IMAGE
- name: Create attestation
run: |
echo '{"builder":"github-actions","repo":"'${{ github.repository }}'","ref":"'${{ github.ref }}'","sha":"'${{ github.sha }}'"}' > predicate.json
cosign attest --keyless --predicate predicate.json $IMAGE
O que acontece: imagem é assinada e uma attestation (predicate.json) é anexada. Cosign keyless usa Fulcio e Rekor para log de transparência; a attestation pode ser verificada por quem tiver a política de aceitação.
Verificador avançado (CronJob) que valida claims)
Contexto: um CronJob dentro do cluster executa periodicamente cosign verify --attestation e, se a attestation cumprir requisitos (por exemplo, builder == github-actions e repo na allowlist), extrai digest e atualiza Constraint; caso contrário, alerta via Slack/Prometheus.
O que faz: executa verificação avançada e atualiza a Constraint do Gatekeeper com digests aprovados.
apiVersion: batch/v1
kind: CronJob
metadata:
name: image-verifier
spec:
schedule: "*/5 * * * *"
jobTemplate:
spec:
template:
spec:
serviceAccountName: verifier-sa
containers:
- name: verifier
image: registry.example.com/tools/cosign-verifier:latest
env:
- name: REGISTRY
value: registry.example.com
- name: CONSTRAINT_NAME
value: approved-image-digests
volumeMounts: []
restartPolicy: OnFailure
O que acontece: o container executa um script que lista tags, para cada tag roda "cosign verify --keyless --attestation" e valida o predicate (checa JSON). Se ok, extrai digest e atualiza o Constraint. Esse container precisa ter cosign e kubectl configurados (ServiceAccount e RBAC).
Rego para checar claims (exemplo)
Contexto: além de checar digest, você pode exigir que a attestation inclua um claim específico. Neste pattern, o CronJob só popula approvedDigests para imagens que passaram nos critérios; Gatekeeper continua simples. Se quiser checar claims diretamente no Rego, sua fonte de verdade precisa incluir os claims no parâmetro do Constraint.
# parâmetro do Constraint poderia conter objetos {digest: "sha256:...", predicate: {builder:..., repo:...}}
# Exemplo de checagem em Rego (snippet dentro do ConstraintTemplate rego acima):
# assume input.parameters.approvedEntries é uma lista de objetos {digest, builder, repo}
# função hipotética
allowed_by_claims(image, entries) {
contains(image, "@sha256:")
parts := split(image, "@")
digest := parts[1]
some i
entries[i].digest == digest
entries[i].builder == "github-actions"
startswith(entries[i].repo, "myorg/")
}
O que acontece: essa lógica exige que a fonte de dados (atualizada pelo verificador) contenha os claims que você aceita e vincule digest a claims.
Debugging e troubleshooting
Erros comuns e armadilhas
Lista prática de problemas que você vai encontrar:
- Gatekeeper rejeita pods porque imagem não usa digest (use sempre @sha256:... no deploys declarativos).
- CASO: cosign verify falha por versão incompatível do cosign ou flags diferentes (sempre pin na versão em CI e verificador do cluster).
- Problemas de RBAC: o verificador precisa permissão para patch em Constraints; sem isso, a lista nunca é atualizada.
- Condições de corrida ao patch: use lógica de merge/patch que mescle arrays ao invés de sobrescrever; considere usar uma controller com lock.
- Key mismatch: se você usar chave pública em vez do keyless, mismatch entre chave usada para sign e a pública no verificador resulta em verify fail.
Dica prática: durante a implementação inicial, deixe Gatekeeper em modo permissivo (breakglass) ou direcione para namespaces de staging até automatizar o verificador e confirmar digests corrigidos.
Comandos e logs úteis
Como investigar problemas de verificação e políticas:
- Logs do Gatekeeper: kubectl -n gatekeeper-system logs deploy/gatekeeper-audit
- Verificar Constraint: kubectl get K8sImageVerified -o yaml
- Debug cosign: cosign verify --key cosign.pub --verbose registry.example.com/myapp:1.2.3
- Inspecionar o conteúdo do attestation: cosign verify-attestation --keyless --format json registry.example.com/myapp:1.2.3
Solucoes para latencia e escala
Em clusters grandes, atualizar um único Constraint com centenas de digests pode ser ineficiente. Padrões para mitigar:
- Particione Constraints por aplicação/namespace.
- Use um CRD (ex: ImageVerification) com entradas por imagem; Gatekeeper pode consultar essa fonte para validação por nome de imagem em vez de varrer um grande array.
- Indexed lookups: mantenha mapas digest->true em parâmetros menores.
Conclusao pratica
Resumo de passos para implantação segura
Passos práticos para adotar esse padrão com mínimo risco:
- Pin nas versões do cosign e da imagem do verificador; crie secrets para chaves quando usar keypair.
- Modifique pipelines CI para assinar e (ideal) gerar attestations com metadata.
- Implemente um verificador que consome signatures/attestations e popula uma fonte de verdade (Constraint, CRD ou ConfigMap) que Gatekeeper lê.
- Implemente ConstraintTemplate + Constraint e teste em ambiente de staging com imagens de teste.
- Monitore logs e métricas do verificador e do gatekeeper; automatize alarmes para assinaturas inválidas ou falhas de sync.
Notas finais e boas práticas
Combinar verificação em CI e política no admission é a prática recomendada: CI falha rápido, Gatekeeper garante defesa em runtime. Proteja suas chaves quando usar keypair; considere keyless para reduzir surface area, mas saiba que isso implica confiar na cadeia Fulcio/Rekor. Documente o fluxo para a equipe e automatize rotinas de rotação e recuperação.
Implementar esse padrão exige coordenação entre times de plataforma e de CI. Comece pequeno (um namespace ou app crítico) e expanda quando automática e robustamente comprovado. A segurança da cadeia de build é incremental: cada camada adicional reduz risco e aumenta confiança.
Assuntos: Kubernetes, Docker, cosign, opa, gatekeeper, github-actions
Leia também
Vault + GitHub Actions (OIDC): secrets dinâmicos e melhores práticas
O objetivo é eliminar credenciais estáticas armazenadas no repositório ou em secrets do GitHub. Em vez disso, o pipeline solicita um token OIDC curto…
Acelerando builds Docker em CI usando cache BuildKit (export/import) no GitHub Actions
Reduza drasticamente tempo de builds Docker no GitHub Actions usando BuildKit cache export/import. Veja exemplos reais de Dockerfile, workflows, erros comuns…
Ambientes de preview em PRs com GitHub Actions e Docker Compose — passo a passo
Aprenda a criar ambientes de preview para PRs usando GitHub Actions e Docker Compose. Mostro o jeito errado e o certo, com exemplos reais, scripts de saúde e…
Canary deploy com GitHub Actions e Docker: métricas, rollout e rollback automático
Pipeline prático de canary deploy com GitHub Actions, Docker e Kubernetes: exemplos funcionais de Dockerfile, manifests Nginx-canary, workflow e script de…
Escanear imagens Docker no CI com Trivy e GitHub Actions — falhe por CVSS
Aprenda a escanear imagens Docker no CI com Trivy e GitHub Actions e a fazer o pipeline falhar quando uma vulnerabilidade atingir um limiar CVSS. Passo a…
Canary deploy com GitHub Actions em Kubernetes usando Helm e Flagger
Guia prático para implementar canary deploy com GitHub Actions, Helm e Flagger: padrões de pipeline, exemplos de Canary CRD e AnalysisTemplate, integrações…