← CKS: segurança Kubernetes em produção
22 / 23 · 120 MIN

Recuperar confiança na cadeia de fornecimento

Distingue integridade, proveniência e aceitação ao recuperar imagens, builds e confiança dos consumidores.

Definir o que precisa de recuperar confiança

Uma recuperação da cadeia de fornecimento começa por identificar onde a confiança deixou de estar demonstrada. Pode ser a chave signatária, o runner, uma action externa, a imagem base ou a configuração do controlo de promoção. Cada causa exige evidência diferente. Se o runner foi comprometido, assinar novamente os seus resultados não demonstra que os bytes são seguros. Se uma chave foi exposta, reconstruir num ambiente limpo não retira automaticamente a confiança dos verificadores antigos. Num caso bancário fictício, a equipa APS recebe uma candidata de reconciliação trinta minutos antes do fecho. A pipeline está verde, mas houve execução maliciosa num runner persistente. O responsável técnico reúne desenvolvimento, segurança e operação para delimitar a janela afetada, identificar os digests produzidos, os consumidores e as credenciais acessíveis. O objetivo é decidir que artefactos precisam de quarentena ou reconstrução e que fluxos de negócio precisam de contingência. Este caso não descreve procedimentos internos da BNP Paribas. Prepara um registo com digest, origem do build, assinatura, proveniência, política aplicada e decisão por consumidor. Separa factos observados de hipóteses e de ações pendentes. Uma tag como candidate ajuda a localizar uma release, mas não identifica permanentemente os seus bytes. Guarda a resolução por digest no momento da decisão. Define checkpoints com o negócio: hora limite, dados já processados, consequências do adiamento e autoridade para aceitar uma exceção delimitada. A urgência altera prioridades, mas não transforma evidência em falta numa aprovação.

Reconstruir a partir de entradas de confiança

O manifesto OCI referencia uma configuração e uma sequência de camadas. O filesystem que o container vê resulta da aplicação dessas camadas; não é uma lista de todos os bytes que continuam distribuídos nos blobs. Uma whiteout pode ocultar um ficheiro de uma camada anterior sem o apagar dessa camada. Por isso, copiar uma credencial e eliminá-la numa instrução posterior não é um método de proteção de segredos. Na recuperação, roda a credencial exposta, avalia onde a imagem circulou e reconstrói sem incorporar esse material. Uma base atualizada pelo fornecedor também não modifica o digest de uma aplicação já construída. É preciso produzir um novo artefacto que incorpore as alterações, avaliar o resultado e confirmar que os consumidores o adotaram. Regista o digest de entrada e o de saída. Se a redução da imagem removeu certificados de confiança ou bibliotecas necessárias, um processo que arranca não demonstra funcionamento do fluxo de negócio. Testa as dependências realmente utilizadas, mantendo a validação TLS e os restantes requisitos. A recuperação do ambiente de build merece uma decisão própria. Num runner self-hosted persistente, corrigir o workflow não prova que processos, ficheiros ou credenciais deixaram de estar comprometidos. Um registo JIT de um único job também não limpa automaticamente uma máquina reutilizada. Usa um ambiente cuja confiança tenha sido restabelecida e revê as credenciais acessíveis. Ao fixar uma action por SHA completo, confirma o commit no repositório original e revê código e permissões. Imutabilidade da referência ajuda a repetir uma escolha; não demonstra que a escolha inicial era segura.

Distinguir assinatura válida de aceitação

Uma assinatura liga um payload a uma chave. No ensaio, o Cosign verifica a assinatura da imagem e a sua ligação ao digest. Quando candidate passa da imagem anterior para a imagem de recuperação ainda sem assinatura, a verificação da tag deixa de passar, enquanto o digest anterior continua verificável. Não houve transferência da assinatura para os novos bytes. Este resultado explica por que motivo a decisão de promoção deve guardar a identidade resolvida, em vez de depender de um nome móvel. A prática cria duas chaves descartáveis. Depois de assinar a recuperação com a chave antiga, a verificação com essa chave passa e a verificação apenas com a chave nova falha. Depois de acrescentar uma assinatura nova, a política que usa a nova chave passa. A assinatura antiga não deixou de ser criptograficamente válida. O ensaio não implementa um serviço de revogação; observa a seleção explícita de confiança pelo verificador. Em produção, retirar confiança exige inventariar consumidores e confirmar a política efetivamente utilizada. Mudar a chave da pipeline não atualiza, por si só, os ambientes que verificam as imagens. Apagar uma cópia privada também não elimina cópias já expostas. Define os critérios de aceitação e recusa, testa os dois sentidos e regista onde ainda existe adoção pendente. Uma annotation de release acrescenta uma expectativa sobre o payload assinado: a chave correta pode passar enquanto o valor de release exigido falha. Nenhuma destas verificações demonstra, isoladamente, ausência de vulnerabilidades ou legitimidade de um build comprometido.

