← Professional Cloud Security Engineer: controls and evidence
14 / 23 · 135 MIN

Revocation, sessions and recovery decisions

Reconstruct an identity timeline, distinguish revocation scopes and support containment and recovery with evidence.

Start with the identity and the clock

At 09:12, a fictional bank’s reconciliation service shows unusual reads. At 09:16, the security owner confirms that a credential left the authorized environment. The incident manager must coordinate containment, investigation and batch continuity. This exercise does not describe internal BNP Paribas procedures. The first useful record identifies the acting identity, credential, accessed resource, observation time and evidence origin. “Access was revoked” is too broad if the action only removed a file from a laptop. Build a timeline with three columns: observed fact, executed action and outcome still requiring confirmation. A successful HTTP response to an administrative change establishes acceptance of that operation; it does not measure every access path. Do not put complete tokens, passwords or private keys in the ticket. Use identifiers that allow records to be correlated without redistributing the secret. Platform staff confirm affected workloads; security assesses containment; operations maintains the batch completion forecast; the PM communicates impact, uncertainty and the next update. Before choosing commands, distinguish a managed user account, a Workforce identity, a service account and an external workload credential source. The same phrase “sign out” can describe different effects. Also define the temporal scope: an action at 09:20 does not automatically erase what happened at 09:10.

Contain users without confusing suspension and deletion

Employee departure and response to a compromised account can use parts of the same inventory, but each needs its own decisions. For a managed Workspace account, suspension resets sign-in cookies and OAuth tokens. Earlier activity and suspicious configuration still need investigation before returning access. If the person used an external identity with direct resource grants, closing their internal directory record does not establish removal of those authorizations. Record the exact principal and check the scopes where it received access. In a second fictional case, a contractor’s engagement ends on Friday. The main project removed its binding, but a data team retains a resource grant. Handover should list the scope already handled and what remains to be confirmed, with an owner for each action. Do not turn one successful change into a certificate of complete revocation. Investigation can continue even when sufficient containment already exists to reduce immediate risk. Deleting a Workforce subject can also affect data that it exclusively owns. A recovery window does not mean data remains accessible throughout that period. Before using deletion for a temporary need, identify ownership, preservation and recovery conditions. A deliberate deletion decision differs from using the command as a shortcut to end a session.

Separate the key from tokens already issued

The team deletes a key at 10:05 and marks the action complete. A token issued at 10:00 remains within its lifetime. These are different objects: preventing future key use does not itself invalidate already-issued tokens. The response plan must also identify impersonation paths and who can issue new credentials. If an unauthorized principal retains Token Creator, key rotation has not closed that path. For the batch, disabling its service account can affect both export and reconciliation when they share that identity. Record this consequence in the decision, together with business impact and a recovery alternative. Do not communicate “only the malicious process was blocked” without a control that actually has that scope. An alternative identity, if approved, needs its own minimum privileges, tests and acceptance criteria. Urgency does not turn a proposal into authorization. Define re-enablement as a decision separate from containment. The team needs to know the latest time unauthorized issuance was possible, the applicable credential lifetime and evidence supporting those bounds. An extended lifetime policy prevents using one hour as a universal maximum. During handover, state the pending condition and who will confirm it; avoid a recovery time based solely on when an alert disappeared.

Identify the credential the application actually uses

An operator revokes local CLI configuration and sees an empty account list. Meanwhile, a service on another server continues accessing data. The observations are not contradictory if the processes use different sources. For service accounts and certain external accounts, gcloud auth revoke has a local effect and does not delete the key or source credential in its issuing system. Record exactly which configuration was removed and on which machine the action occurred. Application Default Credentials also requires an inventory of the effective source. Revoking ADC created by application-default login does not automatically cover an environment-selected file or the technical identity attached to a VM. In a guided exercise, ask the learner for a table of process, host, principal and credential source. Only then associate each action with the scope it can address. Identifying the source does not require printing secrets; the file path and expected identity can be handled under suitable access controls. For human users, a password reset can interrupt mail synchronization for applications using particular OAuth scopes. This does not establish that every other authentication mechanism was handled or that previously downloaded data was erased. Incident closure should combine evidence by access path, including exceptions and gaps, instead of depending on success of one command.

Connect temporary groups, sessions and authentication evidence

