OpenID Connect, Cloud Federation, Trust Policies, and Secretless Deployment: Guided Hands-On Workflow
The first lesson established the federation trust chain. This lesson makes it executable without a cloud account: you will create synthetic claims, prove one allowed and two denied identities locally, then map the same invariant to AWS, Microsoft Entra/Azure, and Google Cloud without storing a long-lived credential.
Learning objectives
- Run a complete free local claim-to-policy simulation without requesting a real OIDC token.
- Model one allowed identity and multiple denied identities before touching a cloud account.
- Translate the same workload identity into AWS, Azure, and Google Cloud trust designs.
- Distinguish provider action configuration from provider-side trust configuration.
- Capture reproducible claim, policy, run, and decision evidence without storing credentials.
1. Scenario and safety boundary
The lab uses a synthetic company named acme-labs, a
synthetic repository named widget-api, and invented
numeric IDs. No real JWT, cloud account, provider role, service
account, tenant, subscription, or access credential is required. The
mandatory path is therefore free and local. Optional cloud exercises
are clearly separated and should be used only in learner-owned
sandboxes.
The target identity is a production deployment job. It must come from one repository, target one environment, use the expected audience, and execute through an approved workflow path. The lab first proves the policy locally; only then does it show how an equivalent policy maps to providers.
2. Preflight: record assumptions before changing anything
mkdir -p oidc-lab
cd oidc-lab
python3 --version
git --version 2>/dev/null || true
cat > assumptions.txt <<'EOF'
checked=2026-09-10
runner_label=ubuntu-24.04
real_oidc_token_requested=false
cloud_credentials_used=false
synthetic_owner=acme-labs
synthetic_repo=widget-api
synthetic_owner_id=100200300
synthetic_repo_id=400500600
target_environment=production
EOF
The assumptions file is evidence, not configuration magic. It states which parts are simulated and prevents a reviewer from confusing the lab with a live federation proof.
3. Create an explicitly synthetic claim set
{
"iss": "https://token.actions.githubusercontent.com",
"aud": "sts.amazonaws.com",
"sub": "repo:acme-labs@100200300/widget-api@400500600:environment:production",
"repository": "acme-labs/widget-api",
"repository_owner_id": "100200300",
"repository_id": "400500600",
"workflow_ref": "acme-labs/widget-api/.github/workflows/oidc-production.yml@refs/heads/main",
"environment": "production",
"ref": "refs/heads/main",
"runner_environment": "github-hosted",
"run_id": "900001",
"run_attempt": "1"
}
Save that JSON as claims-allowed.json. It is not a JWT
and has no signature. That is deliberate: the exercise teaches
trust-condition logic without normalizing the unsafe habit of
printing or sharing signed tokens.
4. Build the policy evaluator before provider YAML
# evaluate.py
import json, sys
EXPECTED = {
"iss": "https://token.actions.githubusercontent.com",
"aud": "sts.amazonaws.com",
"repository_owner_id": "100200300",
"repository_id": "400500600",
"environment": "production",
"ref": "refs/heads/main",
}
claims = json.load(open(sys.argv[1], encoding="utf-8"))
failed = [k for k, v in EXPECTED.items() if claims.get(k) != v]
if failed:
print("decision=DENY")
print("mismatched_claims=" + ",".join(failed))
raise SystemExit(1)
print("decision=ALLOW")
print("subject=" + claims["sub"])
print("run_id=" + claims["run_id"])
The evaluator deliberately checks independent fields rather than accepting an organization wildcard. In a provider, signature and token-time validation are also mandatory and are performed by the federation service. The local script does not pretend to simulate cryptographic verification; it simulates only the policy-match layer.
5. Prove one allow and two denial cases
cp claims-allowed.json claims-wrong-repo.json
python3 - <<'PY'
import json
p="claims-wrong-repo.json"
d=json.load(open(p))
d["repository_id"]="999999999"
json.dump(d, open(p,"w"), indent=2)
PY
cp claims-allowed.json claims-wrong-env.json
python3 - <<'PY'
import json
p="claims-wrong-env.json"
d=json.load(open(p))
d["environment"]="preview"
json.dump(d, open(p,"w"), indent=2)
PY
python3 evaluate.py claims-allowed.json
python3 evaluate.py claims-wrong-repo.json || true
python3 evaluate.py claims-wrong-env.json || true
Expected evidence is ALLOW only for the exact
production identity. The repository-ID mutation and environment
mutation must be denied. Keep those failures: they demonstrate that
the trust policy rejects identities outside the intended boundary.
6. What the real GitHub job would change
name: OIDC production
on:
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-24.04
environment: production
permissions:
contents: read
id-token: write
steps:
- name: Record safe run identity
shell: bash
run: |
printf 'run=%s attempt=%s sha=%s ref=%s\n' \
"$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT" "$GITHUB_SHA" "$GITHUB_REF"
# Provider-auth step belongs here. Do not print the raw JWT.
This workflow configuration changes GitHub's token-request
eligibility for the deploy job. It does not create AWS
IAM roles, Azure federated credentials, or Google Workload Identity
Pools. Those are provider-side resources that must exist first.
7. Map the identity to AWS trust
AWS expects the GitHub issuer and commonly uses audience
sts.amazonaws.com. A narrow role trust policy should
bind the exact workload identity. Current AWS IAM also exposes
several GitHub OIDC condition keys, including immutable identifiers,
workflow/ref, and environment fields. GitHub's AWS guidance still
cautions that AWS does not support GitHub's subject-customization
mechanism in the same way as some other providers, so use only
condition keys AWS documents.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:repository_owner_id": "100200300",
"token.actions.githubusercontent.com:repository_id": "400500600",
"token.actions.githubusercontent.com:environment": "production"
}
}
}]
}
Optional learner-owned integration: the current verified official
action release is
aws-actions/configure-aws-credentials v6.2.4, commit
cbe3b392738ccf3f987d68400dafcf4b0624a56c. Keep the
role's permission policy separate from its trust policy; trust
decides who may assume the role, while the permission policy decides
what the assumed role may do.
8. Map the identity to Microsoft Entra / Azure
A standard Microsoft Entra federated identity credential binds
issuer, subject, and audience. For Azure public cloud, the
documented audience is api://AzureADTokenExchange. The
subject should match the actual GitHub subject format used by the
repository. New immutable GitHub subjects can be used directly;
Microsoft also documents newer flexible federated identity
credentials for richer claim matching where applicable.
{
"name": "github-widget-api-production",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:acme-labs@100200300/widget-api@400500600:environment:production",
"audiences": ["api://AzureADTokenExchange"]
}
Optional learner-owned integration: the current verified
azure/login release is v3.1.0, whose release commit
resolves to a641126d1b8aa4d1fa005f4f92df94a3a4c4c906.
Client ID, tenant ID, and subscription ID are identifiers rather
than passwords, but still manage them according to your
organization's configuration policy. No client secret is needed for
OIDC login.
9. Map the identity to Google Cloud Workload Identity Federation
Google Cloud maps incoming GitHub claims to workload attributes and uses an attribute condition to reject identities that should not enter the pool. Because GitHub is a multi-tenant issuer, Google recommends an attribute condition that restricts the trusted organization/tenant instead of trusting the issuer URL by itself. Prefer immutable IDs in mappings/conditions where practical.
Attribute mapping:
google.subject=assertion.sub
attribute.repository_id=assertion.repository_id
attribute.owner_id=assertion.repository_owner_id
attribute.environment=assertion.environment
Attribute condition:
assertion.repository_owner_id == '100200300' &&
assertion.repository_id == '400500600' &&
assertion.environment == 'production'
Optional learner-owned integration:
google-github-actions/auth v3.0.0 is pinned at
7c6bc770dae815cd3e89ee6cdf493a5fab2cc093. Its
workload-identity-provider input determines the default OIDC
audience used by the action. The action can use direct federation or
service-account impersonation; those are different provider-side
authorization models.
10. Provider actions are token exchangers, not trust-policy substitutes
| Provider | GitHub OIDC audience | Optional pinned auth action | Provider-side control |
|---|---|---|---|
| AWS | sts.amazonaws.com |
aws-actions/configure-aws-credentials@cbe3b392…
(v6.2.4)
|
IAM OIDC provider + role trust + role permissions |
| Azure | api://AzureADTokenExchange |
azure/login@a641126… (v3.1.0) |
Entra federated identity credential + Azure RBAC |
| Google Cloud | Workload Identity Provider URI by default in auth action |
google-github-actions/auth@7c6bc77… (v3.0.0)
|
Pool/provider mappings + attribute condition + IAM binding |
All three patterns have the same causal shape: GitHub identity → provider trust decision → provider credential → provider authorization. The syntax differs; the evidence model does not.
11. Challenge: choose the layer that should reject the request
A preview branch accidentally reaches a production deployment job.
The job is correctly pinned and has id-token: write.
Which layer should prevent cloud credentials from being issued? The
provider trust policy should reject the ref/environment/repository
identity even if GitHub is technically able to mint an OIDC token.
GitHub environment rules are a valuable first gate, but the cloud
trust policy is the independent second gate.
12. Cleanup and rollback
cd ..
rm -rf oidc-lab
The mandatory simulation creates only local files. If you exercised a real provider sandbox, remove only the exact lab federated credential, role/service account binding, and temporary test resources you created. Read current state first and retain non-secret audit evidence before cleanup.
13. Lesson summary
- Test claim-policy logic locally before provisioning provider trust.
- An allowed identity and denied identities are equally important evidence.
- AWS, Azure, and Google Cloud implement different federation syntax but the same trust separation.
- Provider auth actions request/exchange identity; they do not replace provider-side trust and authorization.
- The free mandatory path never requests or prints a real OIDC token.
Knowledge check
Why does the local evaluator not prove cryptographic token validity?
It intentionally simulates only claim-policy matching. A real provider also verifies JWT signature, issuer, audience, and time validity before evaluating authorization conditions.
Which AWS policy decides whether GitHub may assume the role?
The IAM role trust policy. The role's permission policy separately decides what the assumed role may do.
Why should Google Cloud use an attribute condition with GitHub's issuer?
GitHub is a multi-tenant issuer. The issuer alone identifies GitHub, not your organization or repository, so the provider must restrict the incoming workload identity.
Does Azure OIDC require a client secret?
No. With a correctly configured federated identity credential, Azure Login can use GitHub OIDC without a client secret; the workload identity is matched through issuer, subject/claims, and audience.
A denied test differs only by repository_id. Why
keep that failure as evidence?
It proves the policy is actually enforcing repository identity rather than merely accepting any GitHub token with the right issuer.
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.