GitHub’s OpenID Connect provider is the right way to give Actions workflows short-lived AWS credentials. Used correctly, it removes long-lived access keys from your CI surface entirely. Used incorrectly, it hands every repository in your GitHub organization the ability to assume a production role.
Key takeaway
The trust policy’s Condition block is where OIDC scoping lives. If your condition only checks aud, your role is effectively open to every workflow on GitHub.com that targets your AWS account ID.
The minimum viable trust policy
Here is the shape of a correctly-scoped trust policy. Read past it; the details below are what most teams get wrong.
{
"Version": "2012-10-17",
"Statement": [{
"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"
},
"StringLike": {
"token.actions.githubusercontent.com:sub":
"repo:acme-org/payments-api:ref:refs/heads/main"
}
}
}]
}
What the subject claim actually contains
GitHub’s OIDC subject (sub) claim encodes four things: the repo owner, the repo name, the ref type (branch, tag, pull_request, environment), and the ref value. The format is:
repo:<owner>/<repo>:<ref-type>:<ref>
So repo:acme-org/payments-api:ref:refs/heads/main means “workflows running on the main branch of acme-org/payments-api.” Narrow that further with environment claims if you want production-only scoping:
repo:acme-org/payments-api:environment:production
The four mistakes we see in every audit
1. Missing the sub condition entirely
Critical — seen in ~35% of audits
Trust policy has aud: sts.amazonaws.com but no sub condition. Every workflow on every repository on GitHub.com that writes to your account can assume the role. The mitigation is one JSON key.
If you see a trust policy that only checks aud, fix it before you finish reading this post. There is no scenario in which that is correct.
2. Wildcarding the org
"token.actions.githubusercontent.com:sub": "repo:acme-org/*"
Technically scoped, practically dangerous. Every repo in the org can assume the role, including forks of a public repo that land in the organization, dependabot branches, and any new repo created by any org member. On a role with write access to production, this is a breach waiting for a motivated insider or a compromised contributor account.
3. Using StringEquals with a list of branches
"StringEquals": {
"token.actions.githubusercontent.com:sub": [
"repo:acme-org/payments-api:ref:refs/heads/main",
"repo:acme-org/payments-api:ref:refs/heads/release/*"
]
}
StringEquals does not evaluate wildcards. The second entry matches a branch literally named release/*, not branches under release/. Use StringLike whenever any entry contains a wildcard, or split the policy into multiple statements.
4. Trusting PR workflows with production permissions
repo:acme-org/payments-api:pull_request
Pull-request workflows run with the token of the PR author. Anyone who can open a PR — including external contributors on public repos — can run a workflow under this trust. If the role has any production write, you have granted unreviewed code in a fork the ability to touch production. Never condition production trust on pull_request.
How to verify your policies are tight
Run IAM Access Analyzer with external-access findings enabled, then filter to Federated principals. Any role with an OIDC federation and a sub that matches more than one repo/branch pair is a candidate for narrowing.
On the GitHub side, every production deploy should use a protected environment — not a branch filter. Environments enforce approval rules and they encode cleanly into the sub claim.
What to change today
- Audit every role with
Federatedprincipal pointing attoken.actions.githubusercontent.com. - Reject any trust policy without a
subcondition. - Replace wildcard
subpatterns with branch-specific or environment-specific claims. - Move production deploys to GitHub environments and condition the trust on
environment:production.
The fix takes a morning. The exposure is measured in every secret your CI role can read, for as long as the policy stays open.