Chapter 20Lesson 02~230 minutes

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.

Synthetic claimsAWSAzureGoogle CloudNegative tests

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.
Next lesson

OpenID Connect, Cloud Federation, Trust Policies, and Secretless Deployment: Configuration, Design Patterns, and Trade-Offs

Continue with the next lesson to build on the current concepts, evidence, security boundaries, and operational practices.

Knowledge check

Why does the local evaluator not prove cryptographic token validity?

Which AWS policy decides whether GitHub may assume the role?

Why should Google Cloud use an attribute condition with GitHub's issuer?

Does Azure OIDC require a client secret?

A denied test differs only by repository_id. Why keep that failure as evidence?

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.