Technical briefing

Hardening GitHub Actions OIDC trust policies on AWS

A practical walkthrough of the subject-condition patterns that actually scope OIDC trust correctly — and the four mistakes we find in nearly every audit.

TZ

Taha Zubair

Founder, Cloud Upload · · 3 min

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 Federated principal pointing at token.actions.githubusercontent.com.
  • Reject any trust policy without a sub condition.
  • Replace wildcard sub patterns 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.


Last updated April 5, 2026 ← All briefings