DEV Community

New GitHub OIDC Changes That Break AWS Deployments

How a trust policy that worked for every existing repo failed for a brand-new one, how to track it down through CloudTrail, and what you should change in your AWS OIDC trust policies today.

Disclaimer: This post focuses on diagnosing a breaking issue in our AWS OIDC integration with GitHub Actions, not on establishing configuration best practices. The trust policy examples below use wildcards for active troubleshooting and illustrative purposes — these should be scoped appropriately to match your specific GitHub repository branch/environment and AWS environment before use in production.

TL;DR

On July 15th 2026, GitHub started issuing OIDC tokens with a new subject (sub) claim format for newly created repositories. The classic format:

repo:my-org/my-repo:ref:refs/heads/main
Enter fullscreen mode Exit fullscreen mode

is being replaced, for new repos, with an immutable-ID format that embeds the numeric organization ID and repository ID:

repo:my-org@12345678/my-repo@9876543210:ref:refs/heads/main
Enter fullscreen mode Exit fullscreen mode

If your AWS IAM trust policy matches subjects with a pattern like repo:my-org/*, tokens from new repos will not match, and aws-actions/configure-aws-credentials fails with:

Could not assume role with OIDC: Not authorized to perform sts:AssumeRoleWithWebIdentity
Enter fullscreen mode Exit fullscreen mode

The fix is a one-line change: make your sub condition a list that matches both formats.

"StringLike": {
  "token.actions.githubusercontent.com:sub": [
    "repo:my-org/*",
    "repo:my-org@<your-org-id>/*"
  ]
}
Enter fullscreen mode Exit fullscreen mode

Renaming the repo will not help — the embedded IDs are immutable by design and survive renames. That's the whole point of the format.

The Setup

I deploy AWS CDK stacks from GitHub Actions using OIDC federation — the recommended, keyless approach. The IAM role trust policy was the standard org-wide pattern that countless blog posts and templates (including AWS's own samples) recommend:

Condition:
  StringEquals:
    token.actions.githubusercontent.com:aud: "sts.amazonaws.com"
  StringLike:
    token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrganization}/*:*
Enter fullscreen mode Exit fullscreen mode

This setup had been working across many repositories in our Github and AWS org for months. Then I created a new repository, wired up the exact same workflow, the same environment, the same role...and every run died at the credentials step.

The Symptom

##[error]Could not assume role with OIDC: Not authorized to perform sts:AssumeRoleWithWebIdentity
Enter fullscreen mode Exit fullscreen mode

This error is maddening because it means "a trust policy condition didn't match" and nothing more. The GitHub side looks completely healthy and the runner fetches its ID token without complaint. If you turn on step debug logging you'll see something like:

##[debug]ID token url is https://run-actions-....actions.githubusercontent.com/.../idtoken/...?api-version=2.0&audience=sts.amazonaws.com
Enter fullscreen mode Exit fullscreen mode

which is easy to misread as an audience problem. It isn't...that's just the runner requesting the token, and the audience matched fine.

I went through the usual suspects, and none of them were it:

  • id-token: write permission —> present.
  • The audience input on configure-aws-credentials —> already sts.amazonaws.com, matching the trust policy's aud condition.
  • The OIDC identity provider existing in the account —> it did.
  • The trust policy allowing the org —> it did, with a wildcard, under StringLike (wildcards don't work under StringEquals, a classic mistake, but not ours today).
  • The GitHub environment twist —> jobs that run under environment: development get a sub of repo:org/repo:environment:development rather than the branch form. Worth knowing! But our troubleshooting efforts leading to a trailing wildcard covered it.

Everything checked out. The same configuration demonstrably worked in sibling repos. So what was different about this repo?

CloudTrail Doesn't Lie

The breakthrough came from looking at the failed AssumeRoleWithWebIdentity events in CloudTrail (in the region your workflow targets, since STS calls are regional):

aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity \
  --max-results 5
Enter fullscreen mode Exit fullscreen mode

The event's userIdentity showed exactly what GitHub presented to AWS:

repo:my-github-org@12345678/my-github-repo@9876543210:environment:development
Enter fullscreen mode Exit fullscreen mode

There it is. The subject claim wasn't repo:my-github-org/my-github-repo:... — the org and repo slugs each carried an @<numeric-id> suffix. Our trust pattern repo:my-github-org/*:* requires a / immediately after the org slug, so repo:my-github-org@12345678/... could never match.

You can also see this without waiting for a failed run. GitHub exposes each repo's subject-claim template:

gh api /repos/<org>/<repo>/actions/oidc/customization/sub
Enter fullscreen mode Exit fullscreen mode

For the broken repo:

{
  "use_default": true,
  "use_immutable_subject": false,
  "sub_claim_prefix": "repo:my-github-org@12345678/my-github-repo@9876543210"
}
Enter fullscreen mode Exit fullscreen mode

It's the Repo's Creation Date

I queried that endpoint for every repo in the org, and a clean pattern fell out:

Repo Created Sub prefix format
repo created Jul 10 July 10, 2026 repo:my-github-org/<repo> (classic)
repo created Jul 17 July 17, 2026 repo:my-github-org@12345678/<repo>@<id> (immutable IDs)
repo created Jul 17 July 17, 2026 repo:my-github-org@12345678/<repo>@<id> (immutable IDs)

Nothing about our configuration changed. GitHub changed the default subject-claim format for newly created repositories — in our org's case, sometime between July 10 and July 17, 2026. Existing repos kept the classic format (for now); new repos get the immutable-ID format. That's why "I validated the setup is identical to my other repos" was true and irrelevant: the difference wasn't in anything I could see in the repo settings UI.

Github did all this on July 15th:

July 15, 2026: GitHub will automatically enforce the new format for all new repositories and renames.

The embedded IDs are the numeric, permanent identifiers:

gh api /orgs/<org> --jq .id      # organization ID
gh api /repos/<org>/<repo> --jq .id  # repository ID
Enter fullscreen mode Exit fullscreen mode

Why This Format Exists

The immutable-ID format fixes a real security gap in slug-based trust. Org and repo names are reusable: if an org gets renamed or deleted, someone else can claim the old name, create matching repos, and mint OIDC tokens whose subjects satisfy your slug-based trust policy. Your AWS account would happily hand them a role. Numeric IDs can't be re-registered, so repo:my-org@12345678/* can only ever be satisfied by your org, no matter what happens to the name.

The IDs are there precisely to survive renames. Rename the repo and the sub becomes repo:my-org@12345678/new-name@9876543210:..., still carrying the same IDs, still not matching a classic-only trust pattern.

The Fix

StringLike conditions accept a list of patterns, and any match allows the assume. Add the immutable-ID pattern alongside the classic one:

{
  "Effect": "Allow",
  "Principal": {
    "Federated": "arn:aws:iam::<account-id>:oidc-provider/token.actions.githubusercontent.com"
  },
  "Action": "sts:AssumeRoleWithWebIdentity",
  "Condition": {
    "StringEquals": {
      "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
    },
    "StringLike": {
      "token.actions.githubusercontent.com:sub": [
        "repo:my-org/*",
        "repo:my-org@12345678/*"
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Or in CloudFormation, parameterized:

Parameters:
  GitHubOrganization:
    Type: String
    Description: GitHub organization slug (case sensitive)
  GitHubOrganizationId:
    Type: String
    Description: Numeric GitHub organization ID (gh api /orgs/<org> --jq .id)

# ...inside the role's AssumeRolePolicyDocument:
            Condition:
              StringEquals:
                token.actions.githubusercontent.com:aud: "sts.amazonaws.com"
              StringLike:
                token.actions.githubusercontent.com:sub:
                  # Classic format (older repos)
                  - !Sub repo:${GitHubOrganization}/*:*
                  # Immutable-ID format (newly created repos)
                  - !Sub repo:${GitHubOrganization}@${GitHubOrganizationId}/*:*
Enter fullscreen mode Exit fullscreen mode

Details that matter:

  • Pin the org ID; don't wildcard it. repo:my-org@*/* works, but it gives up the rename protection the format exists to provide. The ID is a constant — treat it like one.
  • Wildcards only work under StringLike. A wildcard under StringEquals is compared literally and silently never matches.
  • A single * matches across colons, so repo:my-org/* covers both :ref:refs/heads/main and :environment:production subjects. If you scope trust per-repo or per-branch (recommended for high-value roles), update those patterns for both formats too, e.g. repo:my-org@<org-id>/my-repo@<repo-id>:environment:production.
  • Keep both patterns for now. Your existing repos still emit classic subjects. If GitHub migrates existing repos to the new format later, the classic entry becomes dead weight you can eventually remove — but removing it prematurely breaks everything old.

Implications Worth Sitting With

  1. Every slug-based GitHub OIDC trust policy in AWS is now a time bomb for new repos. If your org's trust policies use repo:org/* or per-repo slugs, the first newly created repo that tries to deploy will fail. Fix the policies before that happens, not after.

  2. This isn't AWS-specific. Anything that validates GitHub's OIDC sub claim — GCP Workload Identity Federation, Azure federated credentials, HashiCorp Vault JWT auth has the same exposure. Azure federated credentials are especially brittle here because they match the subject as an exact string, not a pattern.

  3. "Same setup as the working repo" is no longer sufficient validation. Two repos with identical workflows, secrets, and settings can present different identities to your cloud. When diagnosing OIDC failures, compare the actual subjects: gh api /repos/<org>/<repo>/actions/oidc/customization/sub on both, or read the real thing out of CloudTrail.

  4. CloudTrail is the ground truth for STS trust failures. The Not authorized to perform sts:AssumeRoleWithWebIdentity error tells you nothing; the CloudTrail event for the failed call shows the exact federated identity that was rejected. Start there and you'll skip hours of guessing.

A Quick Diagnostic Checklist

When Could not assume role with OIDC strikes:

  1. Read the rejected identity from CloudTrail (lookup-events, event name AssumeRoleWithWebIdentity, in the workflow's target region). Compare it character-by-character against your trust policy patterns.
  2. Check the repo's sub template: gh api /repos/<org>/<repo>/actions/oidc/customization/sub — look for @<id> suffixes in sub_claim_prefix.
  3. Remember the environment: twist — jobs bound to a GitHub environment get ...:environment:<name> subjects, not branch refs.
  4. Confirm aud matches your trust policy's StringEquals (default sts.amazonaws.com).
  5. Confirm wildcards live under StringLike, not StringEquals.
  6. Confirm the OIDC provider exists in the target account (aws iam list-open-id-connect-providers) — though a missing provider produces a different error ("No OpenIDConnect provider found").

Hopefully this saves you the afternoon it cost me.

Top comments (1)

Collapse
 
shieldlyio profile image
Shieldly

Ran into this exact issue on a repo created after 07-15. One thing worth adding: if your trust policy also pins aud to sts.amazonaws.com (it should), the fix is purely the sub list change, nothing else moves. I work on Shieldly, an AI-Powered IAM policy analyzer, and this wildcard-sub pattern (repo:org/* without the numeric-ID variant) is one of the more common trust-policy gaps it flags. Free demo at shieldly.io/app/iam if useful for auditing trust policies like this one.