A person starts a Workforce session at 14:00 and activates a JIT group at 14:10. In a non-SCIM design, the existing session can retain previous claims. Adding project roles does not automatically solve the problem. First compare authentication time with group-change time and test fresh authentication. In the opposite direction, an IdP membership that expired can remain represented in an old session; the JIT deadline should not be promised as a guaranteed loss-of-access instant without analyzing that mechanism. Session lifetime should form part of acceptance for temporary-access design. In a workshop, present two timelines and ask the team to identify the interval between administrative intent and the observable identity update. If the design uses SCIM, add provisioning evidence and synchronization-health evidence. Do not replace these with an assumption of instantaneous updates. The refresh mechanism must be explicit in the scenario. An IAP session expiry can lead to silent authentication when the IdP session remains active. Not seeing a password box does not establish absence of an authentication flow. Investigate by correlating events and identifiers. Workforce sign-in and token-exchange events belong to STS Data Access collection; confirm configuration before treating an empty search as absence of activity.

Interpret incomplete diagnosis without inventing certainty

A diagnostic tool has inputs, permissions and support limits. If an investigator cannot view a policy or establish group membership, Unknown does not mean the user is prevented from accessing resources. Before changing grants, verify that the query describes the principal, resource and permission actually involved. Repeating a query for an administrator and applying the result to someone else changes the subject of analysis. Pub/Sub policy diagnosis requires attention to supported scope: reviewing the project does not replace reviewing the resource’s own allow policy. Similarly, rejection of an unsupported principal type is not a negative authorization decision. For PABs, the principal-set organization can differ from the target-resource organization. Record where visibility is missing and who can supply evidence. The inspected PAB troubleshooting feature is marked Preview; make that explicit when it affects tool selection. In a guided case, a user receives access during a window and the investigator gets Unknown because group-reading permission is missing. A good next action is obtaining the missing evidence through the authorized process while maintaining necessary containment. Granting Owner to the user to make the result green changes risk without explaining the cause. The report should distinguish an observed real decision, a simulation with defined scope and an incomplete evaluation.

Preserve identity and review hierarchy changes

A deleted technical account and a new account with the same email are not the same identity. Numeric identifiers distinguish them, and old grants do not transfer to the new account merely because names match. When the requirement is recovering the original identity, check the undelete window and conditions before occupying the name with another account. During recovery review, request evidence of the identifier, relevant bindings and workload dependencies. Successfully creating a name does not prove functional restoration. Hierarchy introduces another source of easily missed differences. A project moved to a folder can retain local policy while gaining or losing inherited access. In a fictional case, a reporting team becomes able to read a resource because its project entered a folder with broader grants. A review limited to local IAM misses that change. Compare source and destination, including applicable controls, expected access and negative tests. Analyze Move helps identify warnings and blockers, but existence of a report does not establish every destination condition. A firewall section that failed because of missing permission remains unevaluated. The committee needs a list of completed checks, gaps and owners alongside the execution window. If the move is postponed, record the specific evidence required to decide again.

Practice with a declared timeline and state its limits

The Python exercise uses whole minutes on a fictional incident clock. It receives a principal, current time, declared latest possible issuance time, declared maximum lifetime and token inventory. It does not read real tokens, verify signatures, query IAM or authorize production recovery. Inputs are supplied claims for analysis rather than facts the program independently verified. Consequently, correct code output does not establish containment of a real environment. Start with issuance ending at minute 40, maximum lifetime 60 and observation at minute 95. The declared horizon is 100, leaving five minutes in that model. Then change the time to 100 and inspect the boundary: a token expiring at that instant no longer appears among known tokens still within their lifetime. Remove maximum lifetime or issuance-stop time and confirm the result becomes unknown instead of assuming a favorable deadline. Now add a token issued after the declared stop or lasting longer than the supplied bound. The exercise preserves the contradiction and stops treating that bound as reliable. A token belonging to another principal is identified separately. Incomplete inventory remains explicit even when the calculated horizon has passed. During debrief, explain what the calculation establishes under its assumptions, what incident evidence remains missing and who can authorize recovery. Separating calculation, evidence and decision is the lesson outcome.

"""Offline worksheet for declared credential timing; no cloud authorization decisions.
Times are whole minutes on a fictional incident clock. Input claims are not verified.
"""
import copy
import hashlib
import itertools
import json
from pathlib import Path


def integer(value, name, minimum=0):
 if type(value) is not int or value < minimum:
 raise ValueError(name + ' must be an integer >= ' + str(minimum))
 return value


def label(value, name):
 if not isinstance(value, str) or not value.strip:
 raise ValueError(name + ' must be nonempty text')
 return value


