Ensaiar a cadeia completa de recuperação
Às 07:40, uma equipa APS de um banco fictício prepara a abertura do serviço de fundos. O IdP está indisponível e o administrador de prevenção perdeu o segundo fator. O gestor tem de descobrir se o acesso de recuperação funciona nas condições do incidente. Este caso não descreve procedimentos internos BNP Paribas. Desenha uma cadeia com operador, identidade, fator, dispositivo, percurso de rede, cofre e autorização. Assinala cada dependência do componente em falha. Uma conta independente não basta se a única chave suplente estiver num cofre que exige o IdP indisponível. A recuperação administrativa também tem limites de privilégio. O procedimento Google distingue a recuperação de utilizadores comuns da recuperação de outra conta administrativa, para a qual é necessário outro super administrador. Confirma o endereço principal no runbook: receber mensagens num alias não o transforma no identificador de início de sessão Google. Mantém o mecanismo de recuperação compatível com a política 2SV aplicável; mover a conta para uma unidade sem obrigatoriedade muda a proteção. No ensaio, o observador regista onde o operador ficou bloqueado, que evidência consultou e quem pode decidir o próximo passo. Testa também ausência do titular e sessão expirada. Não coloques códigos reais no relatório partilhado. Como decisão guiada, compara dois planos: ter mais um operador dependente do mesmo cofre ou disponibilizar um percurso de recuperação autorizado e independente. Explica qual dependência cada plano remove e qual continua presente.
Restaurar federação sem reabrir exposição
Um processo mensal de reconciliação depende de um workload identity pool que parece inativo durante o dia. Antes de o retirar, identifica o calendário real dos consumidores. Uma desativação reversível, com uma janela que inclua o processo mensal, permite observar dependências. Ausência de alertas numa tarde não cobre esse processo. Regista a condição de reversão, o responsável pelo batch e a evidência esperada de uma nova troca de tokens. Distingue depois as operações administrativas. Desativar um provider impede novas trocas, mas tokens existentes podem continuar a conceder acesso. Eliminar o pool tem outro comportamento: as credenciais anteriores não são revogadas, embora não concedam acesso enquanto o pool está eliminado. Se houver undelete antes de expirarem, podem voltar a conceder acesso. O restauro precisa por isso de avaliar a exposição anterior, além da disponibilidade. O nome de um pool eliminado permanece indisponível para reutilização até a eliminação ser permanente. No caso guiado, a equipa de continuidade quer restaurar imediatamente o pool para cumprir o prazo do batch. Segurança ainda investiga uma credencial emitida antes da eliminação. Prepara uma nota de decisão com três linhas: dependência de negócio, efeito do restauro sobre a credencial e condição que falta confirmar. Não concluas que a credencial está expirada apenas porque a consola deixou de mostrar o pool. Um teste de nova autenticação também não demonstra o estado de todas as credenciais anteriores. A aprovação deve especificar os consumidores e o risco residual que foram efetivamente analisados.
Coordenar rotação entre quem assina e quem valida
O percurso de autenticação tem dois lados que podem mudar em momentos diferentes. Num perfil SAML com dois certificados de verificação, a sobreposição permite preparar a nova confiança antes de o IdP começar a assinar com a nova chave. A orientação Google consultada prevê carregar o segundo certificado e aguardar 24 horas para atualização das contas antes da mudança no IdP. Planeia a janela a partir dessa dependência, em vez de tratar o upload como efeito imediato. Retira o material anterior depois de confirmar o fluxo legítimo e o âmbito dos perfis envolvidos. Para OIDC com JWKS carregado manualmente, a cópia configurada também exige atualização coordenada. Publicar uma chave nova no IdP não atualiza essa cópia por si. Na descoberta pública, o endpoint JWKS tem requisitos TLS; a exceção aceite no browser de um operador não torna suportado um certificado autoassinado no consumidor Google. Separa a chave que valida a assinatura do token do certificado que autentica o endpoint HTTPS. Constrói uma tabela de transição original: estado do assinante, chaves aceites no destino, teste positivo e teste negativo. O estado intermédio deve aceitar o fluxo legítimo previsto, mantendo a rejeição do material não confiável. Num incidente, confirma primeiro qual perfil e qual conjunto de chaves estão efetivos. Evita alargar a mudança a todos os perfis só porque um deles falhou. No fecho, guarda referências de configuração e resultados sem incluir chaves privadas ou tokens reais.
Validar o destino e a confiança do fallback
A equipa recebe um ficheiro de configuração de federação num ticket de incidente. O ficheiro não contém chave privada, mas orienta o cliente para endpoints de autenticação. A revisão deve comparar o conteúdo com a configuração aprovada, incluindo destinatário e destinos de rede. Sintaxe JSON válida e nome de provider conhecido não demonstram que o ficheiro é confiável. Trata a proveniência e a integridade da configuração como parte da recuperação. Um proxy termina TLS à frente do IdP. Depois de mudar o backend, o health check devolve sucesso e o certificado frontal continua válido. Ainda é necessário confirmar que o backend serve as chaves do emissor pretendido. Um documento com os campos esperados pode conter outro material. Da mesma forma, uma assinatura válida não resolve um audience diferente: confirma para que destinatário o token foi emitido antes de ampliar os audiences aceites. Considera agora um requisito regional aprovado para troca de tokens. A documentação consultada identifica endpoints STS regionais como Preview. Mudar token_url para o endpoint global pode ajudar a disponibilidade, mas exige avaliar o requisito e a autorização desse desvio. Neste caso, o requisito é uma premissa fictícia da organização, não uma obrigação legal que o curso atribua a todos os bancos. Prepara duas opções para o responsável: manter a restrição e adiar o processamento, ou avaliar formalmente um fallback delimitado. Expõe o impacto e a evidência em falta sem prometer que a mesma configuração serve todos os contextos.
Reverter uma alteração mantendo trabalho concorrente
Às 10:00, o snapshot A contém um binding condicionado para o batch. Às 10:05, uma alteração errada remove a condição. Às 10:08, outra equipa acrescenta uma concessão legítima para recuperação. Repor A inteiro apagaria essa concessão. O âmbito pedido é inverter a mudança errada, preservando o trabalho posterior autorizado. Compara o estado anterior, o resultado da alteração e o estado atual. Se o mesmo binding voltou a mudar entretanto, assinala conflito para reconciliação. Na operação IAM real, a leitura deve conservar condições e a escrita deve usar proteção de concorrência. Uma vista antiga com sufixo _withcond_ não é uma autorização para criar um binding paralelo sem condição. Obtém representação versão 3 e preserva o conteúdo relevante. O etag acompanha a revisão observada; perante ABORTED, repete leitura, análise e escrita. Colar um etag recente num backup antigo não reconcilia esse backup. O gestor de mudança prepara uma comparação que mostre o acesso a recuperar, a alteração concorrente a preservar e os critérios de validação. Pede que outro interveniente explique o efeito esperado antes da execução. Uma resposta versão 1 após remoção autorizada da última condição pode corresponder ao conteúdo pretendido; verifica o conteúdo, em vez de classificar o número isoladamente como erro. No exercício seguinte, esta lógica é praticada com registos sintéticos. O resultado do exercício é uma proposta para revisão e não um payload IAM pronto para aplicação.
Relacionar o estado do papel com as concessões
Uma equipa desativou um papel personalizado durante contenção. Os bindings continuam visíveis e o relatório de turno diz que a ação falhou. A conclusão confunde existência da associação com efeito das permissões. No estado DISABLED, os bindings desse papel não concedem as respetivas permissões. Isso não demonstra ausência de outros caminhos de acesso. O inventário precisa de identificar também os restantes papéis e identidades relevantes para a operação. Se o papel foi eliminado há dois dias, a recuperação por undelete pode ser avaliada dentro da janela documentada de sete dias. Os bindings anteriores continuam registados, sem efeito enquanto o papel está eliminado, e podem recuperar efeito com o restauro. Antes de reativar, revê os membros, o âmbito e as permissões da definição. Um título igual num novo papel não estabelece continuidade com o identificador anterior. Aplica a análise a um incidente fictício de fecho de posições. O gestor recebe um pedido para restaurar rapidamente o papel usado pelo serviço. Produz um pequeno registo com motivo da contenção, definição a recuperar, consumidores dependentes e testes acordados. Inclui uma verificação de acesso que deve voltar a funcionar e outra que deve continuar negada. Se faltar evidência sobre um binding, atribui um responsável à revisão em vez de assumir que uma aplicação funcional prova correção de todo o acesso. Regista separadamente quem aprovou o restauro e quem observou o resultado técnico.
Separar ensaio, herança e efeito observado
Uma proposta de organization policy está em dry-run. O log indica liveResult=ALLOWED e dryRunResult=DENIED. O pedido observado foi permitido pela política live; o outro campo descreve a avaliação simulada. Um painel que conte qualquer DENIED como bloqueio mistura resultados com significados diferentes. Usa o ensaio para identificar operações que uma futura mudança afetaria, com dono de negócio e plano de correção. Quando a autorização abrange apenas o ensaio, limita a atualização a dryRunSpec e confirma que spec ficou preservado. Um ficheiro chamado teste não limita a escrita na API. Guarda a comparação entre configuração anterior e posterior e relaciona os resultados com a versão ensaiada. Promover configuração é uma decisão adicional que precisa do âmbito aprovado. Para recuperar uma release bloqueada, a equipa pode propor eliminar a política local do projeto. Se a pasta tem política explícita, o projeto passa a herdar essa configuração; delete não significa necessariamente regressar ao padrão da constraint. Confirma a hierarquia atual e a política efetiva. Depois de uma correção aceite, considera a propagação antes de concluir que é necessário retirar todas as restrições. O caso guiado exige uma nota de três frases: que operação falha, que política deve governá-la e que observação demonstrará a recuperação. Evita usar apenas a resposta de sucesso da alteração como prova de que o comportamento já mudou.
Exercício: comparar antes, depois e agora
Executa o programa Python fornecido numa pasta local com dados fictícios. Antes de o executar, prevê o resultado de preserve-concurrent-grant: a condição anterior deve regressar e a concessão recovery deve permanecer. Em same-binding-concurrent-edit, o binding atual já não corresponde nem ao estado anterior nem ao resultado da alteração; a proposta deve ficar ausente e o conflito deve estar explícito. Em atomic-conflict, um conflito impede a proposta completa, mesmo que outro binding pudesse ser revertido isoladamente. O campo localId existe apenas neste exercício e não é um campo IAM. As condições são texto opaco, sem interpretação de CEL; expressões logicamente equivalentes podem gerar conflito. A ordenação de membros não altera a comparação, mas duplicados e campos desconhecidos são rejeitados. O esquema exclui auditConfigs e outras partes de uma política real, que uma ferramenta de produção teria de preservar. A ausência de bindings significa lista vazia declarada, não leitura incompleta silenciosamente aceite. O etag atual acompanha a proposta, mas o programa não verifica a sua atualidade num servidor. Não calcula acesso efetivo, não contacta cloud e não autoriza uma escrita. Altera uma condição no estado atual, conserva uma concessão independente e explica a saída antes de repetir os testes. Como entrega final, escreve uma nota de handover com alteração a inverter, trabalho concorrente preservado, conflito por resolver e próxima verificação real necessária. O hash identifica o código executado; não autentica os snapshots nem prova aprovação operacional.
"""Offline worksheet: reconcile synthetic binding records, never call IAM.
localId is an exercise identifier, NOT an IAM binding field. Conditions are
opaque text: this program does not parse CEL or calculate effective access.
A proposal needs human review and a fresh real read-modify-write cycle.
"""
from copy import deepcopy
from hashlib import sha256
from itertools import permutations, product
from pathlib import Path
import json
def nonblank(value):
return isinstance(value, str) and bool(value.strip()) and value == value.strip()
def validate(snapshot):
if not isinstance(snapshot, dict) or set(snapshot) != {'resource', 'etag', 'version', 'bindings'}:
raise ValueError('Expected exactly resource, etag, version and bindings')
if not all(nonblank(snapshot[k]) for k in ['resource', 'etag']):
raise ValueError('Resource and declared etag must be nonblank strings')
if type(snapshot['version']) is not int or snapshot['version'] not in (1, 3):
raise ValueError('Worksheet accepts schema version 1 or 3 only')
if not isinstance(snapshot['bindings'], list):
raise ValueError('Bindings must be a list')
indexed = {}
for binding in snapshot['bindings']:
if not isinstance(binding, dict) or set(binding) != {'localId', 'role', 'members', 'condition'}:
raise ValueError('Expected exact synthetic binding fields')
if not all(nonblank(binding[k]) for k in ['localId', 'role']):
raise ValueError('Local ID and role must be nonblank strings')
if binding['localId'] in indexed:
raise ValueError('Duplicate local ID')
members = binding['members']
if not isinstance(members, list) or not members or not all(nonblank(m) for m in members):
raise ValueError('Members must be a nonempty list of nonblank strings')
if len(members) != len(set(members)):
raise ValueError('Duplicate member')
condition = binding['condition']
if condition is not None and (not nonblank(condition) or snapshot['version'] != 3):
raise ValueError('Opaque condition text requires schema version 3')
indexed[binding['localId']] = {**deepcopy(binding), 'members': sorted(members)}
return indexed
def reconcile(before, after, current):
"""Produce an all-or-nothing structural proposal from declared snapshots.
IDs touched between before/after define the scope. A touched current record
must match after (invert it) or before (already reverted); otherwise conflict.
Unrelated current records survive. Etag is carried as evidence, not verified.
"""
old, changed, now = [validate(s) for s in (before, after, current)]
if len({s['resource'] for s in (before, after, current)}) != 1:
raise ValueError('Snapshots must describe the same declared resource')
touched = sorted(k for k in old.keys() | changed.keys() if old.get(k) != changed.get(k))
proposal = deepcopy(now)
decisions = []
for key in touched:
if now.get(key) == old.get(key):
action = 'already-reverted'
elif now.get(key) == changed.get(key):
action = 'restore-before'
if key in old:
proposal[key] = deepcopy(old[key])
else:
proposal.pop(key, None)
else:
action = 'conflict'
decisions.append({'localId': key, 'action': action})
conflict = any(d['action'] == 'conflict' for d in decisions)
result = {
'resource': current['resource'], 'observedEtag': current['etag'],
'decisions': decisions, 'conflict': conflict,
'untouchedCurrentIds': sorted(set(now) - set(touched)),
'proposal': None, 'humanReviewRequired': True,
'etagFreshnessVerified': False, 'celEvaluated': False,
'effectiveAccessVerified': False, 'productionWriteAuthorized': False,
}
if not conflict:
bindings = [proposal[k] for k in sorted(proposal)]
result['proposal'] = {'resource': current['resource'], 'etag': current['etag'],
'version': 3 if any(b['condition'] is not None for b in bindings) else current['version'],
'bindings': bindings}
return result
def binding(key='batch', member='serviceAccount:batch@example.invalid', condition='window-A'):
return {'localId': key, 'role': 'roles/example.reader', 'members': [member], 'condition': condition}
def snapshot(bindings=None, etag='current-C', version=3):
return {'resource': 'projects/fictional-funds', 'etag': etag, 'version': version,
'bindings': deepcopy([binding()] if bindings is None else bindings)}
def evidence():
before = snapshot(etag='before-A')
after = snapshot([binding(condition=None)], 'after-B')
concurrent = binding('recovery', 'group:recovery@example.invalid', None)
fixtures = []
def record(label, b, a, c):
original = deepcopy((b, a, c))
result = reconcile(b, a, c)
assert (b, a, c) == original
fixtures.append({'id': label, **result})
return result
r = record('preserve-concurrent-grant', before, after, snapshot([binding(condition=None), concurrent]))
assert r['proposal']['bindings'] == [binding(), concurrent]
assert r['proposal']['etag'] == 'current-C'
assert r['untouchedCurrentIds'] == ['recovery']
r = record('same-binding-concurrent-edit', before, after, snapshot([binding(condition='window-C')]))
assert r['conflict'] and r['proposal'] is None
r = record('already-reverted', before, after, before)
assert r['decisions'][0]['action'] == 'already-reverted'
r = record('restore-deleted-binding', before, snapshot([]), snapshot([concurrent]))
assert r['proposal']['bindings'] == [binding(), concurrent]
r = record('remove-added-binding', snapshot([]), before, snapshot([binding(), concurrent]))
assert r['proposal']['bindings'] == [concurrent]
r = record('deleted-by-another-change', before, after, snapshot([]))
assert r['conflict']
r = record('version-three-restored', before, after, snapshot([binding(condition=None)], version=1))
assert r['proposal']['version'] == 3
r = record('no-declared-binding-change', before, before, snapshot([concurrent]))
assert r['decisions'] == [] and r['proposal']['bindings'] == [concurrent]
multi_before = snapshot([binding(), binding('second')])
multi_after = snapshot([binding(condition=None), binding('second', condition=None)])
r = record('atomic-conflict', multi_before, multi_after,
snapshot([binding(condition=None), binding('second', condition='other-change')]))
assert r['proposal'] is None
r = record('opaque-condition-change', before, after, snapshot([binding(condition='window-A && true')]))
assert r['conflict'] # No claim that logically equivalent expressions compare equal.
r = record('member-order-only', snapshot([{'localId':'x','role':'roles/example.reader','members':['user:a','user:b'],'condition':None}]),
snapshot([{'localId':'x','role':'roles/example.reader','members':['user:b','user:a'],'condition':None}]), snapshot([]))
assert r['decisions'] == []
r = record('no-condition-version-preserved', snapshot([], version=1), snapshot([concurrent]), snapshot([concurrent]))
assert r['proposal']['version'] == 3
states = [None, binding(), binding(condition=None), binding(condition='window-C')]
combinations = 0
for b, a, c in product(states, repeat=3):
inputs = [snapshot([] if x is None else [x]) for x in (b, a, c)]
result = reconcile(*inputs)
expected_conflict = b != a and c != b and c != a
assert result['conflict'] == expected_conflict
if not expected_conflict:
expected = c if b == a else b
assert result['proposal']['bindings'] == ([] if expected is None else [expected])
combinations += 1
rows = [binding(condition=None), concurrent, binding('observer', condition=None)]
expected = reconcile(before, after, snapshot(rows))
orders = 0
for order in permutations(rows):
assert reconcile(before, after, snapshot(list(order))) == expected
orders += 1
invalid = []
def altered(fn):
s = snapshot(); fn(s); invalid.append(s)
altered(lambda s:s.pop('etag'))
altered(lambda s:s.update(etag=''))
altered(lambda s:s.update(etag=' stale '))
altered(lambda s:s.update(resource=''))
altered(lambda s:s.update(version=True))
altered(lambda s:s.update(version=2))
altered(lambda s:s.update(version=1))
altered(lambda s:s.update(bindings={}))
altered(lambda s:s['bindings'].append(deepcopy(s['bindings'][0])))
altered(lambda s:s['bindings'][0].update(localId=''))
altered(lambda s:s['bindings'][0].update(role=''))
altered(lambda s:s['bindings'][0].update(members=[]))
altered(lambda s:s['bindings'][0].update(members=['user:a','user:a']))
altered(lambda s:s['bindings'][0].update(members=['']))
altered(lambda s:s['bindings'][0].update(condition=''))
altered(lambda s:s['bindings'][0].update(condition={'expression':'true'}))
altered(lambda s:s['bindings'][0].update(extra='unsupported'))
altered(lambda s:s.update(auditConfigs=[]))
invalid.extend([None, [], 'policy'])
for value in invalid:
try:
reconcile(before, after, value)
except ValueError:
pass
else:
raise AssertionError('Invalid input accepted')
other = snapshot(); other['resource'] = 'projects/other'
try:
reconcile(before, after, other)
except ValueError:
pass
else:
raise AssertionError('Resource mismatch accepted')
return {'scriptSha256': sha256(Path(__file__).read_bytes()).hexdigest(),
'fixtures': fixtures, 'stateCombinations': combinations, 'inputPermutations': orders,
'invalidInputs': len(invalid)+1, 'inputPreserved': True, 'orderIndependent': True,
'network': False, 'cloudExecuted': False, 'persistentWrites': False}
if __name__ == '__main__':
print(json.dumps(evidence(), ensure_ascii=False, indent=2))
O snapshot A contém uma condição; B remove-a; C acrescenta uma concessão legítima. A proposta recupera a condição e conserva a concessão. Se C também alterou o mesmo binding, exige reconciliação.
Armadilhas comuns
Confundir desativação com revogação; restaurar um pool sem avaliar tokens anteriores; copiar etag recente para conteúdo antigo; tratar dry-run como bloqueio; eliminar política local ignorando herança.
Tópicos relacionados: Revogação, sessões e decisões de recuperação · Identidades, credenciais e evidência de acesso · Governação, âmbito e evidência de controlos
Recuperar exige restabelecer o fluxo legítimo, conservar os limites de confiança e demonstrar que a reversão respeita o estado atual.
Referência: Recover an account protected by 2-Step Verification · Current linked guide; edition date unconfirmed (2026-09-30 inspection)