GitHub's new OIDC subject format: fixing AWS trust policies
Not authorized to perform sts:AssumeRoleWithWebIdentity on a new repo? GitHub added owner and repo IDs to the OIDC sub claim. How to check and fix the policy.
Crunu teamPublished 7 min read
On this page
If a GitHub Actions job fails with "Not authorized to perform sts:AssumeRoleWithWebIdentity" in a repository created after July 15, 2026, or one that was renamed, transferred or opted in since, a likely cause is GitHub's new subject claim format. The sub claim now carries numeric IDs, repo:octo-org@123456/octo-repo@456789:ref:refs/heads/main instead of repo:octo-org/octo-repo:ref:refs/heads/main, so an AWS trust policy that matches the old string no longer matches. The fix is to add the new format to the role's trust policy, or better, to check the immutable repository_id and repository_owner_id claims that AWS STS now exposes as condition keys.
This post covers what changed, how to see which format a repository uses, a policy that works for both, and why a wildcard across the whole organization is the wrong way to make the error go away. The guide to CI/CD secrets with OIDC covers the wider setup across CI systems and clouds.
What GitHub changed
GitHub announced the change in its changelog on April 23, 2026, and updated it on June 10, when the separator between name and ID became @ (GitHub changelog). The OIDC reference gives the reason: the OpenID Connect specification requires subject claims to be locally unique and never reassigned, but the old format used only names, and "if a namespace was recycled, a different owner could create the same subject value" (GitHub: OpenID Connect reference).
The new rules, in short:
- New repositories created after July 15, 2026 use the immutable format automatically.
- Renames and transfers after July 15, 2026 also move a repository to the new format.
- Existing repositories keep the old format "unless you opt in", at the organization or repository level, in the OIDC settings or through the REST API.
- GitHub Enterprise Server is not included.
The syntax is repo:OWNER@OWNER-ID/REPO@REPO-ID:ref:refs/heads/BRANCH. GitHub chose @ "because @ cannot appear in GitHub usernames or repository names." Only sub changes; other claims such as repository and job_workflow_ref keep their name-based values.
This is why the breakage looks random. One team's new service fails on its first deploy while the old services, with an identical workflow, keep working.
Check which format a repository uses
The repository's OIDC customization endpoint returns two useful fields: use_immutable_subject, and sub_claim_prefix, described as "the current sub claim prefix for this repository" (GitHub REST API: OIDC).
gh api repos/kiteline/web-app/actions/oidc/customization/subIf the prefix contains @ and a number after the owner and the repository, the repository is on the new format. Kiteline is a fictional company; use your own owner and repository names.
You will need the numeric IDs for the policy. The repository API returns both:
gh api repos/kiteline/web-app --jq '.owner.id, .id'To see every claim a job actually receives before touching the cloud side, GitHub suggests the github/actions-oidc-debugger action, which "requests a JWT and prints the claims included within the JWT".
A trust policy that accepts both formats
During a migration you may have repositories on both formats behind one role. The simplest fix lists both subjects explicitly:
{
"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:kiteline/web-app:ref:refs/heads/main",
"repo:kiteline@123456/web-app@456789:ref:refs/heads/main"
]
}
}
}A list inside StringEquals matches if either value matches. Once the repository is on the new format and deploys work, remove the old line.
A sturdier fix doesn't depend on the sub string at all. AWS STS now maps individual GitHub claims to condition keys, including repository_id, repository_owner_id, ref, environment and job_workflow_ref. The IAM documentation says repository IDs "are immutable and don't change even if the repository is renamed", and it recommends using such IDs because "a name that is freed by renaming or deletion can be claimed by a different account" (AWS: IAM and STS condition keys).
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:repository_owner_id": "123456",
"token.actions.githubusercontent.com:repository_id": "456789",
"token.actions.githubusercontent.com:ref": "refs/heads/main"
}
}This policy works the same whichever sub format the repository uses, keeps working after a rename, and refuses a new repository that someone creates under a recycled name.
Switching existing repositories without a failed deploy
Repositories created before July 15, 2026 will keep working on the old format until something moves them: a rename, a transfer, or an opt-in. It is better to choose the moment yourself than to find out during an incident, so move them one at a time:
-
Update the role's trust policy to accept the new format, using one of the two policies above.
-
Opt the repository in. Through the API, the same endpoint takes a
use_immutable_subjectflag:Shell gh api -X PUT repos/kiteline/web-app/actions/oidc/customization/sub \ -F use_default=true -F use_immutable_subject=true -
Run the deploy workflow and confirm the role is assumed.
-
Remove the old-format subject from the policy.
use_default is required by the endpoint. If the repository uses a custom subject template, send use_default=false with its existing include_claim_keys instead, so the opt-in doesn't reset the template. GitHub notes that with immutable subjects, the owner and repository IDs are always part of the repo segment, even in a customized subject.
Don't fix it with a wildcard across the organization
The tempting fix for "the subject doesn't match" is to loosen the match. One write-up of the breakage shows a policy that matched repo:my-org/* and fixed it by adding repo:my-org@<your-org-id>/* (DEV Community, aws-builders). That restores the deploy, and it also keeps a role that any workflow in the organization can assume.
Count what repo:my-org/* admits:
- Every repository in the organization, including ones created next month by anyone who can create repositories.
- Every branch, including a feature branch where someone adds a step that runs commands as the role.
- Pull request runs, whose subject is
repo:ORG/REPO:pull_request. - Every workflow file, not just the deploy workflow.
A role that deploys production should match one repository, by ID, and either the protected branch or a deployment environment with required reviewers. Separate roles for separate jobs cost a few lines of policy each. If an organization-wide role is genuinely needed, for example to read a shared artifact bucket, give it read-only permissions that would do little harm in the wrong job.
The same idea on GitLab and Bitbucket
The underlying lesson, match stable IDs rather than names, applies to the other CI systems too.
GitLab CI. The default sub is project_path:{group}/{project}:ref_type:{type}:ref:{branch_name}. GitLab's documentation recommends including "stable, unique identifiers such as namespace_id and project_id alongside path-based claims like sub". On GitLab.com, AWS accepts these as condition keys; on GitLab Self-Managed and Dedicated, only sub is supported as an AWS condition key. GitLab also blocks ID tokens when the configured sub contains a project path "that another project previously used", with the error "ID token issuance is disabled in CI because a project path in the sub claim was previously used by a different project". The fix is to put project_id first in the project's ci_id_token_sub_claim_components and update the trust policy to match (GitLab: OIDC authentication using ID tokens).
Bitbucket Pipelines. Atlassian's setup guide has you copy the workspace, repository and deployment environment UUIDs when configuring the resource server, rather than names (Atlassian: Pipelines and OIDC).
To draft a policy for your own repository and cloud, try the OIDC policy builder.
How a Crunu CI binding handles it
Crunu uses OIDC the same way a cloud does, as a check on who is calling. A service token gets a CI binding (OIDC), and the server accepts the token only together with a JWT from the CI provider whose claims match the binding's rules.
The rules for GitHub Actions match individual claims, not the sub string: the repository, such as kiteline/web-app, the ref, the deployment environment, and optionally the workflow ref, repository ID and owner ID. So the new subject format doesn't break an existing binding, and adding the repository ID and owner ID pins the token to that exact repository even if the name is later reused. A replayed JWT is refused, and the replay raises an alert.
The binding answers the question this post is about, which job may sign in. It doesn't limit what a legitimate job does with what it receives, so each token should still read only the environments its pipeline needs. Details are on the GitHub Actions integration page and in the glossary entries for the OIDC subject claim and IAM trust policy.
Checklist
Sources
- Immutable subject claims for GitHub Actions OIDC tokens, GitHub changelog, checked October 30, 2026.
- OpenID Connect reference, GitHub Docs, checked October 30, 2026.
- REST API endpoints for GitHub Actions OIDC, GitHub Docs, checked October 30, 2026.
- IAM and AWS STS condition context keys, AWS IAM User Guide, checked October 30, 2026.
- GitHub changed its OIDC subject claims and broke my AWS deploys for new repos, DEV Community, checked October 30, 2026.
- OpenID Connect (OIDC) authentication using ID tokens, GitLab Docs, checked October 30, 2026.
- Integrate Pipelines with resource servers using OIDC, Atlassian Support, checked October 30, 2026.