Verificar proveniência antes de aplicar expectativas

A assinatura de uma imagem e uma attestation de proveniência não são equivalentes. O laboratório confirma que uma imagem assinada pode continuar sem a declaração exigida por verify-attestation. A política deve tratar essa ausência como condição não satisfeita. Obtém a evidência, verifica a assinatura com a confiança aprovada e só depois interpreta o payload autenticado. Um JSON obtido sem essa ligação não deve ser apresentado como declaração validada do signatário. O Cosign 3.1.3 utilizado neste ensaio emitiu um Statement in-toto v0.1 com predicateType SLSA provenance v1. O envelope DSSE declara application/vnd.in-toto+json. Estas camadas têm identificadores próprios. O laboratório reconhece explicitamente o Statement observado e não reescreve o seu _type para aparentar uma versão diferente. A referência SLSA consultada é a especificação v1.2; o URI do tipo de proveniência continua a terminar em v1. Regista os identificadores reais em vez de deduzir equivalência numérica. A política local compara subject.digest, predicateType, buildType, builder.id e o conjunto exato de externalParameters aprovado. Uma declaração assinada com builder não autorizado falha; outra com debug=true adicional também falha; a combinação aprovada passa. A mesma declaração é recusada para um digest diferente. Este exercício demonstra decisões sobre declarações sintéticas. Não executa uma plataforma CI isolada, não prova que o builder declarado produziu os bytes e não estabelece um nível SLSA. Na aplicação real, a confiança na plataforma e na emissão da proveniência tem de sustentar a interpretação desses campos.

Rever o que os relatórios de análise omitiram

Um relatório sem resultados pode representar ausência de problemas no âmbito analisado ou ausência de análise relevante. Durante uma recuperação, confirma a imagem avaliada, a configuração, as entradas e os filtros. Na documentação Grype consultada, only-fixed exclui resultados com estados como not-fixed, wont-fix e unknown. O limiar fail-on é avaliado depois da filtragem. Logo, sucesso com esse filtro não demonstra ausência de vulnerabilidades sem correção. Consulta também ignoredMatches e appliedIgnoreRules para compreender o que não participou na decisão. Uma declaração VEX precisa de corresponder ao produto e à versão avaliados. Under_investigation não significa not_affected. Não uses incerteza como demonstração de ausência de impacto, nem transfiras automaticamente uma justificação ligada à imagem anterior para uma tag que agora resolve para outro digest. Documenta a base da decisão, o responsável e o momento de reavaliação. Estes cenários são fundamentados na documentação; este novo laboratório não executa Grype nem um scan de vulnerabilidades. Para KubeLinter, confirma a configuração efetiva quando o job muda de diretório. Uma inclusão explícita não anula um checks.exclude correspondente. Revê ignorePaths e annotations, sobretudo no Job de recuperação e no manifesto de rollback, que podem ter ficado fora da análise principal. Uma annotation ignore-all antiga não conserva validade por aparecer num commit antes aprovado. A exceção precisa de âmbito e justificação atuais. Prepara uma entrada que deveria falhar para observar se o controlo continua ativo. Um resultado positivo do Deployment principal não substitui a avaliação dos restantes workloads necessários à contingência.

Executar o laboratório OCI local