def evaluate(snapshot):
 if not isinstance(snapshot, dict):
 raise ValueError('snapshot must be an object')
 principal = label(snapshot.get('principal'), 'principal')
 now = integer(snapshot.get('now'), 'now')
 stopped = snapshot.get('issuanceStoppedAt')
 maximum = snapshot.get('maximumLifetime')
 if stopped is not None:
 integer(stopped, 'issuanceStoppedAt')
 if stopped > now:
 raise ValueError('issuance stop cannot be in the future')
 if maximum is not None:
 integer(maximum, 'maximumLifetime', 1)
 complete = snapshot.get('inventoryComplete')
 if type(complete) is not bool:
 raise ValueError('inventoryComplete must be boolean')
 tokens = snapshot.get('tokens')
 if not isinstance(tokens, list):
 raise ValueError('tokens must be a list')
 ids = set
 selected = []
 ignored = []
 for token in tokens:
 if not isinstance(token, dict):
 raise ValueError('token must be an object')
 tid = label(token.get('id'), 'id')
 if tid in ids:
 raise ValueError('duplicate token id')
 ids.add(tid)
 owner = label(token.get('principal'), 'token principal')
 issued = integer(token.get('issuedAt'), 'issuedAt')
 expires = integer(token.get('expiresAt'), 'expiresAt')
 if issued > now or expires <= issued:
 raise ValueError('invalid token interval')
 (selected if owner == principal else ignored).append(token)
 contradictions = []
 for token in selected:
 if stopped is not None and token['issuedAt'] > stopped:
 contradictions.append(token['id'] + ':issued-after-declared-stop')
 if maximum is not None and token['expiresAt'] - token['issuedAt'] > maximum:
 contradictions.append(token['id'] + ':exceeds-declared-lifetime')
 # Issuance at the stop minute is included conservatively in the upper bound.
 horizon = stopped + maximum if stopped is not None and maximum is not None else None
 elapsed = None if horizon is None or contradictions else now >= horizon
 return {
 'principal': principal,
 'knownUnexpiredTokens': sorted(t['id'] for t in selected if now < t['expiresAt']),
 'ignoredOtherPrincipal': sorted(t['id'] for t in ignored),
 'declaredHorizon': horizon,
 'remainingUnderDeclaredBounds': None if horizon is None or contradictions else max(0, horizon - now),
 'elapsedUnderDeclaredBounds': elapsed,
 'contradictions': sorted(contradictions),
 'inventoryComplete': complete,
 'inventoryCoverageUnproven': not complete,
 'credentialValidityVerified': False,
 'actualAccessEvaluated': False,
 'productionRecoveryAuthorized': False,
 }


