1. Identify the requester, decision maker and executor
A fictional payments change fails with access denied and the first suggestion is to grant Owner. Before changing permissions, draw the path: workflow, identity, token issuance, destination trust, resource authorization and action execution. Record the failing stage and the identity performing it. The user starting a pipeline may be neither its build identity nor the cloud identity used by the task. In GitHub Actions, id-token: write permits requesting an OIDC token; it does not grant Azure write access. Successful login also does not establish authorization for every resource. Request evidence for the intended operation without copying tokens into tickets. In an L3 report, separate authentication failure from insufficient access to an identified resource and name each configuration owner. This distinction helps correct the cause without making the entire subscription accessible to the pipeline.
2. Compare trust with the context actually issued
The example uses a GitHub.com repository created in September 2026. Its observed subject includes owner and repository IDs, while the destination still expects names alone. The current reference describes an immutable format applying to new repositories since July 2026, with differences for earlier repositories and exclusion of GitHub Enterprise Server. Do not turn an old template into a universal rule. Confirm applicable configuration and field matching before changing trust. Adopting an environment can also change subject context. If the intent was to allow only main, retain that intent through appropriate environment controls instead of assuming the Production name proves the branch. In the exercise, issuer and subject are correct but audience differs. Correct the intended match and rehearse authorized access. Increasing an Azure role does not repair a federated credential that fails to match the token.
3. Grant explicit access to cross-project dependencies
Project A’s pipeline needs to read Tools in B. After scoping its identity to the project, clone stops working. Confirm the build identity, required destination-project access and Read on the specific repository. Reverting to a collection identity can hide a misconfigured dependency and broaden access far beyond what is needed. With Azure Repos YAML repository protection, also review explicit checkout declaration and pipeline authorization. A human user’s portal access does not establish equivalent job access. During handover, retain an inventory containing repository, reason for reading, consuming identity and authorization owner. Include indirect dependencies where they exist. When removing a dependency, review the permission that is no longer needed. The intended outcome is a reproducible, restricted path rather than a clone that happened to succeed once. Use that inventory when investigating subsequent permission changes.
4. Separate value rotation from name selection
A Key Vault-linked variable group selects secret names. In a new run, an updated value for an already mapped name can be fetched at runtime; creating api-password-next does not automatically add that new name to group selection. The exercise asks you to identify which kind of change occurred. Before editing scripts, check mapping, access and retrieval evidence without printing values. For a new pipeline using a secret-bearing group, authorize the specific consumer and review who can change code using those secrets. Open access is not a neutral way to eliminate authorization requests. The operational contract should identify who rotates credentials, who updates consumers and how functioning is confirmed without disclosure. If a credential already appeared in a log, fixing the next run does not invalidate the exposed value. Handle rotation or revocation and log access through the response process.
5. Keep untrusted data out of code generation
A PR title looks descriptive but can be controlled by an external contributor. If its expression is interpolated directly into the generated script, the title participates in constructing code the runner executes. For the exercise’s prefix check, pass the value through an intermediate environment variable and treat it as data with quoted expansion. Do not use eval to reinterpret its contents. Reducing token permissions helps limit operations but does not itself correct improper interpretation. During workflow review, ask which fields come from the PR, where they are expanded and which processes consume them. Request benign examples containing spaces and quotation marks to discuss the distinction between text and instruction. The learning objective is recognizing the boundary, not executing commands supplied by an external author. Apply the same reasoning to branch names and other received metadata.
6. Separate external validation from privileged execution
In the scenario, pull_request_target has deployment secrets and checks out an external PR head. Pinning the checkout action by SHA does not make retrieved code trustworthy. Separate external-contribution validation from the privileged job and do not execute that head’s scripts with production secrets. Changing only the event to workflow_run also does not fix untrusted content received as an artifact. Follow the chain from producer to consumer. A second risk arises when both contexts share a persistent runner. Cleaning its workspace does not establish restoration of the operating system, processes and access. Environment approval is an access decision, not host cleanup. Plan execution boundaries and clean-environment evidence appropriate to the risk, including who can schedule runner work and which services the host can reach. Document these assumptions before granting production access.
7. Resolve the case without confusing possibility and evidence
The payments case leaves fifteen minutes in the window and does not establish runner cleanliness after the external PR. Hold delivery on that host and seek a trusted path with change and security owners. Evidence supports saying integrity was not established; it does not automatically establish compromise. Retain records needed for investigation. Rotating a secret and immediately placing its replacement on the same runner can repeat exposure. If no alternative is ready within the window, defer the change and communicate impact, owner and conditions for resumption. The approved package may remain the intended one, but still needs a trusted executor. For sensitive derived values, avoid logs and review explicit masking of transformations where applicable. Base64 is encoding, not permission to publish credentials. Keep observed facts separate from assumptions in the incident and change records.
8. Rehearse a decision with explicit assumptions
The local model below compares three supplied fields and four decision conditions. It validates no JWT, signature, expiry or authenticity; it also performs no login and applies no RBAC. Data is fictional and cryptographic trust is an external assumption, not a code result. Before execution, predict the effect of a different audience and an old subject without IDs. Then retain matching fields and change host_clean to false. Approval remains true but the decision must not allow delivery. The sixteen combinations show that each condition can independently block progress. Use the exercise to construct a table of real evidence needed to populate each input. Finish the lesson with identity, resource, code context, executor state and next owner. Connect this model to the artifacts lesson: trusting package bytes is a different dimension from trusting their executor.
# Fictional decision model over supplied facts; no JWT validation or cloud login.
# Signature, expiry and authenticity are explicitly assumed, never checked here.
# Do not use this exercise as an authentication or authorization implementation.
from itertools import product
def trust_fields_match(observed, expected):
fields = ('iss', 'sub', 'aud')
return all(expected.get(k) and observed.get(k) == expected[k] for k in fields)
def release_ready(fields_match, resource_authorized, host_clean, change_approved):
return all((fields_match, resource_authorized, host_clean, change_approved))
expected = {'iss': 'https://token.actions.githubusercontent.com',
'sub': 'repo:dr-lab@110/funds@220:ref:refs/heads/main',
'aud': 'api://AzureADTokenExchange'}
assert trust_fields_match(dict(expected), expected)
assert not trust_fields_match({**expected, 'aud': 'another-service'}, expected)
assert not trust_fields_match({**expected, 'sub': 'repo:dr-lab/funds:ref:refs/heads/main'}, expected)
assert not trust_fields_match({**expected, 'iss': 'https://example.invalid'}, expected)
assert not trust_fields_match({**expected, 'sub': expected['sub'].replace('main', 'Main')}, expected)
assert not trust_fields_match({}, expected)
assert not trust_fields_match(dict(expected), {})
assert release_ready(True, True, True, True)
assert not release_ready(True, False, True, True)
assert not release_ready(True, True, False, True)
assert not release_ready(True, True, True, False)
assert not release_ready(False, True, True, True)
combinations = list(product((False, True), repeat=4))
assert len(combinations) == 16
assert [c for c in combinations if release_ready(*c)] == [(True, True, True, True)]
print('14 checks and 16 decision combinations passed; supplied facts, not authenticated tokens')
Change approval is complete, but a persistent runner executed external PR code and host cleanliness is unproven. The learner decides how to contain risk and resume authorized delivery.
Common pitfalls
Increasing roles to fix token mismatch; assuming one universal subject; opening groups to every pipeline; confusing rotation with a new name; trusting external code because of its event; treating approval as host cleanup.
Related topics: Least privilege · Secret-exposure response · Software supply chain · Change management
Confirm identity, trust and authorization separately. Delivery also requires trusted code and execution context; window pressure does not replace that evidence.
Reference: Configuring OpenID Connect in Azure · AZ-400 objectives 2026-07-27