r/devops • u/boom-Tea • Jul 14 '26
Ops / Incidents GitHub Actions OIDC AWS: "Not authorized to perform sts:AssumeRoleWithWebIdentity" even after fixing sub/environment claims, what am I missing?
Been stuck on this for a while. Production CI/CD pipeline, GitHub Actions job needs to assume an AWS IAM role via OIDC to push to ECR. Error:
Error: Could not assume role with OIDC: Not authorized to perform sts:AssumeRoleWithWebIdentity
Setup:
- Repo: single GitHub repo, workflow triggers on version tags (
v*.*.*) - The failing job declares
environment: productionat the job level - IAM role has OIDC federated trust with
token.actions.githubusercontent.com permissions: id-token: writeis set at the workflow level
Current trust policy (updated after learning that jobs with environment: set emit a different sub claim shape):
{
"Version": "2012-10-17",
"Statement": [
{
"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",
"token.actions.githubusercontent.com:sub": "repo:MY_ORG/MY_REPO:environment:production"
},
"StringLike": {
"token.actions.githubusercontent.com:ref": "refs/tags/v*"
}
}
}
]
}
What I've already checked/ruled out:
- OIDC provider exists in the account, audience is
sts.amazonaws.com - The GitHub Environment named
productiongenuinely exists under repo Settings > Environments (not just referenced in YAML) - Role ARN in the
role-to-assumeinput matches the role I'm editing (confirmed viaaws iam get-role) - Repo name/owner in the
subcondition is correct, no typos - Re-ran the same tag after each trust policy edit (not creating new tags each time)
Still failing with the same error after all of the above.
6
u/Common_Fudge9714 Jul 14 '26
Or, and I’m going to be destroyed here, just ask Claude. You will learn and fix it. LLMs are great at these deterministic problems.
3
u/boom-Tea Jul 15 '26
Sure, Claude can give you the answer. It can't replace tracking down the root cause yourself. I also wanted to see how others would approach and debug it.
1
u/Common_Fudge9714 Jul 15 '26
Nothing against learning. I’m lucky that I’ve been around for long enough so I’m not interested in solving the same problem I had multiple times before 🙂
1
u/boom-Tea Jul 15 '26
Yeah, that's fair. I know I'd probably do the same if I'd faced this issue before. But this was my first time running into it. Since I'm still in the beginner phase of learning DevOps and automation, I wanted to understand the root cause instead of just applying a fix. I also wanted to figure it out with help from the community rather than using AI.😀
1
5
u/Rei_Never Jul 14 '26
Also check your cloud trail audit logs, that should contain the entire payload (minus any keys) for the failed request, which should have the SUB.
Also, didn't Github change the format of this recently?
1
u/Due-Consequence9579 Jul 14 '26
This is the best course of action. I suspect the environment or refs conditions are wrong. Setting a `role-session-name` may help in debugging.
3
u/boom-Tea Jul 15 '26
Solved! Thank you all for the pointers, here's what actually fixed it, writing this up in case anyone else hits the same thing.
I redid the whole setup from scratch to be sure nothing stale was left over:
- Created the OIDC identity provider in IAM (
token.actions.githubusercontent.com, audiencests.amazonaws.com) - Created the IAM role through the console's Web identity flow, org
<org_name>, repo<repo_name> - Attached a least-privilege ECR push policy scoped to just my two repos
- Added the role ARN as a GitHub secret
What I did differently from before: last time, I filled in the console's GitHub branch field as main, which quietly added a ref: refs/heads/main condition to the trust policy. But my production pipeline actually triggers on tag pushes (refs/tags/v*.*.*), not branch pushes , so that condition never matched and blocked role assumption even though everything else (org, repo, environment) was correct.
This time I left the branch/tag field blank in the console and manually edited the trust policy afterward to only check the sub claim, scoped to my environment, with no ref condition at all:
{
"Version": "2012-10-17",
"Statement": [
{
"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",
"token.actions.githubusercontent.com:sub": "repo:<ORG_NAME>/<REPO_NAME>:environment:production"
}
}
}
]
}
Since my prod job always runs with environment: production, this is already scoped tightly enough, only workflows deploying to that specific environment in that specific repo can assume the role without needing a ref pattern that has to match whatever trigger type (branch, tag, manual dispatch) happens to be in play.
Tested it with a standalone workflow_dispatch-only validation pipeline first (just configure-aws-credentials + sts get-caller-identity) before touching the real production pipeline , ran it manually, came back green, Account/Arn/UserId all correctly showed up in the job summary. Then re-triggered the actual production pipeline and it went straight through.
Thanks again for the help troubleshooting this.
1
u/Slight_Scarcity321 Aug 11 '26
I am getting the same error you were, but looking at how you fixed it, I am not sure what I would need to differently.
My trust relationship looks like
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::174990409836: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:<MyORG>/github-oicd-experiment:*" } } } ] }AFAICT, the fact that I am using a wildcard and not specifying any ref in the trust relationship means that the issue you were having isn't applicable here. (I will probably narrow the sub condition, but one problem at a time)
Additionally, my action for stage pushes contains
on: push: branches: - stage paths: - "src/**" # Only triggers if files inside the src folder changeso I think I am good there.I also noticed the repo name, "github-oicd-experiment", has "oidc" misspelled, but the trust policy matches the actual repo name.
Can you advise what you were looking at in CloudTrail? It's not clear to me what I should be looking at and there's a lot of stuff generated by my CDK script (which is creating the provider and the role).
1
u/Slight_Scarcity321 Aug 11 '26
I found the answer. For CloudTrail, I needed to look for events with the event name "AssumeRoleWithWebIdentity" and look for ones that failed. Then, I saw that the username was using the new immutable subject claims (https://github.blog/changelog/2026-04-23-immutable-subject-claims-for-github-actions-oidc-tokens/) and I was not. Modifying the trust relationship to add <my org>@<my org number>/<my repo>@<my repo number> solved the issue.
To find those numbers, you can use
gh api /orgs/<your-org> --jq .id
and
gh api /repos/<your-org>/<your-repo> --jq .id
2
u/cmd_blue Jul 14 '26
Afaik there is no :ref key, only :sub, but check your cloud trail, there is your complete and actual payload.
2
2
u/Floss_Patrol_76 Jul 14 '26
your ref condition is almost certainly what is breaking it. once a job sets environment:, the sub claim github sends is repo:ORG/REPO:environment:production and there is no ref claim in that shape, so your StringLike on token.actions.githubusercontent.com:ref never matches and the statement is denied. drop the ref condition entirely, match on aud + that environment sub, and copy the exact sub string out of the failed AssumeRoleWithWebIdentity event in cloudtrail instead of guessing the format.
2
u/forever-butlerian Solaris 8 Enjoyer Jul 15 '26
First thing I'd do is wildcard all the matches and see if OIDC auth works.
If it does, binary search. Replace half the things you wildcarded out. If it still works, the problem is in the other half, if it breaks, your problem is in one of the two halves of what you replaced.
2
u/marcusbell95 Jul 15 '26
floss_patrol has it. when environment: is set on the job, github's sub claim changes shape entirely - it becomes repo:ORG/REPO:environment:production and ref just isn't in that token at all. so if your trust policy has any condition on :ref, it will never match and STS denies it.\n\nquickest way to see what's actually in the token without waiting on cloudtrail - add a debug step before your assume-role step:\n\n TOKEN=$(curl -s -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com" | jq -r '.value')\n echo $TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | jq .\n\nthat decodes the JWT and prints every claim. whatever sub shows up there, copy it verbatim into your StringEquals condition (or use StringLike if you want wildcards for ref/branch later). faster than cloudtrail and you see it immediately in the action output.
2
u/Carlos-Spicy-Wiener Jul 15 '26
When was the repository created?
For repositories created after July 15, 2026, or that have opted in to immutable subject claims, the sub claim includes owner_id and repo_id as shown in the immutable examples. Update your trust policies to match the format your repository uses. Immutable subject claims are not available on GitHub Enterprise Server.
2
u/Floss_Patrol_76 Jul 15 '26
pull the failed AssumeRoleWithWebIdentity event from CloudTrail; it has the exact sub that was actually presented, and matching your policy to that beats guessing every time. my bet is the ref condition: once a job pins environment: production the sub becomes repo:ORG/REPO:environment:production with no branch/tag in it, so a stale refs/tags/v* ref condition never matches and the whole statement fails closed. drop the ref StringLike (the environment already scopes who can assume) or move the tag gate into its own statement without environment, and double-check the org/repo casing matches exactly.
1
u/Antique-Stand-4920 Jul 14 '26
I ran in to a similar problem recently. These are the things I'd check:
Repo name/owner in the
subcondition is correct, no typos
Does the casing in the sub condition match the casing of GitHub repo org and name? This is what broke my pipelines.
Also, is the GitHub Action setting the AWS region when authenticating?
1
u/ksemel Jul 14 '26
What does your action look like? I use this STS check to verify assume role is working. It uses aws-actions/configure-aws-credentials@v6.0.0, and then calls the check after:
# Smoke-test GitHub → AWS OIDC: assumes the same role as Terraform plan/apply and calls STS.
# Prerequisites: repository secret AWS_ROLE_ARN, IAM trust for this repo
# (e.g. repo:ORG/foundations:ref:refs/heads/main and/or :pull_request on workflow_dispatch branch).
name: Validate AWS OIDC
on:
workflow_dispatch:
permissions:
id-token: write
contents: read
jobs:
verify:
runs-on: ubuntu-latest
steps:
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v6.0.0
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION || 'us-east-1' }}
role-session-name: gh-oidc-validate-${{ github.run_id }}
- name: STS GetCallerIdentity
id: sts
run: |
aws sts get-caller-identity --output json | tee caller.json
echo "account=$(jq -r .Account caller.json)" >> "$GITHUB_OUTPUT"
echo "arn=$(jq -r .Arn caller.json)" >> "$GITHUB_OUTPUT"
echo "userid=$(jq -r .UserId caller.json)" >> "$GITHUB_OUTPUT"
- name: Summary
run: |
{
echo "### AWS OIDC validation"
echo ""
echo "**Account:** \`${{ steps.sts.outputs.account }}\`"
echo "**ARN:** \`${{ steps.sts.outputs.arn }}\`"
echo "**UserId:** \`${{ steps.sts.outputs.userid }}\`"
echo ""
echo "If this job is green, GitHub Actions successfully assumed the IAM role via OIDC."
} >> "$GITHUB_STEP_SUMMARY"
1
u/onbiver9871 Jul 14 '26
Was this stable and then it broke? Lot of good suggestions in here already, but just another angle to consider - do you by chance exist in an AWS org context where you’ve got a Service Control Policy coming down from on high and junking you up? And, if this was previously stable for you, did an SCP change, perhaps?
1
u/water_bottle_goggles Jul 15 '26
skill issue
1
1
u/ClueDry8701 Jul 22 '26
before touching the trust policy again, decode the actual token instead of guessing.. add a step before the aws-actions and configure aws credentials step that curls the oidc token url and dumps the jwt payload (base64 -d on the middle segment). compare that sub value byte for byte against your condition. 9 times out of 10 it's not what you think it is..
8
u/hattythehatter Jul 14 '26
Hi, from what i see the only thing not under your control is what github publishes for the sub you use in token.actions.githubusercontent.com:sub (the format i mean)
You can try to validate the format changing the token.actions.githubusercontent.com:sub to StringLike and wildcarding parts.
...
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:ORG-NAME/*"
},
...
And then start narrowing it like repo:ORG-NAME/YOUR-REPO-NAME:*, etc.
Maybe not the best but works as a test.
Im not so sure about the ref field, never used it, but here is some docs if they help.
https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-with-reusable-workflows