Checkpoint Lab — OpenID Connect, Cloud Federation, Trust Policies, and Secretless Deployment
This checkpoint combines Chapter 20 into one evidence-driven migration exercise. You will define a fake production workload identity, prove one allow and two denials, map the policy to three cloud providers, and plan the retirement of a static credential without requesting or printing a real OIDC token.
Learning objectives
- Define a production workload identity and predict its authorization state before testing.
- Show one allowed and two denied identities with a faithful local policy simulator.
- Produce AWS, Azure, and Google Cloud mappings without real credentials or cloud mutation.
- Design a migration from a static credential to short-lived federation with a bounded rollback plan.
- Assemble an evidence packet linking run/revision, claims, trust policy, role identity, and expected audit state.
1. Checkpoint scenario
You are migrating the fictional
acme-labs/widget-api production deployment away from a
long-lived cloud key. The intended GitHub identity is repository ID
400500600, owner ID 100200300, environment
production, branch main, and the
production workflow. The exercise never requests a live OIDC token
and never creates a cloud role.
Your deliverable is not just an “allow” result. You must prove that the intended identity is accepted, two neighboring identities are denied, provider mappings preserve the same invariant, and the old static credential has a controlled retirement plan.
2. Current assumptions and preflight
| Assumption | Checkpoint value |
|---|---|
| Verification date | 2026-09-10 |
| GitHub-hosted runner for optional workflow | ubuntu-24.04 |
| OIDC issuer |
https://token.actions.githubusercontent.com
|
| Required GitHub permission for real token request |
id-token: write on the credential-bearing job
|
| Repository subject assumption | Synthetic immutable-format subject; learner must inspect actual repository format |
| Cloud account required | No — mandatory path is local simulation |
| Credentials | None; all identities and IDs are synthetic |
mkdir -p ch20-checkpoint/evidence
cd ch20-checkpoint
cat > evidence/assumptions.txt <<'EOF'
checked=2026-09-10
real_oidc_token=false
cloud_mutation=false
runner=ubuntu-24.04
owner_id=100200300
repository_id=400500600
environment=production
ref=refs/heads/main
EOF
3. Make predictions before execution
Prediction A: the exact production identity will be allowed by the local policy evaluator because issuer, audience, immutable repository/owner IDs, environment, and ref all match.
Prediction B: a pull/preview identity with
environment=preview will be denied even if it comes
from the correct repository.
Prediction C: a different repository ID will be denied even if it uses the production environment. Record these predictions before creating the variant files.
cat > evidence/predictions.txt <<'EOF'
A exact production identity => ALLOW
B same repo, preview environment => DENY
C different repo id, production environment => DENY
EOF
4. Create the synthetic allowed identity
{
"iss": "https://token.actions.githubusercontent.com",
"aud": "sts.amazonaws.com",
"sub": "repo:acme-labs@100200300/widget-api@400500600:environment:production",
"repository_owner_id": "100200300",
"repository_id": "400500600",
"repository": "acme-labs/widget-api",
"environment": "production",
"ref": "refs/heads/main",
"workflow_ref": "acme-labs/widget-api/.github/workflows/oidc-production.yml@refs/heads/main",
"workflow_sha": "0123456789abcdef0123456789abcdef01234567",
"run_id": "920001",
"run_attempt": "1",
"runner_environment": "github-hosted"
}
Save this as claims-prod.json. The SHA is synthetic but
shaped like a Git commit. No field is a secret. The subject is
explicitly synthetic and demonstrates the 2026 immutable format; do
not assume your repository uses this exact string.
5. Write the checkpoint trust policy as data
{
"iss": "https://token.actions.githubusercontent.com",
"aud": "sts.amazonaws.com",
"repository_owner_id": "100200300",
"repository_id": "400500600",
"environment": "production",
"ref": "refs/heads/main",
"workflow_ref": "acme-labs/widget-api/.github/workflows/oidc-production.yml@refs/heads/main"
}
Save this as policy.json. A real provider also verifies
the token signature and time validity; the local evaluator will
verify only this authorization condition set.
6. Evaluate exact-match policy safely
# check_policy.py
import json, sys, pathlib
claims = json.load(open(sys.argv[1], encoding="utf-8"))
policy = json.load(open("policy.json", encoding="utf-8"))
mismatch = {k: {"expected": v, "actual": claims.get(k)}
for k, v in policy.items() if claims.get(k) != v}
decision = "DENY" if mismatch else "ALLOW"
result = {
"input": pathlib.Path(sys.argv[1]).name,
"decision": decision,
"mismatch_keys": sorted(mismatch),
"subject": claims.get("sub"),
"run_id": claims.get("run_id"),
}
print(json.dumps(result, indent=2))
raise SystemExit(1 if mismatch else 0)
The evaluator prints only non-secret identity metadata. It never accepts a raw JWT and never performs a network call.
7. Create two denied identities and preserve their failures
cp claims-prod.json claims-preview.json
cp claims-prod.json claims-other-repo.json
python3 - <<'PY'
import json
p="claims-preview.json"; d=json.load(open(p))
d["environment"]="preview"
d["sub"]="repo:acme-labs@100200300/widget-api@400500600:environment:preview"
json.dump(d, open(p,"w"), indent=2)
p="claims-other-repo.json"; d=json.load(open(p))
d["repository_id"]="700800900"
d["repository"]="acme-labs/other-api"
d["sub"]="repo:acme-labs@100200300/other-api@700800900:environment:production"
json.dump(d, open(p,"w"), indent=2)
PY
python3 check_policy.py claims-prod.json \
| tee evidence/allowed.json
python3 check_policy.py claims-preview.json \
> evidence/denied-preview.json || true
python3 check_policy.py claims-other-repo.json \
> evidence/denied-other-repo.json || true
Open all three evidence files. Confirm that the two denied cases
name the mismatched claim keys. The || true is used
only so the checkpoint script can preserve both deliberate denial
outputs; it must not be used to hide a real production
authentication failure.
8. Translate the same invariant to three providers
AWS design: configure the GitHub OIDC provider,
require audience sts.amazonaws.com, bind the documented
immutable repository/owner/environment conditions, and attach only
the deployment permissions needed by the role. Optional action pin:
aws-actions/configure-aws-credentials@cbe3b392738ccf3f987d68400dafcf4b0624a56c
(v6.2.4).
Azure design: create a Microsoft Entra federated
identity credential using issuer
https://token.actions.githubusercontent.com, the actual
production subject, and audience
api://AzureADTokenExchange; grant Azure RBAC
separately. Optional action pin:
azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906
(v3.1.0).
Google Cloud design: create a Workload Identity
Pool/Provider, map immutable GitHub claims, require an attribute
condition for the trusted owner/repository/environment, and grant
only the required IAM binding. Optional action pin:
google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093
(v3.0.0).
9. Optional real-GitHub workflow skeleton
name: OIDC checkpoint
on:
workflow_dispatch:
permissions: {}
jobs:
build-evidence:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: Record source
shell: bash
run: |
printf 'run=%s attempt=%s sha=%s\n' \
"$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT" "$GITHUB_SHA"
federated-deploy:
needs: build-evidence
runs-on: ubuntu-24.04
environment: production
permissions:
contents: read
id-token: write
steps:
- name: Provider exchange belongs here
run: echo "Use only a learner-owned sandbox; never print the JWT."
This skeleton proves permission placement and job separation, but without provider configuration it should not claim a successful federation. The mandatory checkpoint remains the local simulation.
10. Migration plan: static key → federation
| Phase | Action | Evidence / rollback guard |
|---|---|---|
| 1. Inventory | Identify existing cloud principal, GitHub secret name, scope, and consumers | Owner, expiry/rotation metadata; never export secret value |
| 2. Create trust | Add narrow federated provider identity and least-privilege role | Policy revision, exact expected claims |
| 3. Negative tests | Prove wrong repo/environment are denied | Provider/local denial evidence |
| 4. Positive sandbox | Run the exact workload through OIDC in a disposable target | Run ID/attempt/SHA + provider session/audit + target state |
| 5. Controlled cutover | Switch production workflow to federation | Exact artifact digest, deployment record, health evidence |
| 6. Retire static key | Remove GitHub secret and revoke/delete provider key | Independent proof both GitHub storage and provider credential are gone |
| 7. Monitor | Alert on unexpected federation principals/claims | Provider audit queries and exception ownership |
Rollback during phases 2–4 can restore the previous workflow path while the old key is still deliberately controlled. After phase 6, a failure should normally roll back federation policy/workflow revisions—not silently recreate the old long-lived key.
11. Required evidence packet
Event/ref/SHA, workflow path/revision, run ID/attempt, environment, runner label/image.
Issuer, audience, actual/synthetic subject format, immutable repository/owner IDs, workflow/reusable-workflow claims.
One allowed and two denied identities, policy revision, mismatched keys for denials.
Role/service-account identity, least-privilege permission scope, temporary credential lifetime—never credential value.
Exact artifact digest, deployment/environment record, provider audit correlation, external target health.
State clearly whether evidence is local simulation or live provider proof and which plan/provider features were unavailable.
find evidence -maxdepth 1 -type f -print -exec sha256sum {} \;
Hashing the evidence files makes later accidental edits detectable. It does not turn the simulated claims into cryptographically signed GitHub tokens; document that limitation explicitly.
12. Verification checklist
- The allowed identity matches every policy field.
-
The preview environment is denied and names
environmentas a mismatch. -
The other repository is denied and names
repository_idor related identity fields as mismatches. - No raw GitHub OIDC JWT, provider access token, service-account key, client secret, or cloud private key appears in any file.
-
The optional workflow grants
id-token: writeonly to the federation job. - Provider designs keep trust conditions separate from role/RBAC/IAM permissions.
- The migration plan ends with both GitHub secret removal and provider-side key revocation.
13. Cleanup and rollback
cd ..
rm -rf ch20-checkpoint
If you used a live learner-owned provider sandbox, first export non-secret audit identifiers needed for the evidence packet. Then delete only the exact lab trust/role/binding and target resources you created. Do not use ambiguous “latest” selection and do not delete unrelated identities.
14. What Chapter 20 adds to the operating model
The course now has a complete credential chain for governed deployment: Chapter 19 authorizes entry to an environment, and Chapter 20 binds that eligible workload to a short-lived external identity. The production result is stronger because a leaked long-lived cloud key is no longer the default bridge between GitHub and the provider.
Chapter 21 will move from identity federation to another supply-chain boundary: every third-party action is executable code. The same discipline applies—exact identity, immutable version, narrow permissions, verifiable provenance, and explicit trust.
15. Checkpoint summary
- You defined the workload identity before choosing a provider implementation.
- You proved one allow and two deny decisions without real credentials.
- You mapped the invariant to AWS, Azure, and Google Cloud.
- You produced a controlled static-key retirement plan instead of leaving indefinite fallback credentials.
- You assembled an evidence packet that keeps raw tokens out while retaining enough identity and audit context for independent verification.
Knowledge check
Why does the checkpoint require denied identities as well as an allowed identity?
A positive result proves the intended workload can pass. Negative results prove neighboring identities outside the intended trust boundary are actually rejected.
After a successful OIDC cutover, what two independent removals complete static-key retirement?
Remove the stored GitHub secret and revoke/delete the corresponding provider-side long-lived credential. Doing only one leaves residual risk or stale configuration.
Can a hash of claims-prod.json prove GitHub signed
those claims?
No. It only proves the local evidence file did not change after hashing. The checkpoint explicitly uses a synthetic claim simulation, not a signed GitHub JWT.
The production job has id-token: write, but
another repository ID is denied by the provider. Is this a
failure?
No. It is expected security behavior: GitHub may mint a token for an eligible job, while the provider correctly refuses an identity outside its trust policy.
What is the next trust boundary after this chapter?
Third-party actions and reusable executable dependencies: Chapter 21 focuses on immutable commit-SHA pinning, Marketplace risk, and dependency governance.
Official references and version notes
Version-sensitive GitHub Actions and provider behavior in this lesson was rechecked on 2026-09-10. Re-verify current OIDC subject format, provider trust syntax, action release SHAs, plan/visibility constraints, and temporary-credential lifetimes before production use.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.