Skip to content

AWS OIDC with GitHub Actions: Trust Policy Guide

Lesson 2 of 10Advanced4 min readGitHub Actions & CI/CD · Actions SecurityVerified: aws-actions/configure-aws-credentials v6, IAM OIDC federation, August 2026

OIDC explained the architecture. This page writes the AWS half of it: the identity provider, the role, and — the part that matters — the conditions that decide which workflows may assume it.

One per AWS account, shared by every role that trusts GitHub:

Terminal window
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com

--client-id-list is the audience, and sts.amazonaws.com is what aws-actions/configure-aws-credentials requests. A mismatch here produces an authentication failure rather than a permissions error.

Modern AWS validates GitHub’s certificate against its own trust store, so a manually supplied thumbprint is no longer required. Older guidance telling you to paste a specific thumbprint is stale; worse, a hardcoded thumbprint breaks when the certificate rotates.

This is the security boundary. Everything else on this page is detail.

{
"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:OWNER/REPO:ref:refs/heads/main"
}
}
}]
}

StringEquals on sub with an exact value is the strong form: only pushes to main in that one repository can assume this role.

The sub claim’s shape depends on the trigger, so write the condition against the value your workflow really produces:

Jobsub
Push to mainrepo:OWNER/REPO:ref:refs/heads/main
Push of tag v1.0.0repo:OWNER/REPO:ref:refs/tags/v1.0.0
Pull requestrepo:OWNER/REPO:pull_request
Job with environment: productionrepo:OWNER/REPO:environment:production

The environment form is the one to prefer for deployments, because it composes with environment protection rules: the role can only be assumed by a job that declared the environment, and that job cannot start until required reviewers approve. A branch condition alone has no such gate.

For several release tags, StringLike with a narrow pattern is reasonable:

"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:OWNER/REPO:ref:refs/tags/v*"
}

For several environments, prefer a list of exact values over a wildcard:

"StringEquals": {
"token.actions.githubusercontent.com:sub": [
"repo:OWNER/REPO:environment:staging",
"repo:OWNER/REPO:environment:production"
]
}

…though separate roles for staging and production is better still, because it lets their permission policies differ too.

permissions:
contents: read
id-token: write
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-deploy-production
aws-region: eu-west-1
role-session-name: gha-${{ github.run_id }}
- run: aws sts get-caller-identity

role-session-name appears in CloudTrail against every API call the session makes. Setting it to something traceable — the run ID — turns “who deleted that bucket” into a link to a specific workflow run. It costs nothing and is worth doing every time.

with:
role-duration-seconds: 900

Fifteen minutes is a sensible default for a deployment. The maximum is the role’s own MaxSessionDuration, and requesting more fails. Short sessions limit the window in which leaked credentials are useful; if a job legitimately runs longer than the session, the SDK refreshes automatically for most operations.

The trust policy decides who. The permission policy decides what, and it is a separate document that is easy to leave too broad:

  • Scope to specific resource ARNs. "Resource": "*" on s3:* is access to every bucket in the account.
  • Separate plan and apply roles for infrastructure, as in Terraform CI — read-only for pull requests, write for post-merge.
  • Separate roles per environment, each with its own trust condition.
  • Avoid iam:*. A role that can modify IAM can grant itself anything, which makes every other restriction decorative.

configure-aws-credentials failures are terse. The common causes, in the order worth checking:

SymptomUsual cause
Not authorized to perform sts:AssumeRoleWithWebIdentityThe sub condition does not match the actual claim
Same, after adding an environmentsub changed to the environment: form
Credentials could not be loadedid-token: write missing from permissions
InvalidIdentityTokenAudience mismatch between the provider and the request
Works on main, fails on a tagCondition pinned to refs/heads/main
Works for you, fails for a fork PRCorrect — fork pull requests should not reach production

The fastest diagnostic is to print the actual claims with the inspection step in OIDC and compare the sub character by character against the policy. Nearly every failure is a mismatch you can see once both strings are in front of you.

CloudTrail records failed AssumeRoleWithWebIdentity calls with the reason, which is the authoritative answer when the claim looks right and it still fails.

For an organisation with separate accounts per environment, register the identity provider in each account and create a role in each, with a trust condition naming the matching environment. Do not create one role in a management account that can assume into the others — that reintroduces a single identity with access everywhere, which is the thing you were removing.

  1. Create the identity provider in a test account.

  2. Create a role whose trust policy uses StringEquals on repo:OWNER/REPO:ref:refs/heads/main, with a permission policy allowing only s3:ListBucket on one bucket.

  3. Run a workflow on main that assumes it and calls aws sts get-caller-identity. Confirm success.

  4. Run the same workflow from a branch. Confirm it is refused, and read the error.

  5. Add environment: production to the job and run on main again. Confirm it now fails — the sub changed. Fix the condition to the environment form.

  6. Find the successful AssumeRoleWithWebIdentity event in CloudTrail and confirm your role-session-name is on it.

GitHub Actions Security ChecklistAudit your workflows against the failure modes that actually cause incidents. Free and complete.

Get the production security checklists and Actions hardening templates from the Professional Toolkit.