def evidence:
 base = {'principal': 'batch-A', 'now': 100, 'issuanceStoppedAt': 40,
 'maximumLifetime': 60, 'inventoryComplete': True,
 'tokens': [{'id': 't1', 'principal': 'batch-A', 'issuedAt': 39, 'expiresAt': 99}]}
 def variant(**changes):
 return dict(copy.deepcopy(base), **changes)
 samples = {
 'window-elapsed': variant,
 'still-waiting': variant(now=95),
 'unknown-lifetime': variant(maximumLifetime=None),
 'unknown-stop': variant(issuanceStoppedAt=None),
 'incomplete-inventory': variant(inventoryComplete=False),
 'issuance-contradiction': variant(tokens=[{'id': 'late', 'principal': 'batch-A', 'issuedAt': 45, 'expiresAt': 70}]),
 'lifetime-contradiction': variant(tokens=[{'id': 'long', 'principal': 'batch-A', 'issuedAt': 30, 'expiresAt': 110}]),
 'other-principal': variant(tokens=base['tokens'] + [{'id': 'other', 'principal': 'batch-B', 'issuedAt': 90, 'expiresAt': 200}]),
 'expiry-boundary': variant(tokens=[{'id': 'edge', 'principal': 'batch-A', 'issuedAt': 40, 'expiresAt': 100}]),
 'no-tokens-unknown-stop': variant(tokens=[], issuanceStoppedAt=None),
 'two-contradictions': variant(tokens=[{'id': 'conflict', 'principal': 'batch-A', 'issuedAt': 50, 'expiresAt': 150}]),
 }
 results = {name: evaluate(value) for name, value in samples.items}
 assert results['window-elapsed']['elapsedUnderDeclaredBounds'] is True
 assert results['still-waiting']['knownUnexpiredTokens'] == ['t1']
 assert results['still-waiting']['remainingUnderDeclaredBounds'] == 5
 assert results['unknown-lifetime']['declaredHorizon'] is None
 assert results['unknown-stop']['elapsedUnderDeclaredBounds'] is None
 assert results['incomplete-inventory']['inventoryCoverageUnproven'] is True
 assert results['incomplete-inventory']['elapsedUnderDeclaredBounds'] is True
 assert results['issuance-contradiction']['elapsedUnderDeclaredBounds'] is None
 assert results['lifetime-contradiction']['knownUnexpiredTokens'] == ['long']
 assert results['lifetime-contradiction']['remainingUnderDeclaredBounds'] is None
 assert results['other-principal']['ignoredOtherPrincipal'] == ['other']
 assert results['expiry-boundary']['knownUnexpiredTokens'] == []
 assert results['no-tokens-unknown-stop']['elapsedUnderDeclaredBounds'] is None
 assert len(results['two-contradictions']['contradictions']) == 2
 combos = 0
 for stopped, lifetime, now in itertools.product([None, 0, 20], [None, 30, 60], [60, 100, 130]):
 x = variant(tokens=[], issuanceStoppedAt=stopped, maximumLifetime=lifetime, now=now)
 before = copy.deepcopy(x)
 r = evaluate(x)
 assert x == before
 if stopped is None or lifetime is None:
 assert r['elapsedUnderDeclaredBounds'] is None
 elif stopped == 20 and lifetime == 60 and now == 60:
 assert r['elapsedUnderDeclaredBounds'] is False
 assert r['remainingUnderDeclaredBounds'] == 20
 else:
 assert r['elapsedUnderDeclaredBounds'] is True
 assert r['productionRecoveryAuthorized'] is False
 combos += 1
 tokens = [base['tokens'][0], {'id': 'late', 'principal': 'batch-A', 'issuedAt': 45, 'expiresAt': 70},
 {'id': 'other', 'principal': 'batch-B', 'issuedAt': 90, 'expiresAt': 200}]
 expected = evaluate(variant(tokens=tokens))
 permutations = 0
 for perm in itertools.permutations(tokens):
 x = variant(tokens=list(perm)); before = copy.deepcopy(x)
 assert evaluate(x) == expected
 assert x == before
 permutations += 1
 bad = [None, [], variant(principal=''), variant(now=True), variant(now=-1),
 variant(issuanceStoppedAt=101), variant(issuanceStoppedAt='40'),
 variant(maximumLifetime=0), variant(maximumLifetime=True),
 variant(inventoryComplete=1), variant(tokens=None), variant(tokens=[{}]),
 variant(tokens=[base['tokens'][0], base['tokens'][0]]),
 variant(tokens=[{'id':'x','principal':'batch-A','issuedAt':101,'expiresAt':130}]),
 variant(tokens=[{'id':'x','principal':'batch-A','issuedAt':30,'expiresAt':30}]),
 variant(tokens=[{'id':'x','principal':'','issuedAt':30,'expiresAt':50}]),
 variant(tokens=[{'id':'x','principal':'batch-A','issuedAt':30,'expiresAt':True}])]
 for x in bad:
 try:
 evaluate(x)
 except ValueError:
 pass
 else:
 raise AssertionError('invalid input accepted')
 return {'scriptSha256': hashlib.sha256(Path(__file__).read_bytes).hexdigest,
 'fixtures': [{'id': name, **r} for name, r in results.items],
 'timingCombinations': combos, 'inputPermutations': permutations,
 'invalidInputs': len(bad), 'inputPreserved': True, 'orderIndependent': True,
 'network': False, 'cloudExecuted': False, 'persistentWrites': False,
 'scope': 'Fictional declared timing worksheet; no token parsing, signature verification, policy evaluation or real revocation.'}


if __name__ == '__main__':
 print(json.dumps(evidence, indent=2))
IN PRACTICE

On the fictional clock, issuance stopped at 40 and maximum lifetime 60 produce horizon 100. At minute 95 five minutes remain under those assumptions; a token issued at 45 contradicts the declared stop and prevents trusting that calculation.

Common pitfalls

Treating local cleanup as global revocation; deleted key as expired token; Unknown as deny; recreated name as restored identity; partial report as change approval.

Related topics: Identities, credentials and access evidence · Operational security and release evidence · Governance, scope and control evidence

Take this idea with you

Connect identity, credential, scope and time. Administrative action, evidence of its effect and recovery approval are different records that should agree before closing the incident.

Create account

Reference: Respond to compromised Google Cloud credentials · Current linked guide; edition date unconfirmed (2026-09-30 inspection)

Google Cloud is a trademark of Google LLC. bigsavant.com is an independent preparation platform and is not affiliated with, associated with, sponsored, authorised or endorsed by Google. Content and questions are original, are not official exam questions, and completing our tests does not award or guarantee any certification. Names are used only to identify the subject. All other trademarks belong to their respective owners.