Um workload que referencia uma imagem por tag aceita o que o registro tiver no momento do pull. A tag v1.2 pode ser reescrita e apontar para bytes diferentes daqueles que passaram pelo pipeline de build, e o cluster não tem como perceber a troca. Equipes com Kubernetes multi-tenant, registros compartilhados entre times ou dependências de imagens de terceiros são as mais expostas a essa substituição silenciosa, técnica recorrente em ataques de cadeia de suprimentos. A ação prioritária é mover a decisão para dentro do cluster: aceitar apenas imagens assinadas por uma autoridade conhecida e fixar a referência por digest antes de o pod existir. Este guia mostra como montar essa barreira de verificação de imagens no Kubernetes com Kyverno e Cosign.

O risco da tag mutável

Uma tag é um ponteiro mutável dentro do registro. Quando o kubelet recebe app:latest, ele resolve aquele ponteiro na hora e baixa o digest correspondente — que pode ter mudado desde o build, desde a análise de vulnerabilidades e desde a aprovação do change. O relatório que você arquivou sobre a imagem vale para um digest, não para a tag. Se alguém com acesso de escrita ao registro, ou um registro comprometido, mover a tag, seu nó executa código que nunca passou por controle algum.

O mesmo vale para imagens públicas: a confiança está no nome do repositório, não em uma prova criptográfica de quem produziu aquele artefato. A correção estrutural tem duas partes — assinar a imagem no fim do pipeline e verificar a assinatura na admissão do cluster, no único ponto por onde todo pod precisa passar.

Do lado da detecção, os sinais de que o problema existe no seu ambiente aparecem rápido: inventário de pods usando imagePullPolicy padrão com tags sem digest, pipelines que geram tag mutável a cada build e ausência de registro de assinatura no artefato publicado. Nenhum desses sinais gera alerta por conta própria — por isso a barreira precisa ser preventiva, na admissão, e não reativa.

Como a verificação funciona

No Kubernetes, um controlador de admissão intercepta requisições depois da autenticação e antes da persistência do objeto — e se qualquer controlador rejeitar o pedido em qualquer fase, a requisição inteira falha e o usuário recebe o erro. É esse comportamento fail-closed que torna a admissão o lugar certo para política de imagem: nada é agendado sem passar pela checagem.

Há dois caminhos para implementar a checagem. Regras declarativas nativas, como as políticas de admissão sem webhook que já cobrimos, resolvem restrições de configuração. Para verificação criptográfica de assinatura, o padrão é um webhook de admissão como o do Kyverno: a regra verifyImages consulta as assinaturas armazenadas junto da imagem no registro e aceita Cosign e Notary como formatos. Com mutateDigest ativo — o padrão — a regra converte a tag em digest imutável para impedir spoofing: mesmo que a tag seja movida depois, o pod já referencia o digest aprovado. Os campos required e verifyDigest, também ativos por padrão, garantem que toda imagem do recurso foi verificada e está fixada por digest.

Assinando com Cosign

No lado do pipeline, o Cosign assina a imagem e grava a assinatura como artefato no próprio registro, seguindo a especificação de referrers da OCI. O modo keyless dispensa gestão de chaves: você assina com chaves efêmeras autenticando por OIDC, com provedores como Google, GitHub ou Microsoft, e a identidade do signatário fica registrada de forma auditável. Para quem prefere controle direto, o cosign sign aceita par de chaves local, KMS (AWS, Azure, Google, HashiCorp Vault, OpenBao) e até chave guardada em secret do Kubernetes. O comando cosign attest complementa o fluxo: anexa metadados assinados — SBOM, resultado de scan, aprovação de change — que a política de admissão pode exigir antes de liberar o deploy.

Antes de chegar à admissão, valide o fluxo manualmente: cosign verify confere a assinatura e cosign verify-attestation com a flag --policy aplica uma política sobre o conteúdo do atestado — o mesmo par de operações que a regra do cluster executa de forma automática em cada pod.

Política mínima de exemplo

Exemplo próprio, sanitizado, de uma ClusterPolicy que exige assinatura da chave do time de plataforma em todo pod de produção:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-imagem-producao
spec:
  webhookConfiguration:
    failurePolicy: Fail
  rules:
    - name: verifica-assinatura
      match:
        any:
          - resources:
              kinds: [Pod]
      verifyImages:
        - imageReferences:
            - "registry.exemplo.com/producao/*"
          required: true
          mutateDigest: true
          verifyDigest: true
          attestors:
            - count: 1
              entries:
                - keys:
                    publicKeys: |-
                      -----BEGIN PUBLIC KEY-----
                      (chave pública do pipeline)
                      -----END PUBLIC KEY-----

Três detalhes importam mais que o YAML em si. Primeiro, failurePolicy: Fail: se o webhook cair, o cluster rejeita novos pods em vez de liberar tudo — sem isso, a barreira vira opcional justamente quando algo está errado. Segundo, attestors.count: define quantas autoridades precisam assinar; sem o campo, todas as entradas da lista são exigidas. Terceiro, imageReferences aceita apenas strings estáticas — o campo não suporta interpolação de variáveis, então liste explicitamente os prefixos de registro permitidos.

Cache, limites e armadilhas

Verificação de imagem custa chamadas de rede ao registro, então o Kyverno mantém um cache de resultados bem-sucedidos, habilitado por padrão com TTL de 60m e limite de 1000 chaves. O custo disso precisa estar claro para a equipe: qualquer mudança na imagem ou na assinatura no registro não é refletida até a entrada do cache expirar. Em um incidente de revogação — a chave vazou e as assinaturas foram reemitidas — esse intervalo entra no seu tempo real de contenção; ajuste o TTL conforme a tolerância do ambiente e reinicie o controlador quando precisar zerar o cache.

Outros limites práticos: pull de registros privados exige credenciais configuradas por política ou via imagePullSecrets do próprio pod, que o Kyverno passa a usar automaticamente a partir da versão 1.18, junto com o RBAC de leitura de secrets correspondente; e a verificação roda na admissão, portanto não substitui política de runtime — pods já em execução antes da regra continuam de pé até serem recriados.

Prioridades de adoção

A tabela resume a ordem recomendada de implantação, do diagnóstico ao bloqueio total:

Prioridade Ação Critério de pronto
P1 Inventariar imagens em execução por namespace e origem Lista de registros e proporção de tags vs. digests
P1 Assinar no pipeline e aplicar política em modo auditoria Deploys novos com assinatura registrada
P2 Ativar enforce com failurePolicy Fail Deploy sem assinatura rejeitado na admissão em teste
P2 Exigir atestados (SBOM, scan) nas cargas críticas Regra com attestations.conditions ativa em produção

Comece em modo auditoria e com exceções documentadas por prefixo: barreiras de admissão ativadas sem inventário costumam gerar mais exceção do que proteção. E trate o problema como camada, não como solução única — a fixação por digest reduz o raio de dano de um registro comprometido pelo mesmo motivo que backups em conta separada reduzem o de um ransomware: cada dependência precisa ser reestabelecida por uma decisão explícita sua, não por herança do estado anterior.

Fontes