GitHub Actions OIDC to AWS: Not authorized to perform sts:AssumeRoleWithWebIdentity
Our deploy job worked on main and failed on the production environment with Not authorized to perform sts:AssumeRoleWithWebIdentity. Using a GitHub environment changes the OIDC subject claim. We rewrote the trust policy around exact subjects instead of wildcards.
We moved our deploy pipelines from long-lived AWS access keys to GitHub's OIDC provider. Staging deploys from main worked on the first try. The production job, which uses a GitHub environment with required reviewers, failed right after approval:
Run aws-actions/configure-aws-credentials@v4
Assuming role with OIDC
Error: Could not assume role with OIDC: Not authorized to perform sts:AssumeRoleWithWebIdentity
Same repository, same branch, same role ARN pattern. Only the job with environment: production failed.
Reproducing it
We added a step that prints the claims of the OIDC token, without the signature, to see what AWS was being asked to trust:
- name: Show OIDC claims
run: |
TOKEN=$(curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com" | jq -r .value)
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq '{sub, aud, ref, environment}'
Staging job:
{"sub": "repo:example-org/shop-api:ref:refs/heads/main", "aud": "sts.amazonaws.com", "ref": "refs/heads/main", "environment": null}
Production job:
{"sub": "repo:example-org/shop-api:environment:production", "aud": "sts.amazonaws.com", "ref": "refs/heads/main", "environment": "production"}
What we ruled out
- Missing permission to request a token. The workflow already had
id-token: write; without it the action fails earlier with a different message about the token request. - Wrong OIDC provider thumbprint or audience. Staging used the same provider and audience successfully.
- Role ARN typo. We copied the ARN from the console; CloudTrail showed the call reaching the right role.
Root cause
The role's trust policy allowed sub equal to repo:example-org/shop-api:ref:refs/heads/main. When a job references a GitHub environment, the sub claim uses the environment form instead of the ref form. The production job's token had sub: repo:example-org/shop-api:environment:production, which did not match, so STS denied the request.
What is happening under the hood
GitHub issues a signed JWT for each job. AWS validates the signature against the registered OIDC provider and then evaluates the role's trust policy against the token's claims. The sub claim format depends on the trigger and context:
| Context | sub claim |
|---|---|
| Job with an environment | repo:ORG/REPO:environment:NAME |
| Push to a branch, no environment | repo:ORG/REPO:ref:refs/heads/BRANCH |
| Tag push, no environment | repo:ORG/REPO:ref:refs/tags/TAG |
| Pull request | repo:ORG/REPO:pull_request |
The aud claim is sts.amazonaws.com when the AWS action requests the token. Organisations can also customise the subject claim template, which changes these formats again.
CloudTrail logs the AssumeRoleWithWebIdentity call with the failure, which is the quickest way to confirm a trust policy problem from the AWS side.
Environment
| Component | Version / value |
|---|---|
| CI | GitHub Actions, GitHub-hosted Ubuntu runners |
| Action | aws-actions/configure-aws-credentials@v4 |
| AWS | IAM OIDC provider token.actions.githubusercontent.com |
| IaC | Terraform 1.9, AWS provider 5.70 |
| Workflow | deploy.yml with staging (no environment) and production (environment with reviewers) jobs |
Minimal reproduction
{
"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:example-org/shop-api:ref:refs/heads/main"
}
}
}
Any job in this repo with environment: set will be denied.
The fix
We use one role per environment and pin each role to the exact subject it should accept. The production role trusts only the production environment; the staging role trusts only main without an environment.
data "aws_iam_policy_document" "deploy_prod_trust" {
statement {
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [aws_iam_openid_connect_provider.github.arn]
}
condition {
test = "StringEquals"
variable = "token.actions.githubusercontent.com:aud"
values = ["sts.amazonaws.com"]
}
condition {
test = "StringEquals"
variable = "token.actions.githubusercontent.com:sub"
values = ["repo:example-org/shop-api:environment:production"]
}
}
}
resource "aws_iam_role" "deploy_prod" {
name = "gha-shop-api-deploy-prod"
assume_role_policy = data.aws_iam_policy_document.deploy_prod_trust.json
max_session_duration = 3600
}
In GitHub, the production environment is restricted to the main branch through deployment branch rules and requires a reviewer. That combination means only reviewed runs from main can get production credentials.
The workflow:
permissions:
id-token: write
contents: read
jobs:
deploy-prod:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::111122223333:role/gha-shop-api-deploy-prod
aws-region: eu-west-1
- run: ./scripts/deploy.sh
Alternatives and trade-offs
- Wildcard subject
repo:example-org/shop-api:*. Makes the error go away and lets any workflow in the repo, including pull request workflows, assume the role. We do not allow wildcards on deploy roles. StringLikewith a list of exact subjects. Fine when one role must serve several environments; still list them explicitly.- Customised subject claims that include the workflow file or job. Tighter, but every role must be updated to match the new format.
How to test the fix
We ran the workflow three ways: production job approved from main (succeeds), the same job triggered from a feature branch (blocked by the environment branch rule before it even requests credentials), and a pull request workflow that tries to assume the production role (denied by STS, visible in CloudTrail):
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity \
--max-results 5 --query 'Events[].CloudTrailEvent' --output text | jq '{errorCode, userIdentity}'
Confirming in production
- Production deploys have assumed the role successfully for every approved run since the change.
- CloudTrail shows
AssumeRoleWithWebIdentityevents with the environment subject and no unexpected principals. - The old IAM user with access keys was deactivated, then deleted after 30 days with no use.
Regression guard
- A policy check in CI (we use a small OPA rule against
terraform planJSON) that fails if any OIDC trust policy uses asubwildcard on a role with write permissions. - An alert on
AssumeRoleWithWebIdentityfailures for deploy roles, because repeated denials can also mean someone is probing.
Security implications
This change is a security improvement overall: no static keys, short-lived credentials, and credentials bound to a reviewed environment. The risk is in how the error tempts you to fix it. A broad sub wildcard would let a pull request from a contributor run code with production credentials if the workflow is misconfigured. Pin exact subjects and pair them with environment protection rules.
Prevention checklist
- Print the token claims once when setting up a new workflow context.
- One role per environment, exact
submatch,audcheck. - Protect environments with branch rules and reviewers.
- No wildcards in trust policies for roles that can change infrastructure.
- Watch CloudTrail for denied web identity calls.
References
- GitHub, configuring OpenID Connect in AWS — https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services
- GitHub OIDC token claims and subject examples — https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect
- configure-aws-credentials action — https://github.com/aws-actions/configure-aws-credentials
- AWS IAM, creating a role for OIDC federation — https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_oidc.html
Add a label
Embed this post
<iframe src="https://hibug.me/github-actions-oidc-aws-assumerolewithwebidentity-denied" width="700" height="420" frameborder="0" loading="lazy"></iframe>
I build and run the platforms other engineers ship on: CI/CD pipelines, Kubernetes clusters, GPU inference services, LLM features in production, and the cloud bills that come with all of it. I have also spent years writing and operating automated trading systems, where a rounding bug or a missed websocket message is not a log line but a position you did not intend to hold. Each post here starts from a real failure: the alert, the exact error, what we ruled out, the root cause, and how we proved the fix. Same path every time: Investigate, Explain, Fix, Verify. No hype, no price predictions, just engineering.
Comments
0No comments yet. Be the first to comment!
Sign In to post a comment.