GitHub Actions OIDC Trust Policy Checker

Paste an AWS IAM role trust policy and find out whether its conditions actually restrict which repository, branch or environment can assume the role. The common failures are quiet ones: a policy that pins aud and leaves sub unconstrained is assumable by any repository on GitHub; repo:my-org/my-repo:* admits the pull_request context and therefore a fork's pull request; and a * under StringEquals is a literal character, so the role cannot be assumed at all. Each of those looks approximately correct in a policy review.

Nothing is uploaded. Your policy is parsed here, in your browser. That matters more than usual for this input: a trust policy carries your AWS account id, your role name and your repository names.

Already run Wiz or a comparable CNAPP across your cloud account? See this checker vs. Wiz for when each is the right one to reach for. Just reading the policy JSON yourself? See this checker vs. reading the trust policy by eye for what a manual read catches and what it misses.

Paste your trust policy

In the console: IAM → Roles → your role → Trust relationships. Or on the command line — this is the whole document, and the command needs only read access:

aws iam get-role --role-name my-deploy-role --query Role.AssumeRolePolicyDocument

Paste the trust policy above and click Check.

Run it in CI instead

The same classifier ships as a zero-dependency Node.js CLI, so a pull request that widens a trust policy can fail a check rather than being noticed later. It makes no network request and needs no AWS credentials — it reads a file and exits, so it runs offline:

node check.mjs trust-policy.json
# or straight from the AWS CLI:
aws iam get-role --role-name my-deploy-role \
  --query Role.AssumeRolePolicyDocument | node check.mjs -

Exit code 0 when no danger-level finding is present, 1 when there is one — so it works as a gate as it stands. Get check.mjs and analyze.mjs from the repo.

What the sub claim looks like

Every pattern this tool reasons about comes from the shape of GitHub's sub claim. In its default form it is repo:<owner>/<repo>:<context>, and the context is the part that decides whether a stranger's pull request is included:

ref:refs/heads/maina push or workflow run on that branch
ref:refs/tags/v1.2.3a workflow run for that tag
environment:productiona job targeting that environment — the one context that can carry required reviewers
pull_requesta workflow triggered by a pull request, including one opened from a fork

An organisation can customise which claims the issuer includes, in which case a policy written against it will legitimately not match this shape — the report says so rather than guessing. The longer walkthrough of these traps works through each one with the policy that produces it.

Common questions

My trust policy pins the aud claim to sts.amazonaws.com. Isn't that enough?

No, and this is the most expensive misreading of this policy shape. Every AWS and GitHub Actions OIDC integration in the world uses the audience sts.amazonaws.com — it identifies which service the token is for, not which repository issued it. A policy that pins aud and leaves sub unconstrained is assumable by a GitHub Actions workflow in any repository on GitHub. Pinning aud is necessary and it is not isolation; the sub claim is the only condition that says which repository.

What is wrong with sub set to repo:my-org/my-repo:*?

It pins the repository but not the context, and GitHub's sub claim carries the context in that final segment. So the pattern matches every branch, every tag, every environment — and repo:my-org/my-repo:pull_request, which GitHub issues to workflows triggered by a pull request. On a public repository, or any repository that accepts forks, that means someone with no write access can open a pull request and have its workflow assume your role. Pin a ref (repo:owner/name:ref:refs/heads/main) or an environment instead.

Why does a wildcard under StringEquals show up as a notice rather than a danger?

Because IAM only treats * and ? as wildcards under the StringLike family of operators. Under StringEquals they are ordinary characters, so a condition of StringEquals sub = repo:org/name:* is asking for a sub claim whose last character is literally an asterisk. No GitHub token ever carries that, so the condition never matches and the role cannot be assumed at all. That fails closed: it is a broken deployment, not an exposure, and a checker that reported it as an open wildcard would send you to fix the opposite of the real problem.

I listed four pinned branches in my sub condition. Why is it still flagged?

IAM grants access when any one value in the list matches, so a multi-value sub condition is exactly as permissive as its widest entry. Four tightly pinned branches plus one repo:org/name:* is as open as the wildcard alone, and reading the list from the top finds the four good rows first. This tool reports which entry actually governs rather than the first one it sees.

Does this send my trust policy anywhere?

No. The page parses and classifies the policy in your own browser using JavaScript loaded from this site, and makes no network request with the content. That matters here more than usual: a trust policy contains your AWS account id, your role name, and the names of your repositories. The Node.js CLI is the same code and also makes no network request of any kind — it reads a file and exits, so it needs no AWS credentials and works offline.

Does a clean report mean my OIDC setup is secure?

No. This reads the trust policy only — the door, not the room behind it. It cannot see what permissions the role grants once assumed, who can push to the branch you pinned, whether a protected environment actually has reviewers configured, or whether your organisation has customised the OIDC subject claim. A role correctly pinned to a branch that any contributor can push to is still reachable by any contributor. Treat a clean report as evidence about these specific conditions, not a determination that the setup is secure.