A prática utiliza Docker com contexto local por socket Unix e Cosign 3.1.3. Usa as imagens previamente obtidas python@sha256:2d9aefe2fef018a7eb2c13064c89c71929800fd2e5dccdbf52ea5da5bb8d929a e registry@sha256:ddf754342cfc8acc51a56d5d0ab6af06826461864460636d8bd5c546dab2a7b8, correspondente a Distribution 3.1.2. O script não as descarrega: se faltarem, obtém as referências de confiança antes da execução. Passa o caminho do binário com --cosign e um caminho de relatório com --output. O código apresentado é o mesmo das duas execuções finais, identificado por SHA-256 nos relatórios. O script cria um registry temporário publicado apenas em 127.0.0.1, sem credenciais reais. Cria duas imagens sintéticas com marcadores diferentes e confirma a leitura desses marcadores em containers sem rede. Exporta cada imagem e envia configuração, camadas e manifesto através da API HTTP local. Verifica tamanhos e digests dos blobs e a identidade do manifesto devolvido. Este caminho evita confundir o loopback do host com o do daemon Docker Desktop. A montagem das imagens com commit é um fixture didático, não uma pipeline de build segura ou reprodutível. Depois executa os casos de assinatura, mudança de tag, seleção de confiança e proveniência. Cada execução final registou 29 observações aprovadas. O relatório contém versões, digests e declarações sintéticas verificadas; não contém chaves privadas. O bloco de limpeza remove containers e imagens próprios, diretório temporário e material privado. As imagens base em cache permanecem. O script usa configuração Docker temporária vazia e não utiliza credenciais dos registries do utilizador. Lê os limites da prática antes de reutilizar qualquer comando noutro ambiente.

Interpretar os resultados negativos e os limites

Antes de executar, prevê quatro resultados: uma imagem sem assinatura deve ser recusada; uma assinatura pela chave antiga deve falhar quando só a nova é aceite; uma attestation com builder não aprovado deve falhar na política; e uma declaração válida para outro digest deve ser recusada. Depois compara previsão e observação. Uma falha esperada não é um defeito do ensaio: demonstra uma fronteira que a recuperação precisa de conservar. Guarda também os sucessos necessários para provar que o caminho permitido continua utilizável. Os relatórios finais mostram que a tag mudou sem transferir assinatura, que a chave antiga continuou a produzir uma assinatura criptograficamente válida e que uma política mais restrita recusou declarações assinadas. O programa descodifica apenas a saída de uma verificação de attestation bem-sucedida, confirma o payloadType DSSE e aplica as expectativas ao Statement observado. A aceitação local de pelo menos uma declaração aprovada não demonstra que todas as declarações disponíveis descrevem builds legítimos; define conscientemente a política pretendida quando há múltiplas attestations. HTTP e --insecure-ignore-tlog são opções delimitadas a esta prática em loopback. Não há prova de transparência pública, OIDC, KMS, admission control, scan de vulnerabilidades ou recuperação real de uma plataforma CI. O registry de produção precisa dos seus próprios controlos de transporte, acesso, retenção e disponibilidade. As execuções iniciais do desenvolvimento encontraram problemas de permissões do marcador, loopback do Docker Desktop e formato do Statement; foram corrigidos antes das duas execuções finais com código idêntico. Preservar estes limites permite usar os resultados sem os transformar numa afirmação de cobertura que não foi demonstrada.

Entregar uma decisão de recuperação verificável

No handover do caso bancário, entrega uma matriz pequena: artefacto candidato, digest avaliado, origem aceite, confiança exigida, proveniência disponível, análise de segurança, consumidores testados e decisão. Para cada ambiente, indica qual o verificador e a política que realmente participaram no teste. Regista resultados positivos e negativos, o responsável pela adoção restante e o momento em que uma exceção expira. Se a evidência não chega antes da hora limite, a decisão pode ser adiar a release e usar uma contingência avaliada, com impacto comunicado e dados preservados para reconciliação. Não declares a recuperação terminada apenas porque a pipeline assina com a chave nova. Confirma que os consumidores aceitam o artefacto aprovado e recusam o que a política deixou de admitir. Verifica que as assinaturas e attestations continuam acessíveis depois de copiar a imagem para o registry de destino. Uma consulta bem-sucedida na origem não observa o acesso no destino. Reavalia o rollback: um digest historicamente assinado pode continuar identificável e, ainda assim, deixar de cumprir a análise de risco atual. Como exercício final, preenche essa matriz para uma candidata criada no runner comprometido, outra reconstruída num ambiente aceite e um rollback com exceção antiga de análise. Explica quais as decisões que consegues tomar e que evidência pedirias para as restantes. Relaciona o resultado com gestão de incidentes, mudança, menor privilégio e observabilidade. A conclusão desta aula é operacional: recuperar confiança envolve origem, bytes, afirmações verificadas, política e adoção. As 29 verificações locais apoiam partes desse raciocínio; simulados práticos completos e revisão independente do curso continuam pendentes.

