Identificar exatamente o que se promove
Uma equipa fictícia prepara uma release do reconciliador de fundos. Desenvolvimento entrega uma imagem, segurança entrega resultados de análise e APS recebe o pedido de instalação. A primeira tarefa de coordenação é estabelecer se todos falam do mesmo conteúdo. Uma tag, o número do ticket e o nome da aplicação ajudam a organizar trabalho, mas não identificam necessariamente bytes imutáveis. Regista o digest aprovado, a plataforma, os manifests renderizados e as versões das ferramentas que produziram cada evidência. Desenha o fluxo desde o código autorizado até ao workload que vai arrancar: origem, build, publicação do artefacto, inventário, verificação, decisão e consumo. Em cada transição, pergunta qual identificador é preservado e qual entidade pode alterá-lo. Se a verificação resolve uma tag às 10:00 e o deploy a resolve novamente às 10:02, outro job pode ter mudado o destino. Promove a referência aprovada por digest e confirma a configuração consumida. Um índice multi-arquitetura e o manifest escolhido para ARM64 são objetos relacionados, mas não são o mesmo objeto. A evidência deve indicar qual foi analisado. Este cuidado permite investigar divergências sem assumir que a palavra release representa sempre o mesmo conteúdo.
Reduzir componentes e preservar funcionamento
A imagem final precisa dos componentes necessários à execução. Um estágio de compilação pode incluir compilador, testes e ferramentas de desenvolvimento sem que tudo isso tenha de seguir para produção. Numa build com vários estágios, examina os caminhos copiados para o estágio final e gera o inventário desse resultado. O SBOM do compilador pode ser útil para investigar a build, mas não deve ser apresentado como prova do que foi entregue ao runtime. A redução exige testes concretos de compatibilidade. Se um cliente passa a falhar HTTPS com unknown authority, investiga certificados de confiança e a cadeia apresentada; não desligues a validação TLS para compensar uma base incompleta. Considera também bibliotecas dinâmicas, utilizadores, permissões, fuso horário e caminhos de escrita exigidos pela aplicação. Explica que necessidades são reais e como as observaste. Uma base pequena não garante ausência de vulnerabilidades. Uma referência fixada por digest melhora a repetibilidade, mas precisa de um processo de atualização. Desativar cache com --no-cache não equivale a obter automaticamente a base mais recente. Se a build usou ARG para uma credencial, remover a instrução atual não revoga o token nem elimina imagens anteriores. Investiga exposição e corrige o método de entrega do segredo.
Ler um SBOM dentro do seu âmbito
Um SBOM descreve componentes observados ou declarados segundo uma ferramenta, origem e configuração. Antes de comparar relatórios, confirma se foram analisados código fonte, diretório instalado, estágio de build ou imagem final. Os catalogadores padrão de Syft diferem entre diretórios e imagens: uma dependência declarada num ficheiro pode aparecer no repositório sem existir como pacote instalado no resultado. Examina a evidência antes de classificar a diferença como erro. Conserva plataforma, tipo de origem e catalogadores executados. Se selecionares apenas Python, a ausência de pacotes do sistema operativo não prova que desapareceram da imagem. Num exercício local, docker-archive:/caminho/imagem.tar expressa o arquivo que deve ser lido; uma referência curta pode ativar outros mecanismos de resolução. A prática desta aula gera Syft JSON e CycloneDX a partir de um arquivo local ARM64, sem consultar um registo de imagens. Inventário e análise de vulnerabilidades têm saídas diferentes. O laboratório não executa Grype nem descarrega uma base de advisories. A ausência de findings de vulnerabilidade não pode ser deduzida desse ensaio. Para uma decisão real, regista também versão e data dos dados de vulnerabilidade, correspondência do pacote, aplicabilidade e exceções. O digest pode permanecer igual enquanto o conhecimento sobre uma biblioteca muda.
Verificar bytes, chave e identidade
Uma assinatura válida liga bytes a material de verificação sob um modelo de confiança. Não concede automaticamente aprovação de mudança, ausência de vulnerabilidades ou conformidade com requisitos de negócio. Com chaves geridas pela organização, o consumidor precisa de conhecer a chave autorizada através de um canal confiável. Se um pacote traz uma chave nova que valida a própria assinatura, ainda falta autenticar a rotação antes de substituir a âncora aprovada. Na verificação keyless, a identidade esperada e o emissor OIDC fazem parte da decisão. Aceitar todos os workflows de uma organização pode ser mais amplo do que autorizar apenas um workflow de release. Testa correspondências negativas com outro repositório, workflow e referência. Esta aula explica esse modelo, mas o laboratório usa chaves locais descartáveis e não executa autenticação OIDC. O exercício assina um documento JSON que contém hashes de um arquivo e de um SBOM. verify-blob valida esse documento. Para saber se os ficheiros recebidos correspondem, o consumidor ainda calcula os seus hashes e compara-os com os valores autenticados. O hash do arquivo tar não é automaticamente o digest OCI de uma imagem ou de um índice. Conserva o nome e o tipo de cada objeto para evitar uma associação falsa durante o handover.
Comparar proveniência com expectativas
Proveniência útil permite investigar a origem e o processo de construção de um artefacto. A verificação precisa de expectativas definidas pelo consumidor: identidade do builder, repositório autorizado, tipo de build, parâmetros aceites e ligação ao conteúdo entregue. Uma assinatura autentica uma declaração, incluindo uma declaração que descreve um processo não autorizado. Por isso, verificar a assinatura e avaliar a política são etapas distintas. No modelo SLSA, o nível de confiança não deve ser aumentado apenas porque o produtor declarou um nível elevado. O verificador usa a configuração de confiança estabelecida para a identidade e o builder. Parâmetros externos desconhecidos podem alterar o resultado; precisam de avaliação antes de serem aceites. Mantém o formato e a versão da proveniência explícitos para interpretar corretamente os campos. O laboratório cria um formato JSON didático próprio. Não é uma implementação completa de proveniência SLSA, não usa uma attestation DSSE e não demonstra um nível SLSA. Mostra uma fronteira concreta: a mesma chave de laboratório assina um documento com outro builder ou commit; a assinatura passa, mas a comparação com expectativas locais recusa o documento. A decisão é reproduzível porque o conjunto aprovado está definido antes de receber o candidato. Em produção, proteger e governar essa configuração é parte essencial do controlo.
Fazer a análise estática influenciar a decisão
Analisa os manifests efetivamente renderizados para o ambiente. Um chart sem findings com valores de desenvolvimento pode introduzir um sidecar privilegiado através de valores de produção. Guarda o template resultante, a configuração de checks e a versão do linter. Uma execução que não encontrou objetos não demonstra cobertura; uma execução que encontrou problemas mas termina com || true também pode deixar a pipeline verde sem cumprir o gate previsto. A prática seleciona três checks de KubeLinter: privileged-container, run-as-non-root e no-read-only-root-fs. Um Deployment sintético falha os três, outro corrige os campos relevantes, e um ficheiro só com comentário é recusado por ausência de objetos. O segundo resultado prova apenas que aqueles checks passaram naquele manifest. Não demonstra arranque, autorização, políticas de rede, assinatura de imagem nem conformidade integral com Pod Security. Para uma necessidade excecional, evita retirar uma regra globalmente sem avaliar o alcance. Associa a dispensa ao recurso, justificação, responsável, prazo e controlos compensatórios. A ferramenta permite configurar checks e exceções; a aprovação de risco pertence ao processo da organização. Se existir um controlador de políticas de imagem, verifica também namespaces abrangidos e correspondência de políticas. Instalar o componente não prova que todos os pedidos atravessam o controlo.
Executar e interpretar a prática local
Pré-requisitos: Python 3, um arquivo de imagem previamente exportado para linux/arm64 e executáveis Syft 1.54.1, Cosign 3.1.3 e KubeLinter 0.8.3. Guarda o código apresentado como run.py. Executa python3 run.py --image-archive /caminho/imagem.tar --output /caminho/resultado-novo.json; usa --syft, --cosign e --kube-linter para indicar executáveis fora do PATH. O script recusa sobrescrever um resultado existente, lê o arquivo e usa uma pasta temporária para os seus próprios ficheiros. Nos dois ensaios finais, o arquivo local produziu 37 componentes e um documento CycloneDX. Este número pertence à imagem analisada, não é um resultado obrigatório para todas as imagens. Observa os 14 registos: inventários, assinatura original, documento alterado, chave diferente, ligação aos hashes, SBOM alterado, digest candidato diferente, builder e commit não aprovados, manifests e entrada vazia. O digest candidato diferente é calculado acrescentando um marcador aos bytes no cálculo; o arquivo de entrada não é alterado. As chaves são novas, descartáveis e protegidas por uma palavra-passe gerada durante a execução. A configuração omite serviços públicos. --insecure-ignore-tlog é usado apenas nesta verificação de blobs locais sem publicação: não demonstra inclusão no Rekor e não deve ser copiado para um gate que exija transparência. O ensaio não instala workloads. No fim, confirma remoção dos ficheiros temporários e integridade do arquivo original.
Decidir a release e entregar a operação
No caso final, a assinatura de release.json passa, mas o SBOM recebido não corresponde ao hash autenticado. Faltam 25 minutos para a decisão de uma release de reconciliação. A equipa deve preservar os ficheiros, identificar a origem da divergência e pedir um pacote coerente. Alterar manualmente o hash no documento não recupera a assinatura original nem prova aprovação. Se a ligação não ficar demonstrada dentro do prazo, aplica a contingência aprovada e comunica o impacto de negócio. Prepara para RUN uma relação entre digest, plataforma, origem autorizada, inventário, análises, versões, exceções e decisão. Inclui quem mantém cada controlo e como obter os artefactos durante um incidente. Um runbook de rollback que referencia conteúdo apagado pelo registo não é executável: combina retenção com a janela de recuperação e testa acesso aos bytes e às evidências necessárias. Rever risco também é trabalho de operação, porque advisories e prazos de exceção mudam depois da publicação. Resumo: identifica conteúdo, observa componentes, autentica evidências, compara expectativas e confirma que os gates afetam a promoção. Cada conclusão deve indicar o que foi observado e o que continua por testar. Liga esta aula a identidades, admissão de workloads, gestão de mudança e resposta a incidentes. Os exemplos bancários são originais e fictícios, sem representar procedimentos internos da BNP Paribas.
#!/usr/bin/env python3
"""Local CKS exercise: actual inventory, blob signatures and static manifests.
Use an existing exported linux/arm64 Docker image archive. No registry writes,
OIDC login, production credentials, Kubernetes admission or SLSA level claim.
Temporary keys and fixtures are removed even when a check fails.
"""
import argparse, copy, datetime, hashlib, json, os, pathlib, secrets, subprocess, tempfile
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--image-archive', required=True)
p.add_argument('--syft', default='syft')
p.add_argument('--cosign', default='cosign')
p.add_argument('--kube-linter', default='kube-linter')
p.add_argument('--output', required=True)
a = p.parse_args()
archive = pathlib.Path(a.image_archive).resolve(strict=True)
out = pathlib.Path(a.output).resolve()
if out.exists() or out == archive:
raise SystemExit('Choose a new output path; input is read only.')
def sha(path):
h = hashlib.sha256()
with open(path, 'rb') as f:
for part in iter(lambda: f.read(1024 * 1024), b''):
h.update(part)
return h.hexdigest()
observations = []
def observe(name, passed, **details):
observations.append(dict(name=name, passed=bool(passed), observed=details))
if not passed:
raise AssertionError(name + ': ' + str(details))
report = dict(startedAt=datetime.datetime.now(datetime.timezone.utc).isoformat(),
scriptSha256=sha(__file__), observations=observations,
scope='Local image inventory, disposable-key blob signatures, explicit binding policy and selected static checks. No OCI image signature, vulnerability scan, keyless identity, transparency-log proof, live admission or SLSA certification.',
inputArchiveSha256=sha(archive), platform='linux/arm64',
fullPracticalMock=False, independentVerification=False)
try:
with tempfile.TemporaryDirectory(prefix='dr-cks-supply-') as temporary:
root = pathlib.Path(temporary)
# A new passphrase for new disposable keys; never printed or persisted.
env = dict(os.environ, COSIGN_PASSWORD=secrets.token_urlsafe(24),
COSIGN_YES='true', SYFT_CHECK_FOR_APP_UPDATE='false')
def run(tool, *args, ok=True):
result = subprocess.run([tool, *map(str, args)], cwd=root, env=env,
text=True, capture_output=True, timeout=120)
if ok and result.returncode:
raise RuntimeError(f'{pathlib.Path(tool).name} {args[0]}: {result.stderr[-1800:]}')
return result
report['versions'] = dict(syft=run(a.syft, 'version', '-o', 'json').stdout,
cosign=run(a.cosign, 'version', '--json').stdout,
kubeLinter=run(a.kube_linter, 'version').stdout.strip())
# Explicit local source and platform, no fallback to a remote registry.
run(a.syft, 'scan', 'docker-archive:' + str(archive), '--platform', 'linux/arm64',
'-o', 'syft-json=sbom.json', '-o', 'cyclonedx-json=sbom.cdx.json')
sbom = json.loads((root / 'sbom.json').read_text())
packages = sbom['artifacts']
observe('image-inventory-generated', len(packages) > 0,
packages=len(packages), sample=sorted({x['name'] for x in packages})[:8],
sourceType=sbom['source']['type'], tool=sbom['descriptor']['name'],
source=sbom['source'], catalogers=sbom['descriptor']['configuration'].get('catalogers'))
cdx = json.loads((root / 'sbom.cdx.json').read_text())
observe('cyclonedx-generated', cdx['bomFormat'] == 'CycloneDX' and len(cdx.get('components', [])) > 0,
specVersion=cdx['specVersion'], components=len(cdx.get('components', [])))
for name in ['trusted-lab', 'other-lab']:
run(a.cosign, 'generate-key-pair', '--output-key-prefix', name)
run(a.cosign, 'signing-config', 'create', '--no-default-rekor', '--no-default-fulcio',
'--no-default-oidc', '--no-default-tsa', '--out', 'local-signing.json')
# These deliberately local signatures provide NO public transparency evidence.
# The ignore flag is confined to verify-blob with a newly generated lab key.
def sign(name):
run(a.cosign, 'sign-blob', '--key', 'trusted-lab.key', '--signing-config',
'local-signing.json', '--bundle', name + '.bundle', name)
def verify(name, key='trusted-lab.pub', bundle=None):
return run(a.cosign, 'verify-blob', '--key', key, '--bundle', bundle or name + '.bundle',
'--insecure-ignore-tlog', name, ok=False)
approved = dict(builder='https://ci.example.invalid/funds/release-v2',
repository='https://git.example.invalid/funds/reconciler',
sourceCommit='0123456789abcdef0123456789abcdef01234567',
buildType='dr.pt/local-training-evidence/v1')
evidence = dict(**approved, archiveSha256=sha(archive), sbomSha256=sha(root / 'sbom.json'))
def save(name, value):
(root / name).write_text(json.dumps(value, sort_keys=True) + '\n')
save('release.json', evidence)
sign('release.json')
result = verify('release.json')
observe('original-blob-signature-valid', result.returncode == 0, exitCode=result.returncode)
(root / 'edited.json').write_text((root / 'release.json').read_text() + ' ')
result = verify('edited.json', bundle='release.json.bundle')
observe('edited-blob-signature-denied', result.returncode != 0, exitCode=result.returncode)
result = verify('release.json', key='other-lab.pub')
observe('different-key-denied', result.returncode != 0, exitCode=result.returncode)
def policy(record, archive_hash, sbom_hash):
return (all(record.get(k) == v for k, v in approved.items())
and record.get('archiveSha256') == archive_hash
and record.get('sbomSha256') == sbom_hash)
observe('original-bindings-accepted', policy(evidence, sha(archive), sha(root / 'sbom.json')),
archiveSha256=evidence['archiveSha256'], sbomSha256=evidence['sbomSha256'])
# Editing a separate unsigned file does not change a signature on release.json.
(root / 'changed-sbom.json').write_bytes((root / 'sbom.json').read_bytes() + b'\n')
observe('changed-sbom-binding-denied',
verify('release.json').returncode == 0 and not policy(evidence, sha(archive), sha(root / 'changed-sbom.json')),
signedRecordStillValid=True, separateSBOMBindingAccepted=False)
changed = hashlib.sha256()
with archive.open('rb') as f:
for part in iter(lambda: f.read(1024 * 1024), b''):
changed.update(part)
changed.update(b'local-tampering-marker')
observe('different-archive-digest-denied', not policy(evidence, changed.hexdigest(), sha(root / 'sbom.json')),
candidateDigest=changed.hexdigest(), accepted=False)
other = dict(evidence, builder='https://ci.example.invalid/unapproved/runner')
save('other-builder.json', other)
sign('other-builder.json')
observe('other-builder-signature-valid', verify('other-builder.json').returncode == 0,
note='The approved lab key can sign a claim that policy does not approve.')
observe('other-builder-policy-denied', not policy(other, sha(archive), sha(root / 'sbom.json')),
accepted=False)
other = dict(evidence, sourceCommit='f' * 40)
save('other-commit.json', other)
sign('other-commit.json')
observe('other-commit-policy-denied', verify('other-commit.json').returncode == 0 and not policy(other, sha(archive), sha(root / 'sbom.json')),
signedRecordValid=True, sourceCommitAccepted=False)
# Original local fixtures. These are parsed, never submitted to a cluster.
bad = dict(apiVersion='apps/v1', kind='Deployment', metadata=dict(name='reconcile', namespace='training'),
spec=dict(replicas=1, selector=dict(matchLabels=dict(app='reconcile')),
template=dict(metadata=dict(labels=dict(app='reconcile')),
spec=dict(containers=[dict(name='worker', image='example.invalid/reconcile:training',
securityContext=dict(privileged=True))]))))
good = copy.deepcopy(bad)
good['spec']['template']['spec']['containers'][0]['securityContext'] = dict(
privileged=False, runAsNonRoot=True, runAsUser=10001, readOnlyRootFilesystem=True)
for name, value in [('bad.json', bad), ('good.json', good)]:
save(name, value)
checks = 'privileged-container,run-as-non-root,no-read-only-root-fs'
def lint(name):
return run(a.kube_linter, 'lint', name, '--do-not-auto-add-defaults',
'--include', checks, '--fail-if-no-objects-found', '--format', 'json', ok=False)
result = lint('bad.json')
findings = json.loads(result.stdout)
names = sorted({x['Check'] for x in findings.get('Reports', [])})
observe('insecure-manifest-findings', result.returncode != 0 and names == sorted(checks.split(',')),
exitCode=result.returncode, checks=names)
result = lint('good.json')
observe('corrected-selected-checks-pass', result.returncode == 0, exitCode=result.returncode,
selectedChecks=checks.split(','), scope='Only these three checks; no runtime or admission evidence.')
(root / 'empty.yaml').write_text('# No Kubernetes objects\n')
result = lint('empty.yaml')
observe('empty-input-denied', result.returncode != 0, exitCode=result.returncode)
report['cleanup'] = dict(realCredentialsUsed=False, temporaryKeysCreated=2)
report['cleanup']['temporaryMaterialRemoved'] = not root.exists()
report['inputUnchanged'] = sha(archive) == report['inputArchiveSha256']
report['passed'] = all(x['passed'] for x in observations) and report['inputUnchanged']
finally:
report['finishedAt'] = datetime.datetime.now(datetime.timezone.utc).isoformat()
out.write_text(json.dumps(report, indent=2) + '\n')
print(json.dumps(dict(passed=report['passed'], observations=len(observations), output=str(out))))
A assinatura do documento passa, mas o SBOM entregue não corresponde ao hash autenticado; o prazo de decisão exige correção demonstrada ou contingência.
Armadilhas comuns
Confundir inventário com análise de vulnerabilidades, assinatura com aprovação, hash de arquivo com digest OCI ou zero findings com análise completa.
Tópicos relacionados: Identidade e confiança · Admissão de workloads · Gestão de mudança e recuperação
A decisão de release precisa de conteúdo identificado, evidências ligadas e expectativas avaliadas, com âmbito e versões explícitos.
Referência: CKS certification and domains · Kubernetes v1.35; current six-domain CKS outline