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.
Register the identity provider
Section titled “Register the identity provider”One per AWS account, shared by every role that trusts GitHub:
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.
The trust policy
Section titled “The trust policy”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.
Matching the sub you actually get
Section titled “Matching the sub you actually get”The sub claim’s shape depends on the trigger, so write the condition against the value your workflow
really produces:
| Job | sub |
|---|---|
Push to main | repo:OWNER/REPO:ref:refs/heads/main |
Push of tag v1.0.0 | repo:OWNER/REPO:ref:refs/tags/v1.0.0 |
| Pull request | repo:OWNER/REPO:pull_request |
Job with environment: production | repo: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.
Using it
Section titled “Using it”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-identityrole-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.
Session duration
Section titled “Session duration” with: role-duration-seconds: 900Fifteen 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.
Permissions on the role
Section titled “Permissions on the role”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": "*"ons3:*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.
Diagnosing failures
Section titled “Diagnosing failures”configure-aws-credentials failures are terse. The common causes, in the order worth checking:
| Symptom | Usual cause |
|---|---|
Not authorized to perform sts:AssumeRoleWithWebIdentity | The sub condition does not match the actual claim |
| Same, after adding an environment | sub changed to the environment: form |
Credentials could not be loaded | id-token: write missing from permissions |
InvalidIdentityToken | Audience mismatch between the provider and the request |
Works on main, fails on a tag | Condition pinned to refs/heads/main |
| Works for you, fails for a fork PR | Correct — 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.
Multi-account setups
Section titled “Multi-account setups”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.
Exercise
Section titled “Exercise”-
Create the identity provider in a test account.
-
Create a role whose trust policy uses
StringEqualsonrepo:OWNER/REPO:ref:refs/heads/main, with a permission policy allowing onlys3:ListBucketon one bucket. -
Run a workflow on
mainthat assumes it and callsaws sts get-caller-identity. Confirm success. -
Run the same workflow from a branch. Confirm it is refused, and read the error.
-
Add
environment: productionto the job and run onmainagain. Confirm it now fails — thesubchanged. Fix the condition to the environment form. -
Find the successful
AssumeRoleWithWebIdentityevent in CloudTrail and confirm yourrole-session-nameis on it.