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. Simple thesis, 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.
The tips
-
Register the OIDC provider once per account, and drop the thumbprint. You add
token.actions.githubusercontent.comas 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
-
The first thing that breaks is a missing
id-token: write.Could not assume role with OIDC: Not authorized to perform sts:AssumeRoleWithWebIdentityis the single most-reported failure on theconfigure-aws-credentialsrepo (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
Pin the audience to
sts.amazonaws.com. The official action requestsaud: sts.amazonaws.com, so your trust policy asserts it withStringEquals. 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.Pin the
subclaim to a branch or environment, never a wildcard.StringLikewithrepo:octo-org/octo-repo:*hands the role to every branch, every PR, and every fork's merge ref. For anything that touches production, useStringEqualson an exactsub. The formats you actually need:ref:refs/heads/mainfor a branch,ref:refs/tags/v1.2.3for a tag,environment:prodfor a deployment environment,pull_requestfor 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"
}
}
Prepare for the July 15, 2026 immutable subject-claim change now. Per GitHub's April 23, 2026 changelog, the default
subclaim is gaining immutable owner and repo IDs. Any repository created, renamed, or transferred after July 15, 2026 will mint asubshaped likerepo: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 yoursubconditions are exact-match on names, audit them before the deadline. This is exactly the gotcha a model trained before April 2026 gets wrong.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-basedsubstill satisfies your trust policy. GitHub exposes a stable numericrepository_idthat never gets recycled. Add it as a second condition: identity lives in the immutable ID, branch scoping stays insub.
"StringEquals": {
"token.actions.githubusercontent.com:repository_id": "456789",
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
}
-
Pin the action to a full 40-character commit SHA.
configure-aws-credentialsis at v6.1.0 as of this writing (v5.1.1 shipped 2025-11-24). A floating@v4tag 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.
- 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 }}
Set
role-session-nameso 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 shortrole-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.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
AdministratorAccessto a deploy role, ever. Scope it to the exact actions and resource ARNs the job touches, and keep separate roles forprodandstagingso a staging workflow can never reach production. Gate the prod role behind a GitHub Environment with required reviewers, then pin itssubtoenvironment:prod.When it fails, decode the token instead of guessing. The
AccessDeniedmessage 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 entire point. A credential that does not exist cannot leak.
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
Originally published at indragustiprasetya.com
Top comments (0)