"""Original local OCI-signature and recovery-policy lab; no production registry or keys.
Requires cached Docker images. Uses a loopback-only disposable registry and
synthetic image variants created by docker commit, not a production build chain.
Public transparency and OIDC are deliberately absent; no SLSA level is claimed.
"""
import argparse,base64,datetime,hashlib,json,os,secrets,shutil,subprocess,tempfile,time,urllib.request,urllib.parse,uuid,tarfile
from pathlib import Path
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--cosign',required=True);p.add_argument('--output',required=True);a=p.parse_args();out=Path(a.output).resolve();assert not out.exists()
BASE='python@sha256:2d9aefe2fef018a7eb2c13064c89c71929800fd2e5dccdbf52ea5da5bb8d929a'
REGISTRY='registry@sha256:ddf754342cfc8acc51a56d5d0ab6af06826461864460636d8bd5c546dab2a7b8'
os.umask(0o077);tmp=Path(tempfile.mkdtemp(prefix='dr-cks-oci-'));prefix='dr-cks-oci-'+uuid.uuid4().hex[:8];containers=[];images=[];tags=[];observations=[];report={};env=dict(os.environ)
def run(cmd,ok=True,timeout=90):
 r=subprocess.run(cmd,cwd=tmp,env=env,text=True,capture_output=True,timeout=timeout)
 if ok and r.returncode:raise RuntimeError('Command failed '+str(cmd[:3])+': '+r.stderr[-1200:])
 return r

def docker(*args,ok=True):return run(['docker',*map(str,args)],ok=ok)
def check(name,value,expected):
 assert value==expected,(name,value,expected);observations.append(dict(name=name,observed=value,passed=True));print(name,flush=True)
def cos(*args,ok=True):return run([a.cosign,*map(str,args)],ok=ok)
def request(path,method='GET',data=None,headers=None):
 req=urllib.request.Request('http://'+address+path,method=method,data=data,headers=headers or {})
 with urllib.request.urlopen(req,timeout=10) as res:return res.read(),dict(res.headers)
def manifest(tag):
 data,headers=request('/v2/funds/reconciler/manifests/'+tag,headers={'Accept':'application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.v2+json'})
 digest='sha256:'+hashlib.sha256(data).hexdigest();assert headers.get('Docker-Content-Digest')==digest
 return digest,data,headers['Content-Type']
def push_archive(local,tag):
 archive=tmp/'image.tar';docker('image','save','--platform','linux/arm64','--output',archive,local)
 def upload(data):
  digest='sha256:'+hashlib.sha256(data).hexdigest()
  _,headers=request('/v2/funds/reconciler/blobs/uploads/','POST',b'')
  location=urllib.parse.urlsplit(headers['Location']);assert location.netloc==address
  path=location.path+'?'+location.query+'&digest='+digest
  request(path,'PUT',data,{'Content-Type':'application/octet-stream'})
  return digest
 with tarfile.open(archive,'r') as tar:
  entry=json.load(tar.extractfile('manifest.json'))[0];config=tar.extractfile(entry['Config']).read();cd=upload(config);layers=[]
  for path in entry['Layers']:
   data=tar.extractfile(path).read();digest=upload(data)
   layers.append(dict(mediaType='application/vnd.oci.image.layer.v1.tar'+('+gzip' if data[:2]==bytes([31,139]) else ''),digest=digest,size=len(data)))
  value=dict(schemaVersion=2,mediaType='application/vnd.oci.image.manifest.v1+json',config=dict(mediaType='application/vnd.oci.image.config.v1+json',digest=cd,size=len(config)),layers=layers)
  body=json.dumps(value,separators=(',',':')).encode();request('/v2/funds/reconciler/manifests/'+tag,'PUT',body,{'Content-Type':value['mediaType']})
 archive.unlink()
