← Professional Cloud Security Engineer: controlos e evidência
19 / 23 · 135 MIN

Recuperação de confiança e reversão de acesso

Coordena recuperação de identidades, rotação de confiança e reversão de políticas preservando alterações legítimas.

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))
NA PRÁTICA

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

Leva esta ideia contigo

Recuperar exige restabelecer o fluxo legítimo, conservar os limites de confiança e demonstrar que a reversão respeita o estado atual.

Criar conta

Referência: Recover an account protected by 2-Step Verification · Current linked guide; edition date unconfirmed (2026-09-30 inspection)

Google Cloud é uma marca comercial de Google LLC. A dr.pt é uma plataforma de preparação independente e não está afiliada, associada, patrocinada, autorizada nem aprovada por Google. 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.