GitHub Actions OIDC cloud credentials replace a stored access key with a token GitHub signs for one job, which AWS, Google Cloud or Azure exchange for short-lived credentials only when the token's claims match the trust conditions you wrote. All of the security lives in those conditions: bind them to a repository ID, an environment with required reviewers, or a reusable workflow, and never to an organization name with a wildcard.

The cloud compares the token's claims with your condition and nothing else. Four jobs in the same organization, one condition, one pass.
You have a deploy job with a cloud access key in a repository secret, and you want it gone. OIDC is the replacement, and the switch itself is a few lines of YAML: grant id-token: write to the job, configure the cloud to trust https://token.actions.githubusercontent.com, and call the provider's login action.
The part that decides whether you are safer afterwards is the trust condition on the cloud side. GitHub's OIDC guide requires one: "you must define at least one condition, so that untrusted repositories can't request access tokens for your cloud resources." Every GitHub repository on earth can get a token from the same issuer. What the token says, how to write conditions on AWS, Google Cloud and Azure that only your job can meet, and the mistakes that quietly widen them make up the rest.
What the token says about the job
The job asks GitHub's OIDC provider for a JSON Web Token, signs nothing itself, and hands the token to the cloud. The cloud verifies the signature against the issuer and then evaluates your conditions against the claims. These are the claims that matter for trust, from GitHub's OIDC reference:
| Claim | What it holds | Use it for |
|---|---|---|
sub | A string built from the repository and the job's context: environment, pull_request, or ref | The primary condition on every cloud |
aud | The repository owner URL by default; the official AWS action sends sts.amazonaws.com, Azure login api://AzureADTokenExchange | Required, but it is the same for everyone using that action |
repository_id, repository_owner_id | Numeric IDs that are never reassigned | Conditions that survive a rename and resist name reuse |
ref, environment | The git ref the run is on, and the environment the job references | Narrowing to a branch or a deployment environment |
event_name | The trigger; Dependabot update jobs send dynamic | Refusing events your deploy never uses |
job_workflow_ref | For a reusable workflow, the path and ref of the called workflow | Requiring that one reviewed workflow does the deploy |
Claims and defaults from docs.github.com/en/actions/reference/security/oidc, the
audiencedefault in aws-actions/configure-aws-credentials v6.3.0 and azure/login v3.1.0action.yml, all read 2026-11-12.
The subject formats, and the immutable one
The default sub is one of four shapes. A job that references an environment gets the environment form. Otherwise a pull request event gets pull_request, and anything else gets its branch or tag. That ordering matters: the environment form wins, so a deploy job that names an environment cannot be matched by a branch condition.
Since July 15, 2026, repositories created, renamed or transferred after that date use an immutable subject that carries the owner and repository IDs. Older repositories keep the name-only format until an owner opts in through the OIDC settings or the REST API. GitHub's changelog gives the reason: with the old format, "If a repository or organization name was recycled, a new owner could mint tokens with the same subject claim." If a new repository's login fails against a policy you copied from an old one, this is the first thing to check.
```Subject claim formats
Job references an environment
repo:octo-org/octo-repo:environment:prod
Pull request event, no environment
repo:octo-org/octo-repo:pull_request
Any other event, no environment
repo:octo-org/octo-repo:ref:refs/heads/main
repo:octo-org/octo-repo:ref:refs/tags/v1.2.0
Immutable format: repositories created after July 15, 2026, or opted in
repo:octo-org@123456/octo-repo@456789:environment:prod
## AWS: one role, one environment AWS evaluates the trust policy on the IAM role. IAM checks that `token.actions.githubusercontent.com:sub` is present and that its value is not only a wildcard, and it refuses to save a policy that fails that check. That stops the worst mistake, not the common ones. GitHub's AWS guide also notes that AWS does not support custom claims, so the subject is the only place to put repository and environment; if you need a reusable-workflow condition on AWS, customize the subject to include `job_workflow_ref` (below). The policy here accepts one environment of one repository and nothing else. Pair it with required reviewers on the `prod` environment, because the environment name in the subject proves the job referenced it, not that anyone approved the run. ```role trust policy and deploy.yml { "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:sub": "repo:octo-org/octo-repo:environment:prod" } } } # .github/workflows/deploy.yml jobs: deploy: runs-on: ubuntu-24.04 environment: prod permissions: contents: read id-token: write steps: - uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0 with: role-to-assume: arn:aws:iam::111122223333:role/github-deploy-prod aws-region: us-east-1 Google Cloud: an attribute condition on numeric IDs
On Google Cloud the check is a CEL attribute condition on the workload identity provider, plus IAM bindings on the pool. The google-github-actions/auth README gives the instruction without qualification: "Always add an Attribute Condition to restrict entry into the Workload Identity Pool." Its security notes go further and recommend the numeric repository_owner_id and repository_id over names, because "If you delete your GitHub repository or GitHub organization, someone could claim that same name and establish an identity."
GitHub's docs list Google Cloud as one of the providers that support custom claims, so this is the cloud where event_name and ref belong in the condition itself rather than in the subject. Find the IDs with gh api repos/octo-org/octo-repo --jq .id and gh api orgs/octo-org --jq .id.
gcloud iam workload-identity-pools providers create-oidc "octo-repo" \ --project="my-project" --location="global" --workload-identity-pool="github" \ --issuer-uri="https://token.actions.githubusercontent.com" \ --attribute-mapping="google.subject=assertion.sub,attribute.repository_id=assertion.repository_id" \ --attribute-condition="assertion.repository_owner_id == '1342004' && assertion.repository_id == '260064828' && assertion.ref == 'refs/heads/main' && assertion.event_name == 'push'" # in the workflow - uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 with: workload_identity_provider: projects/123456789/locations/global/workloadIdentityPools/github/providers/octo-repo service_account: deployer@my-project.iam.gserviceaccount.com The IDs 1342004 and 260064828 are the example values from the action's docs/SECURITY_CONSIDERATIONS.md; substitute your own.
Azure: exact match, or a flexible credential
Microsoft Entra federated identity credentials compare issuer, subject and audience by exact match, with no wildcards, and an app or user-assigned managed identity holds at most 20 of them. Exact match is the safe default: one credential per environment, with subject repo:octo-org/octo-repo:environment:prod and audience api://AzureADTokenExchange, which is what azure/login sends.
Flexible federated identity credentials, in preview on 2026-11-12, add a small expression language with matches for wildcards. For GitHub tokens Microsoft requires every flexible expression to match sub and at least one of repository_id or repository_owner_id, which closes the name-reuse hole by design. The Azure CLI, Azure PowerShell and the Terraform providers do not support them yet, so you create them through Microsoft Graph, for example with az rest, or in the portal.
```Azure federated credential and login step
Flexible credential expression (Microsoft Graph, claimsMatchingExpression.value)
claims['sub'] matches 'repo:octo-org/octo-repo:environment:*' and claims['repository_id'] eq '456789'
in the workflow
- uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906 # v3.1.0 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} ```
The mistakes that make a trust policy too wide
Each of these passes a casual review and accepts tokens you did not mean to accept.
| Condition | What else it accepts | Tighten it to |
|---|---|---|
repo:octo-org/*, or repository_owner alone | Every repository in the organization, including one created tomorrow by anyone allowed to create repositories | One repository by ID, or an environment subject |
StringLike on repo:octo-org/octo-repo* | Any repository whose name starts that way: repo:octo-org/octo-repo-sandbox:environment:prod passes | StringEquals on the full subject; if you need a wildcard, keep the context before it: repo:octo-org/octo-repo:environment:prod-* |
ref:refs/heads/main on a deploy role | Jobs from issues, issue_comment and workflow_run, which run with the default branch as their ref and present the same subject as a push to main | An environment subject with required reviewers |
repo:octo-org/octo-repo:pull_request | Every pull request run in the repository, from any branch | Never on a role that can write anything |
| Names instead of IDs | A new owner of a deleted or renamed organization or repository | repository_id, repository_owner_id, or the immutable subject |
aud checked, sub not | Any repository using the same official action, since they all send the same audience | Always both |
The
issues,issue_commentandworkflow_runrows in the events reference list "Default branch" asGITHUB_REF, and the subject reference says the branch form applies when the job has no environment and the event is not a pull request event (docs.github.com, read 2026-11-12).
Environments, reusable workflows and the subject template
An environment in the subject only proves the job referenced it. Since December 8, 2025, environment branch rules evaluate against the ref the job runs on, which for pull_request_target is the default branch, so a "main only" deployment rule does not keep a privileged pull request workflow out. Required reviewers do: a job that references the environment waits until one of them approves it, and cannot read the environment's secrets before that. On GitHub Free, Pro and Team plans, required reviewers are available only for public repositories.
For organizations with many repositories, the stronger pattern is a reusable deploy workflow that every repository calls. Customize the subject with the REST API to include job_workflow_ref, then require that value on the cloud side. GitHub's documented template combines repo, context and job_workflow_ref. Create the matching cloud condition first: every new job uses the new template the moment you set it, and logins fail until the cloud agrees.
gh api -X PUT repos/octo-org/octo-repo/actions/oidc/customization/sub \ --input - <<'JSON' { "use_default": false, "include_claim_keys": ["repo", "context", "job_workflow_ref"] } JSON # resulting subject repo:octo-org/octo-repo:environment:prod:job_workflow_ref:octo-org/octo-automation/.github/workflows/oidc.yml@refs/heads/main Print the claims before you write the condition
Write the condition from what the token actually contains, not from what you expect. GitHub's docs still point at github/actions-oidc-debugger, but that repository is archived; its README now carries the replacement as a plain step, which prints the decoded claims and never the token itself. Run it in the job you plan to trust, on the event you plan to trust, then delete it.
If the job fails before this step with a token error, the cause is usually permissions rather than trust: see OIDC: failed to get ID token and the provider pages for AADSTS70021 on Azure and impersonation errors on Google Cloud.
- name: Show OIDC claims env: AUDIENCE: sts.amazonaws.com run: | TOKEN_JSON=$(curl -s -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=$AUDIENCE") ID_TOKEN=$(echo "$TOKEN_JSON" | jq -r .value) echo "$ID_TOKEN" | awk -F. '{print $2}' | base64 -d 2>/dev/null | jq -r FAQ
Is OIDC more secure than storing AWS access keys in GitHub secrets?
Yes, if the trust policy is narrow. The credentials AWS returns expire on their own and nothing long-lived is stored in the repository. A trust policy that accepts repo:your-org/* gives the same reach to every repository in your organization, so the improvement depends entirely on the condition.
Why is my OIDC sub claim different from the documentation example?
Three usual reasons. The job references an environment, which replaces the branch in the subject; the event is a pull request, which puts pull_request there instead; or the repository uses the immutable format, which adds @ and the numeric owner and repository IDs. Print the claims from the job itself and copy the subject from the output.
Can a pull request from a fork assume my AWS role?
A pull_request run from a public fork gets a read-only token, and its subject is repo:owner/repo:pull_request, so a policy that does not accept that subject refuses it. The risk is the privileged events: pull_request_target, issue_comment and workflow_run run with the default branch as their ref, so scope deploy roles to an environment with required reviewers, not to main.
What audience should I use for GitHub Actions OIDC?
The one your provider's official action sends: sts.amazonaws.com for aws-actions/configure-aws-credentials and api://AzureADTokenExchange for azure/login. google-github-actions/auth defaults it to the workload identity provider name, which is what Google Cloud expects. Treat the audience as required plumbing, not as a security boundary, because every user of the same action sends the same value.
References
- GitHub Docs: OpenID Connect reference, claims and subject formats (read 2026-11-12)
- GitHub Changelog: immutable subject claims for GitHub Actions OIDC tokens
- AWS IAM: configuring a role for the GitHub OIDC identity provider (read 2026-11-12)
- google-github-actions/auth: security considerations
- Microsoft Entra: flexible federated identity credentials (preview, read 2026-11-12)