def sign(ref,key,label):return cos('sign','--yes','--key',key+'.key','--signing-config','local-signing.json','--allow-http-registry','-a','release='+label,ref)
def verify(ref,key,label=None):
 args=['verify','--key',key+'.pub','--allow-http-registry','--insecure-ignore-tlog']
 if label:args+=['-a','release='+label]
 return cos(*args,ref,ok=False)
def attest(ref,predicate):
 (tmp/'predicate.json').write_text(json.dumps(predicate))
 return cos('attest','--yes','--key','recovery.key','--signing-config','local-signing.json','--allow-http-registry','--type','slsaprovenance1','--predicate','predicate.json',ref)
def verify_attestation(ref):return cos('verify-attestation','--key','recovery.pub','--allow-http-registry','--insecure-ignore-tlog','--type','slsaprovenance1',ref,ok=False)
def decode(result):
 assert result.returncode==0
 # Cosign emits one DSSE envelope per line for registry attestations.
 objects=[]
 try:
  value=json.loads(result.stdout);objects=value if isinstance(value,list) else [value]
 except json.JSONDecodeError:objects=[json.loads(line) for line in result.stdout.splitlines() if line.strip()]
 assert all(x.get('payloadType')=='application/vnd.in-toto+json' for x in objects)
 return [json.loads(base64.b64decode(x['payload'])) for x in objects]
builder='https://ci.example.invalid/funds/recovery-v1';repository='https://git.example.invalid/funds/reconciler';commit='0123456789abcdef0123456789abcdef01234567';build_type='https://dr.pt/training/synthetic-image-assembly/v1'
expected_params={'repository':repository,'commit':commit}
def policy(statement,digest):
 predicate=statement.get('predicate',{});definition=predicate.get('buildDefinition',{});details=predicate.get('runDetails',{})
 return (statement.get('_type')=='https://in-toto.io/Statement/v0.1' and statement.get('predicateType')=='https://slsa.dev/provenance/v1'
  and any(x.get('digest',{}).get('sha256')==digest.removeprefix('sha256:') for x in statement.get('subject',[]))
  and definition.get('buildType')==build_type and definition.get('externalParameters')==expected_params
  and details.get('builder',{}).get('id')==builder)
