An AWS_SECRET_ACCESS_KEY living in GitHub Actions secrets is a long-lived credential that never rotates, gets exfiltrated by one poisoned dependency, and shows up in nobody's audit trail. OIDC federation deletes it. GitHub mints a short-lived JWT per run, AWS STS trades that token for temporary credentials, and there is no static key left to steal, a simple thesis with sharp edges.
One of those edges changes on July 15, 2026, and a trust policy that looks correct today will quietly stop matching. These tips are for the engineer wiring this into a real repo who wants it scoped right the first time, not lifted from a tutorial that grants repo:org/* and calls the job done.
TL;DR: Registertoken.actions.githubusercontent.comas an IAM OIDC provider once per account (no thumbprint needed), grant the jobid-token: write, and write a trust policy that exact-matches bothaudand an identity claim. Make that identity claim the immutable numericrepository_id, not the repo name, so the July 15, 2026 immutable-subject rollout and any future repo rename cannot silently break or widen your access. Keep branch and environment scoping insub, pin the action to a full commit SHA, and droprole-duration-secondsto 900.
How does the GitHub to AWS OIDC handshake actually work?
Every failure below is a failure of one hop in this chain, so it is worth holding the whole shape in your head before you write a single condition.
Two policies do two different jobs here, and conflating them is the root of most over-granted deploy roles. The trust policy answers who may assume the role. The permissions policy answers what they can do once they have.
What do you set up once per AWS account?
1. Register the OIDC provider once per account, and drop the thumbprint. You add token.actions.githubusercontent.com as an IAM OIDC provider once in each AWS account. Per the AWS IAM docs, the thumbprint is now optional: IAM validates GitHub's JWKS endpoint against its own library of trusted root CAs and only falls back to a configured thumbprint when the cert is not signed by a trusted CA. So delete that SHA1 string you copied from a 2022 blog post. It does nothing now.
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com
2. The first thing that breaks is a missing id-token: write. Could not assume role with OIDC: Not authorized to perform sts:AssumeRoleWithWebIdentity is the single most-reported failure on the configure-aws-credentials repo (issues #318, #961, #1137). Half the time the trust policy is fine and GitHub never minted a token at all, because the job lacked permission to request one. Set it at job scope, not just as a workflow default that a matrix job can shadow.
permissions:
id-token: write # required to mint the OIDC token
contents: read
3. Pin the audience to sts.amazonaws.com. The official action requests aud: sts.amazonaws.com, so your trust policy asserts it with StringEquals. GitHub's own guidance is blunt about the stakes: you must define at least one condition, or any repository on GitHub can request a token that assumes your role. The audience is not that protective condition; it is table stakes.
How do you scope the sub claim so one branch, and only one branch, can assume the role?
4. Pin the sub claim to a branch or environment, never a wildcard. StringLike with repo:octo-org/octo-repo:* hands the role to every branch, every PR, and every fork's merge ref. For anything that touches production, use StringEquals on an exact sub. The formats you actually need: ref:refs/heads/main for a branch, ref:refs/tags/v1.2.3 for a tag, environment:prod for a deployment environment, pull_request for PR runs.
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:octo-org/octo-repo:ref:refs/heads/main"
}
}
5. Prepare for the July 15, 2026 immutable subject-claim change now. Per GitHub's April 23, 2026 changelog, the default sub claim is gaining immutable owner and repo IDs. Any repository created, renamed, or transferred after July 15, 2026 will mint a sub shaped like repo:octo-org@123456/octo-repo@456789:ref:refs/heads/main.
A policy pinned to the old name-only string stops matching the moment someone renames the repo, with no error until the deploy fails. If your sub conditions are exact-match on names, audit them before the deadline. This is exactly the gotcha a model trained before April 2026 gets wrong.
6. Pin repository_id, not the name, to kill the rename-squat confused deputy. The reason behind the immutable change is real, and worth understanding rather than just working around. Delete or rename a repo, and someone can register a new repo that reclaims the freed name and mint a token whose name-based sub still satisfies your trust policy.
GitHub exposes a stable numeric repository_id that never gets recycled. Add it as a second condition: identity lives in the immutable ID, branch scoping stays in sub.
"StringEquals": {
"token.actions.githubusercontent.com:repository_id": "456789",
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
}
How do you stop the workflow side from becoming the weak link?
7. Pin the action to a full 40-character commit SHA. configure-aws-credentials is at v6.1.0 as of this writing (v5.1.1 shipped 2025-11-24). A floating @v4 tag can be re-pointed by anyone who compromises the tag, and this action handles your credentials. It is the last place you want a mutable reference.
Pin the SHA, leave the human-readable version in a comment, and do the same for every third-party action in the credential path. It is the same reasoning behind dependency cooldowns: never let a third party silently change what runs in your pipeline.
- uses: aws-actions/configure-aws-credentials@<full-40-char-sha> # v6.1.0
with:
role-to-assume: arn:aws:iam::123456789012:role/github-deploy
aws-region: us-east-1
role-session-name: gha-${{ github.run_id }}
8. Set role-session-name so CloudTrail can tell you which run did what. Skip it, and every assumed session looks identical in CloudTrail. Stamp the run ID or repo into the session name and it becomes an audit trail: an unexpected S3 write traces straight back to one workflow run.
Pair it with a short role-duration-seconds. The default is one hour, but a deploy job that finishes in three minutes has no business holding a token for sixty. Drop it to 900 and a leaked token expires before anyone can use it, the same instinct behind bound ServiceAccount tokens in Kubernetes.
9. One narrow role per repo-and-environment, least privilege on the permissions policy. The trust policy decides who can assume the role. The permissions policy decides what they can do, and this is where most setups quietly over-grant. Do not attach AdministratorAccess to a deploy role, ever.
Scope it to the exact actions and resource ARNs the job touches, and keep separate roles for prod and staging so a staging workflow can never reach production. Gate the prod role behind a GitHub Environment with required reviewers, then pin its sub to environment:prod.
Why does AssumeRoleWithWebIdentity fail with AccessDenied?
10. When it fails, decode the token instead of guessing. The AccessDenied message never tells you which claim mismatched, which is maddening the first time and routine after that. Print the actual JWT payload from inside the failing job and compare it byte for byte against your trust policy conditions.
curl -sH "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com" \
| jq -r '.value' | cut -d. -f2 | base64 -d 2>/dev/null | jq .
Reusable workflows hide a trap here: the sub reflects the calling repo, not the reusable one. If you need to trust the reusable workflow itself, condition on the job_workflow_ref claim instead of sub, or the match never fires and you will stare at the trust policy for an hour wondering why.
Wrap-up
If you do one thing, do this: define at least one exact-match condition on both aud and an identity claim, and make that identity claim the immutable repository_id, not the repo name. That single habit buys you least-privilege scoping today, survives the July 15, 2026 immutable-claim rollout, and closes the rename-squatting hole the change exists to fix.
The static key you delete afterward is the point. A credential that does not exist cannot leak, which is where this connects to the wider problem of governing non-human identities instead of inventorying secrets forever. If you would rather have the trust policies scoped and the static keys retired for you, get in touch and I will do the pass with you.
FAQ
Do I still need the OIDC provider thumbprint for GitHub Actions? No. Per the AWS IAM docs the thumbprint is optional: IAM validates GitHub's JWKS endpoint against its own library of trusted root CAs and only falls back to a configured thumbprint when the certificate is not signed by a trusted CA. Delete the SHA1 string you copied from an old blog post.
Why do I get "Not authorized to perform sts:AssumeRoleWithWebIdentity"? Most often the job never minted a token at all, because it lacked permissions: id-token: write. Set it at job scope, not only as a workflow default a matrix job can shadow. If the permission is there, the failure is a claim mismatch, so decode the JWT payload from inside the failing job and compare it against your trust policy conditions.
What changes on July 15, 2026 for the GitHub Actions sub claim? Per the April 23, 2026 GitHub changelog, the default sub claim gains immutable owner and repo IDs. Any repository created, renamed, or transferred after July 15, 2026 mints a sub shaped like repo:octo-org@123456/octo-repo@456789:ref:refs/heads/main, so a trust policy doing exact match on the old name-only string stops matching with no error until the deploy fails.
Should I pin the trust policy to the repo name or to repository_id? Put identity in the immutable numeric repository_id, which is never recycled, and keep branch or environment scoping in sub. Name-only pinning is what makes rename-squatting possible: delete or rename a repo and someone else can claim the freed name and still satisfy your policy.
Why does my reusable workflow never match the trust policy? Because sub reflects the calling repository, not the reusable one. If you need to trust the reusable workflow itself, condition on the job_workflow_ref claim instead of sub.
Sources
- https://docs.github.com/actions/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services
- https://github.blog/changelog/2026-04-23-immutable-subject-claims-for-github-actions-oidc-tokens/
- https://aws.amazon.com/blogs/security/use-iam-roles-to-connect-github-actions-to-actions-in-aws/
- https://github.com/aws-actions/configure-aws-credentials
- https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc.html

Comments
Be the first to comment.