Definir evidência de recuperação utilizável
Uma equipa APS de um banco fictício recupera o processamento de posições após uma eliminação acidental. O backup existe e o seu hash coincide com o registo disponível. Ainda falta demonstrar que é possível desencriptar os dados, criar o destino, limitar o acesso e executar a reconciliação funcional. Este caso não descreve procedimentos internos BNP Paribas. O objetivo da aula é transformar a frase «temos backup» numa lista de dependências verificáveis, com responsáveis e critérios de aceitação. Começa por separar o artefacto, a proteção do backup e a proteção do destino. Um backup vault configurado com CMEK usa essa proteção nos backups, incluindo quando o disco de origem usava encriptação gerida pela Google. A versão da chave que protege um backup antigo continua a ser relevante depois de uma rotação. A existência de uma versão primária nova não demonstra que a versão anterior esteja disponível. Regista identificadores completos, estados, identidades de serviço e evidência observada, sem colocar material secreto no relatório. No exercício guiado, desenha uma seta do plano de restauro para o backup e outra para o destino. Acrescenta as dependências criptográficas e de autorização de cada ramo. Para cada seta, escreve qual a observação que confirmaria a ligação. Um hash pode detetar diferença de bytes perante um valor de referência; não prova que esse valor seja autêntico, que a chave funcione ou que o conteúdo seja funcionalmente correto. O gestor acompanha estas condições separadamente e mantém o serviço em recuperação até existirem resultados para o âmbito acordado.
Restaurar objetos dentro da janela correta
O responsável pelo armazenamento altera a retenção de soft delete de sete para trinta dias após descobrir a perda. Essa mudança protege eliminações posteriores segundo a política aplicável; não prolonga retroativamente a janela do objeto já eliminado. A linha temporal do incidente deve registar a eliminação, a política então aplicável e o prazo restante. Uma configuração atual correta não substitui a análise da configuração que governou o evento. O restauro de um objeto cria uma nova geração live. Não se deve exigir que essa geração seja igual à geração eliminada, nem concluir que o restauro falhou apenas por ter um identificador diferente. Guarda a correspondência entre o recurso de origem e o resultado da operação. Se o bucket também tiver sido eliminado, distingue ferramentas: o restauro do bucket por CLI ou JSON API devolve inicialmente um bucket vazio e exige restaurar os objetos separadamente. A consola pode oferecer um fluxo que inclua ambos. Verifica qual foi realmente usado. Num bucket com hierarchical namespace, certas operações podem produzir objetos eliminados com o mesmo nome e geração. Quando essa combinação é ambígua, a API de restauro exige o restoreToken que identifica o objeto pretendido. Não inventes um token nem escolhas a primeira linha de um inventário. Confirma a identidade junto dos metadados devolvidos pelo serviço. Se o projeto inteiro tiver sido eliminado, a disponibilidade dos dados também fica limitada pela janela de recuperação do projeto. Regista ambas as dependências em vez de prometer que a retenção do bucket, isoladamente, garante recuperação.
Conservar concorrência e âmbito de acesso
A aplicação volta a escrever no caminho recuperado enquanto a equipa prepara o restauro. Se o requisito aprovado for «restaurar apenas quando não existe uma versão live», uma consulta anterior não basta: outro processo pode escrever entre a consulta e a alteração. A precondição ifGenerationMatch=0 liga esse requisito à operação de restauro. Se já existir uma versão live, a operação condicionada deve falhar para permitir reconciliação, em vez de substituir silenciosamente trabalho recente. Num bucket sem uniform bucket-level access, a escolha de ACL também importa. Com copySourceAcl=true, a operação copia a ACL do objeto de origem. Se esse parâmetro for omitido ou falso, o objeto restaurado herda a default object ACL do bucket. No caso fictício, essa ACL por omissão é mais ampla do que a ACL restrita do objeto eliminado. Restaurar os mesmos bytes pode, por isso, alterar quem os consegue ler. A equipa deve rever a ACL pretendida e as políticas de acesso efetivas; copiar uma ACL não neutraliza concessões existentes noutros níveis. Os managed folders introduzem outra dependência: o restauro dos objetos não repõe as políticas IAM desses folders. Inclui a recuperação ou reconciliação autorizada dessas políticas no plano e confirma o acesso com a identidade consumidora adequada. Num ensaio, pede ao formando para escrever duas condições de aceitação: ausência de substituição concorrente e acesso limitado ao público aprovado. Cada uma requer evidência própria. A geração restaurada, um hash coincidente e uma resposta HTTP de sucesso não demonstram, por si só, as duas condições.
Suspender destruição sem reabrir o segredo
Uma versão de Secret Manager foi marcada para destruição num segredo com delayed destruction configurada. Durante essa espera, a versão fica desativada. O incidente é entretanto reconhecido como erro, mas a autorização para voltar a ler o segredo ainda não existe. O cancelamento pode ser feito através de uma operação de enable ou de disable sobre a versão. Neste caso, disable cancela a destruição agendada mantendo a leitura bloqueada. O requisito operacional decide qual a transição apropriada. Não confundas esse mecanismo com uma promessa geral de recuperação. Sem a configuração de atraso, a destruição pode ser imediata. O plano deve identificar a versão exata, o estado observado, o prazo disponível e a operação documentada para cancelar. Alterar uma definição para ações futuras não constitui prova de que uma destruição já agendada foi cancelada. Depois da intervenção, recolhe o estado efetivo e confirma separadamente se deve continuar desativada ou ser reativada por decisão autorizada. Cloud KMS tem o seu próprio ciclo de vida e exige outra análise. Uma versão gerada pelo serviço e definitivamente destruída não pode ser recuperada com uma simples alteração de estado. Contudo, há uma exceção documentada para determinadas versões anteriormente importadas, tratada na secção seguinte. Num comité de incidente, evita tanto «todas as chaves são recuperáveis» como «nenhuma chave destruída pode regressar». Apresenta o tipo de versão, a elegibilidade e o material disponível. O exercício pede uma decisão limitada: preservar a possibilidade de recuperação sem conceder acesso antes de este ser aprovado.
Distinguir chave de backup e chave de destino
O backup vault depende de uma versão CMEK que ficou inacessível. A equipa dispõe de uma chave nova, ativa e autorizada para o disco de destino. Esta condição permite analisar a proteção do destino, mas não elimina a necessidade de desencriptar o backup. Um override de chave durante o restauro de um disco individual não torna utilizável uma chave inacessível que protege o vault. Trata os dois ramos do desenho inicial como dependências distintas e verifica as identidades de serviço correspondentes. No restauro de disco individual, a utilização de outra chave CMEK exige que o service agent de Compute Engine do projeto de destino tenha as permissões documentadas de encrypter/decrypter nessa chave. O acesso humano ao backup não demonstra essa autorização do serviço. Mantém também distinta a recuperação de uma VM inteira, cujo comportamento por omissão conserva a configuração de encriptação dos discos, da opção de override disponível no fluxo documentado de disco individual. Para uma versão anteriormente importada em Cloud KMS, consulta reimport_eligible e as condições de reimportação. Se uma importação anterior tiver sido concluída com sucesso, é necessário o mesmo material original; uma chave nova com algoritmo e tamanho iguais não o substitui. A documentação distingue a situação em que nenhuma importação chegou a concluir-se. Não a uses para justificar material diferente numa versão que já protegeu dados. Com Cloud EKM, acrescenta a disponibilidade do serviço externo à análise: IAM e dados locais disponíveis não impedem falhas de leitura ou escrita quando a dependência criptográfica externa está indisponível.
Repor os controlos de IA no âmbito aprovado
A aplicação fictícia usa Model Armor para avaliar pedidos antes de os encaminhar. Durante a recuperação, a equipa muda a aplicação de localização e configura o endpoint global para reutilizar um template. Esse endpoint só suporta floor settings; não suporta as operações de templates e de sanitização usadas neste caso. A localização do template também não pode ser alterada após a criação. Prepara o recurso e o endpoint suportados para o destino aprovado e confirma que a aplicação usa essa configuração. A política fictícia do exercício exige tratamento na localização autorizada. Com data residency enforcement, funcionalidades que não estão disponíveis localmente podem ser desativadas. A ausência de um filtro esperado não justifica desligar automaticamente essa restrição para obter um resultado semelhante ao anterior. Identifica a funcionalidade afetada, a cobertura que se perdeu e as opções compatíveis com a política acordada. Pode ser necessário adiar a ativação, limitar o fluxo ou obter uma decisão explícita sobre outro desenho. Não apresentes esta exigência do caso como regra legal universal. As versões dos filtros exigem ainda outra evidência. Stable é um alias que pode passar a apontar para uma versão mais recente. Para comparar resultados de um ensaio, regista a versão efetiva, os parâmetros e os exemplos de avaliação. Fixar uma versão específica ajuda enquanto essa versão estiver disponível, mas não garante comportamento imutável para sempre. Quando a versão fica Retired, os pedidos passam a usar Stable. Nem todos os filtros usam o mecanismo de versões. O relatório deve delimitar quais os controlos cuja versão foi realmente estabelecida e quais ainda precisam de validação.
Inventariar caches e verificar dependências locais
A equipa limpa recursos temporários depois do ensaio e não encontra uma cache no destino de recuperação. O identificador do recurso inclui projeto e localização de criação. A localização atual da aplicação não muda automaticamente esses elementos. Usa o inventário do recurso original para dirigir a eliminação da cache correta e regista a resposta observada. Um erro de localização não demonstra que o conteúdo deixou de existir. A ausência de caches explícitas também não demonstra ausência de implicit caching. Quando a política fictícia deste exercício proíbe retenção por caching, a decisão deve considerar ambos os mecanismos: desativar implicit caching e evitar explicit caching segundo a documentação aplicável. Não assumes que eliminar um ficheiro de origem prova a remoção de todas as cópias ou caches. Define o âmbito dos recursos analisados e o que a evidência permite concluir. O programa Python da aula trabalha apenas com strings fictícias e um grafo de dependências declarado pelo formando. Cada raiz pode representar um plano de recuperação; requires lista as dependências diretas. O programa percorre também as dependências transitivas, reporta referências ausentes, estados desconhecidos ou indisponíveis e hashes divergentes. Os identificadores são exatos: vault-key/v4 não satisfaz uma referência a vault-key/v3. Uma falha num recurso fora do ramo analisado não bloqueia esse ramo. Porém, um inventário declarado incompleto impede a conclusão supportedUnderDeclaredEvidence, mesmo quando todas as dependências encontradas parecem consistentes. Esta conclusão refere-se exclusivamente aos dados fornecidos, sem consultar serviços cloud.
Executar o exercício e preparar a passagem ao RUN
Executa python3 run.py localmente, sem credenciais. Antes de ler os resultados, prevê o efeito de quatro alterações: retirar a chave do vault, acrescentar uma versão nova com outro identificador, mudar apenas a chave de destino e alterar os bytes do backup. Compara a previsão com requiredClosure, missing, unavailable e hashMismatches. Se houver diferença, identifica a dependência que faltou no teu raciocínio e corrige o desenho antes de alterar o código. O caso self-consistent-untrusted-manifest altera simultaneamente os bytes fictícios e o hash esperado. A comparação volta a ser consistente, mas manifestAuthenticityVerified continua falso. Esta é uma limitação intencional: um manifesto controlado pela mesma origem dos dados não prova a autenticidade de nenhum deles. Os estados available e unknown também são declarações do exercício, não respostas reais de IAM ou KMS. O programa não cifra nem desencripta, não restaura dados, não autentica evidência e não autoriza uma operação em produção. Os testes incluem dependências transitivas, raízes independentes, combinações de estados, permutações da ordem e rejeição de ciclos ou campos inválidos. A entrada é preservada. Para concluir a aula, escreve uma nota de passagem ao RUN com o recurso recuperado, a geração resultante, as dependências criptográficas, o acesso esperado, o resultado funcional e as lacunas restantes. Se só existir consistência do inventário, diz exatamente isso. A decisão de encerrar o incidente precisa das observações reais previstas no início. Resolve os dois casos finais explicando também por que motivo as alternativas não cumprem a restrição de concorrência ou de desencriptação.
"""Original offline recovery-dependency worksheet using fictional strings.
No encryption, decryption, KMS calls or actual restore is performed. States,
references and expected hashes are supplied by the learner, not authenticated.
Exact dependency IDs matter: a new primary key does not satisfy another ID.
"""
from copy import deepcopy
from hashlib import sha256
from itertools import permutations, product
from pathlib import Path
import json
import re
def label(x):
return isinstance(x,str) and bool(x.strip()) and x == x.strip()
def unique(values, nonempty=False):
return isinstance(values,list) and (bool(values) or not nonempty) and all(label(x) for x in values) and len(values)==len(set(values))
def validate(model):
if not isinstance(model,dict) or set(model)!={'nodes','roots','inventoryComplete'}:
raise ValueError('Expected exact worksheet fields')
if not isinstance(model['nodes'],list) or not unique(model['roots'],True) or type(model['inventoryComplete']) is not bool:
raise ValueError('Nodes, unique roots and inventory flag required')
indexed={}
for n in model['nodes']:
if not isinstance(n,dict) or set(n)!={'id','kind','state','requires','payload','expectedSha256'}:
raise ValueError('Expected exact node fields')
if not label(n['id']) or n['id'] in indexed:
raise ValueError('Unique nonblank node IDs required')
if n['kind'] not in ['artifact','dependency'] or n['state'] not in ['available','unavailable','unknown'] or not unique(n['requires']):
raise ValueError('Recognized kind, state and unique dependencies required')
if n['kind']=='artifact':
if not isinstance(n['payload'],str) or not isinstance(n['expectedSha256'],str) or not re.fullmatch('[0-9a-f]{64}',n['expectedSha256']):
raise ValueError('Artifact requires synthetic text and lowercase SHA256')
elif n['payload'] is not None or n['expectedSha256'] is not None:
raise ValueError('Dependency nodes do not carry artifact bytes')
indexed[n['id']]=n
visiting,done=set(),set()
def visit(id):
if id not in indexed or id in done:return
if id in visiting:raise ValueError('Dependency cycle')
visiting.add(id)
for dep in indexed[id]['requires']:visit(dep)
visiting.remove(id);done.add(id)
for id in indexed:visit(id)
return indexed
def analyze(model):
nodes=validate(model);results=[]
for root in sorted(model['roots']):
closure=set()
def walk(id):
if id in closure:return
closure.add(id)
if id in nodes:
for dep in nodes[id]['requires']:walk(dep)
walk(root)
missing=sorted(closure-nodes.keys())
unavailable=sorted(id for id in closure & nodes.keys() if nodes[id]['state']=='unavailable')
unknown=sorted(id for id in closure & nodes.keys() if nodes[id]['state']=='unknown')
mismatches=sorted(id for id in closure & nodes.keys() if nodes[id]['kind']=='artifact' and sha256(nodes[id]['payload'].encode('utf-8')).hexdigest()!=nodes[id]['expectedSha256'])
consistent=not(missing or unavailable or unknown or mismatches)
results.append({'root':root,'requiredClosure':sorted(closure),'missing':missing,
'unavailable':unavailable,'unknown':unknown,'hashMismatches':mismatches,
'declaredDependenciesConsistent':consistent,
'inventoryCoverageUnproven':not model['inventoryComplete'],
'supportedUnderDeclaredEvidence':consistent and model['inventoryComplete'],
'manifestAuthenticityVerified':False,'cryptographicRecoveryExecuted':False,
'cloudAuthorizationVerified':False,'productionRestoreAuthorized':False})
return results
def node(id, requires=(), state='available', text=None):
return {'id':id,'kind':'dependency' if text is None else 'artifact','state':state,
'requires':list(requires),'payload':text,
'expectedSha256':None if text is None else sha256(text.encode()).hexdigest()}
def model():
return {'roots':['restore-plan'],'inventoryComplete':True,'nodes':[
node('vault-key/v3'),node('vault-agent-access'),node('backup', ['vault-key/v3','vault-agent-access'],text='fictional encrypted backup bytes'),
node('target-key/v8'),node('target-agent-access'),
node('restore-plan',['backup','target-key/v8','target-agent-access'])]}
def evidence():
fixtures=[]
def record(id,m):
old=deepcopy(m);r=analyze(m);assert m==old
fixtures.append({'id':id,'results':r});return r
assert record('complete-declared-chain',model())[0]['supportedUnderDeclaredEvidence']
m=model();m['nodes']=[n for n in m['nodes']if n['id']!='vault-key/v3']
assert record('missing-vault-key',m)[0]['missing']==['vault-key/v3']
m['nodes'].append(node('vault-key/v4'))
assert record('new-primary-not-old-version',m)[0]['missing']==['vault-key/v3']
m=model();m['nodes'][0]['state']='unavailable'
assert record('vault-key-unavailable',m)[0]['unavailable']==['vault-key/v3']
m=model();m['nodes'][1]['state']='unknown'
assert record('vault-agent-unknown',m)[0]['unknown']==['vault-agent-access']
m=model();m['nodes'][4]['state']='unavailable'
assert record('target-agent-unavailable',m)[0]['unavailable']==['target-agent-access']
m=model();m['nodes'][2]['payload']='changed bytes'
assert record('backup-hash-mismatch',m)[0]['hashMismatches']==['backup']
m['nodes'][2]['expectedSha256']=sha256(b'changed bytes').hexdigest()
r=record('self-consistent-untrusted-manifest',m)[0]
assert r['declaredDependenciesConsistent'] and not r['manifestAuthenticityVerified']
m=model();m['inventoryComplete']=False
r=record('incomplete-inventory',m)[0];assert r['declaredDependenciesConsistent'] and not r['supportedUnderDeclaredEvidence']
m=model();m['nodes'][0]['state']='unavailable';m['nodes'][3]=node('target-key/v9');m['nodes'][-1]['requires'][1]='target-key/v9'
assert record('target-override-does-not-recover-backup',m)[0]['unavailable']==['vault-key/v3']
m=model();m['nodes'].append(node('unused-old-key',state='unavailable'))
assert record('unrelated-failure-outside-closure',m)[0]['supportedUnderDeclaredEvidence']
m=model();m['nodes'].append(node('other-backup',['missing-other-key'],text='other bytes'));m['roots'].append('other-backup')
r=record('independent-root-results',m);assert r[0]['missing']==['missing-other-key'] and r[1]['supportedUnderDeclaredEvidence']
m=model();m['roots']=['absent-root']
assert record('missing-root',m)[0]['missing']==['absent-root']
m=model();m['nodes'][-1]['requires'].append('source-policy');m['nodes'].append(node('source-policy',['acl-evidence'],state='unknown'))
r=record('transitive-policy-gap',m)[0];assert r['missing']==['acl-evidence'] and r['unknown']==['source-policy']
combinations=0
for key,access,inventory in product(['available','unavailable','unknown'],['available','unavailable','unknown'],[False,True]):
m=model();m['nodes'][0]['state']=key;m['nodes'][1]['state']=access;m['inventoryComplete']=inventory
r=analyze(m)[0]
assert r['supportedUnderDeclaredEvidence']==(key==access=='available' and inventory)
combinations+=1
m=model();expected=analyze(m);orders=0
for order in permutations(m['nodes'][:3]):
other=deepcopy(m);other['nodes']=list(order)+other['nodes'][3:]
assert analyze(other)==expected;orders+=1
bad=[]
def altered(fn):
v=model();fn(v);bad.append(v)
altered(lambda m:m.update(roots=[]))
altered(lambda m:m.update(roots=['backup','backup']))
altered(lambda m:m.update(roots=[' ']))
altered(lambda m:m.update(inventoryComplete='yes'))
altered(lambda m:m.update(nodes={}))
altered(lambda m:m['nodes'].append(deepcopy(m['nodes'][0])))
altered(lambda m:m['nodes'][0].update(id=''))
altered(lambda m:m['nodes'][0].update(kind='key'))
altered(lambda m:m['nodes'][0].update(state='enabled'))
altered(lambda m:m['nodes'][0].update(requires=['backup','backup']))
altered(lambda m:m['nodes'][0].update(requires='backup'))
altered(lambda m:m['nodes'][0].update(requires=['vault-key/v3']))
altered(lambda m:m['nodes'][0].update(requires=['restore-plan']))
altered(lambda m:m['nodes'][0].update(payload='secret material'))
altered(lambda m:m['nodes'][0].update(expectedSha256='0'*64))
altered(lambda m:m['nodes'][2].update(payload=None))
altered(lambda m:m['nodes'][2].update(expectedSha256='not-a-digest'))
altered(lambda m:m['nodes'][2].update(expectedSha256='A'*64))
altered(lambda m:m['nodes'][2].update(extra=True))
altered(lambda m:m.update(extra=True))
bad.extend([None,[]])
for value in bad:
try:analyze(value)
except ValueError:pass
else:raise AssertionError('Invalid input accepted')
return {'scriptSha256':sha256(Path(__file__).read_bytes()).hexdigest(),'fixtures':fixtures,
'stateCombinations':combinations,'inputPermutations':orders,'invalidInputs':len(bad),
'inputPreserved':True,'orderIndependent':True,'network':False,'cloudExecuted':False,'persistentWrites':False}
if __name__=='__main__':
print(json.dumps(evidence(),ensure_ascii=False,indent=2))
O hash do backup coincide, mas a versão da chave do vault está indisponível. Uma chave nova no disco de destino não resolve essa dependência de desencriptação.
Armadilhas comuns
Confundir retenção atual com retroativa; restaurar bytes sem rever ACL; substituir uma chave antiga pela primária nova no inventário; tratar hashes como prova de autenticidade.
Tópicos relacionados: Proteção de dados e gestão de chaves · Recuperação de confiança e reversão de acesso · Failover com capacidade e confiança preservadas
Um restauro utilizável exige dados recuperáveis, dependências criptográficas disponíveis, acesso correto e validação funcional no âmbito aprovado.
Referência: Soft delete overview · Current linked guide; edition date unconfirmed (2026-09-30 inspection)