try:
 # Avoid reading any real registry credentials; retain only the local daemon endpoint.
 endpoint=docker('context','inspect','--format','{{(index .Endpoints "docker").Host}}').stdout.strip();assert endpoint.startswith('unix://')
 (tmp/'docker-config').mkdir();(tmp/'docker-config/config.json').write_text('{}');env.update(DOCKER_HOST=endpoint,DOCKER_CONFIG=str(tmp/'docker-config'),COSIGN_PASSWORD=secrets.token_urlsafe(24),COSIGN_YES='true');env.pop('DOCKER_CONTEXT',None)
 docker('image','inspect',BASE);docker('image','inspect',REGISTRY)
 report['cosign']=json.loads(cos('version','--json').stdout);report['dockerVersion']=docker('version','--format','{{.Server.Version}}').stdout.strip()
 reg=prefix+'-registry';containers.append(reg)
 docker('run','-d','--name',reg,'--pull=never','--user','1000:1000','--cap-drop=ALL','--security-opt','no-new-privileges','--read-only','--memory','256m','--cpus','1','--tmpfs','/var/lib/registry:rw,noexec,nosuid,nodev,size=256m,uid=1000,gid=1000','--tmpfs','/tmp:rw,noexec,nosuid,nodev,size=16m,uid=1000,gid=1000','-p','127.0.0.1::5000','-e','OTEL_TRACES_EXPORTER=none',REGISTRY)
 inspected=json.loads(docker('inspect',reg).stdout)[0];binding=inspected['NetworkSettings']['Ports']['5000/tcp'][0];assert binding['HostIp']=='127.0.0.1';address='127.0.0.1:'+binding['HostPort'];repo=address+'/funds/reconciler'
 report['registryVersion']=docker('exec',reg,'registry','--version').stdout.strip();report['registryImage']=REGISTRY;report['baseImage']=BASE
 for attempt in range(30):
  try:request('/v2/');break
  except OSError:time.sleep(.2)
 else:raise AssertionError('Registry did not start')
 check('registry-loopback-only',binding['HostIp'],'127.0.0.1')
 refs={};digests={};manifests={}
 for variant in ['prior','recovery']:
  marker=tmp/(variant+'-marker');marker.write_text('synthetic-'+variant+'\n');marker.chmod(0o444);name=prefix+'-'+variant;containers.append(name)
  docker('create','--name',name,'--pull=never','--network','none',BASE,'python','-c','print("unused")');docker('cp',marker,name+':/training-marker')
  local=prefix+':'+variant;images.append(local);docker('commit','--change','LABEL dr.training='+prefix,name,local)
  result=docker('run','--rm','--pull=never','--network','none','--user','1000:1000','--cap-drop=ALL','--security-opt','no-new-privileges','--read-only',local,'python','-c','from pathlib import Path;print(Path("/training-marker").read_text().strip())')
  check(variant+'-image-runs-marker',result.stdout.strip(),'synthetic-'+variant)
  push_archive(local,variant)
  digest,body,media=manifest(variant);descriptors=json.loads(body);verified_blobs=True
  for blob in [descriptors['config'],*descriptors['layers']]:
   content,_=request('/v2/funds/reconciler/blobs/'+blob['digest']);verified_blobs=verified_blobs and len(content)==blob['size'] and 'sha256:'+hashlib.sha256(content).hexdigest()==blob['digest']
  check(variant+'-registry-content-digests-match',verified_blobs,True)
  digests[variant]=digest;manifests[variant]=(body,media);refs[variant]=repo+'@'+digest
 check('image-digests-differ',digests['prior']!=digests['recovery'],True)
 for key in ['prior','recovery']:cos('generate-key-pair','--output-key-prefix',key)
 cos('signing-config','create','--no-default-rekor','--no-default-fulcio','--no-default-oidc','--no-default-tsa','--out','local-signing.json')
 check('unsigned-prior-image-rejected',verify(refs['prior'],'prior').returncode!=0,True)
 sign(refs['prior'],'prior','prior');check('prior-image-signature-accepted',verify(refs['prior'],'prior').returncode,0)
 check('different-key-rejects-prior-image',verify(refs['prior'],'recovery').returncode!=0,True)
 for variant in ['prior','recovery']:
  body,media=manifests[variant];request('/v2/funds/reconciler/manifests/candidate','PUT',body,{'Content-Type':media})
  check('candidate-resolves-'+variant,manifest('candidate')[0],digests[variant])
  result=verify(repo+':candidate','prior');check('candidate-'+variant+'-verification',result.returncode==0,variant=='prior')
 check('pinned-prior-remains-verifiable',verify(refs['prior'],'prior').returncode,0)
 sign(refs['recovery'],'prior','recovery');check('old-key-can-still-produce-valid-signature',verify(refs['recovery'],'prior').returncode,0)
 check('new-trust-rejects-old-only-signature',verify(refs['recovery'],'recovery').returncode!=0,True)
 sign(refs['recovery'],'recovery','recovery');check('new-key-accepts-recovery-image',verify(refs['recovery'],'recovery','recovery').returncode,0)
 check('wrong-release-annotation-rejected',verify(refs['recovery'],'recovery','prior').returncode!=0,True)
 check('old-key-signature-still-cryptographically-valid',verify(refs['recovery'],'prior').returncode,0)
 check('old-image-not-accepted-by-new-trust',verify(refs['prior'],'recovery').returncode!=0,True)
 check('image-signature-does-not-create-provenance',verify_attestation(refs['recovery']).returncode!=0,True)
 predicate={'buildDefinition':{'buildType':build_type,'externalParameters':expected_params,'internalParameters':{},'resolvedDependencies':[]},'runDetails':{'builder':{'id':'https://ci.example.invalid/unapproved/runner'},'metadata':{'invocationId':'synthetic-lab'}}}
 attest(refs['recovery'],predicate);statements=decode(verify_attestation(refs['recovery']));check('observed-statement-version',sorted({x.get('_type') for x in statements}),['https://in-toto.io/Statement/v0.1']);check('unapproved-builder-attestation-signature-valid',len(statements)>0,True)
 check('unapproved-builder-policy-rejected',any(policy(x,digests['recovery']) for x in statements),False)
 predicate['runDetails']['builder']['id']=builder;predicate['buildDefinition']['externalParameters']={**expected_params,'debug':True};attest(refs['recovery'],predicate)
 statements=decode(verify_attestation(refs['recovery']));check('unknown-build-parameter-policy-rejected',any(policy(x,digests['recovery']) for x in statements),False)
 predicate['buildDefinition']['externalParameters']=expected_params;attest(refs['recovery'],predicate)
 statements=decode(verify_attestation(refs['recovery']));report['verifiedSyntheticStatements']=statements;accepted=[x for x in statements if policy(x,digests['recovery'])];check('approved-expectations-policy-accepted',len(accepted)>0,True)
 check('provenance-subject-other-digest-rejected',any(policy(x,digests['prior']) for x in statements),False)
 check('candidate-still-bound-to-recovery',manifest('candidate')[0],digests['recovery'])
 check('recovery-signature-still-valid-after-attestations',verify(refs['recovery'],'recovery','recovery').returncode,0)
 assert len(observations)==29,len(observations)
 report.update(passed=True,digests=digests,verifiedStatementType=accepted[0]['_type'],verifiedPredicateType=accepted[0]['predicateType'],verifiedBuilder=accepted[0]['predicate']['runDetails']['builder']['id'],policyScope='Original local allow-list checks on already cryptographically verified statements; not a SLSA level assessment or cluster admission controller.')
finally:
 for name in reversed(containers):docker('rm','-f',name,ok=False)
 for name in tags+images:docker('image','rm',name,ok=False)
 left=docker('ps','-a','--filter','name='+prefix,'--format','{{.Names}}').stdout.strip();left_images=docker('image','ls','--filter','reference='+prefix+':*','--format','{{.Repository}}:{{.Tag}}').stdout.strip()
 report['cleanup']=dict(ownedContainersRemoved=not left,ownedImageTagsRemoved=not left_images and all(docker('image','inspect',name,ok=False).returncode!=0 for name in tags+images),temporaryMaterialRemoved=False,realCredentialsUsed=False,publicRegistryWrites=False)
 shutil.rmtree(tmp);report['cleanup']['temporaryMaterialRemoved']=not tmp.exists()
 report.update(executedAt=datetime.datetime.now(datetime.timezone.utc).isoformat(),scriptSha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),observations=observations,scope='Actual local OCI image signatures,tag movement,new-key trust selection,annotation checks and signed provenance expectation checks. HTTP only on loopback with synthetic keys; transparency verification explicitly absent. No OIDC,public transparency,KMS,live admission,vulnerability scan,isolated CI builder,SLSA level certification or production recovery executed.')
 out.write_text(json.dumps(report,indent=2)+'\n')
assert report['cleanup']['ownedContainersRemoved'] and report['cleanup']['ownedImageTagsRemoved'];print('PASS',len(observations))
NA PRÁTICA

A chave antiga continua a produzir uma assinatura válida, mas a confiança nova e a política de proveniência recusam o artefacto.

Armadilhas comuns

Reassinar bytes suspeitos, confiar numa tag móvel, omitir consumidores antigos e confundir filtros de análise com correções.

Tópicos relacionados: Assinatura e proveniência · Resposta a incidentes · CI/CD e menor privilégio · Análise estática e exceções

Leva esta ideia contigo

Uma recuperação precisa de origem aceite, evidência vinculada ao digest e adoção demonstrada pelos consumidores.

Criar conta

Referência: CKS domains and exam details · Kubernetes v1.35; current six-domain CKS outline

Kubernetes® e CKS são marcas comerciais ou marcas registadas de The Linux Foundation. A dr.pt é uma plataforma de preparação independente e não está afiliada, associada, patrocinada, autorizada nem aprovada por The Linux Foundation. Os conteúdos e as perguntas são originais, não são perguntas oficiais de exame, e concluir os nossos testes não atribui nem garante qualquer certificação. Os nomes são usados apenas para identificar o tema. Todas as outras marcas pertencem aos respetivos titulares.