OIDC Subject Pinning Patterns
How to write GitHub Actions OIDC trust subjects that are safe, and why the
unpinned form is a footgun. Every IAM role in hungry-hub-iam,
hungryhub-terraform, and hungryhub-ai that GitHub Actions can assume must
follow these rules.
- Repos: hungryhub-iam, hungryhub-terraform, hungryhub-ai
- Rule source:
AGENTS.md§3.5–3.7 (hungry-hub-iam), §3.6 (hungryhub-terraform) - Canonical incident: OIDC Subject Typo Incident (2026-05-20)
The subject claim
When a GitHub Actions workflow assumes an AWS role via OIDC, GitHub presents a
token whose sub claim is:
repo:<org>/<repo>:<suffix>
The role’s trust policy uses a StringLike (or StringEquals) condition on
token.actions.githubusercontent.com:sub to decide which workflows may assume
it. The <suffix> is the part that says which event — a branch, a pull
request, or a deployment environment.
The two rules
1. Never use the :* wildcard suffix
# BAD — any branch, any tag, any fork's PR can assume this role
"repo:hungryhub-team/hungryhub-iam:*"
Pin to a specific event instead:
# GOOD — only the main branch
"repo:hungryhub-team/hungryhub-iam:ref:refs/heads/main"
# GOOD — only pull requests targeting this repo
"repo:hungryhub-team/hungryhub-iam:pull_request"
# GOOD — only the `production` GitHub Environment (forces the approval gate)
"repo:hungryhub-team/hungryhub-iam:environment:production"
2. The shape must be repo:<org>/<repo>:<suffix> with the exact repo name
The <repo> slug must match the GitHub repository name exactly. The
repository is named hungryhub-iam (no hyphen between hungry and hub),
even though the local checkout directory is hungry-hub-iam. A subject that
says repo:hungryhub-team/hungry-hub-iam:... never matches anything — GitHub
will never mint a token with that sub, so the role becomes silently
unusable. That exact typo caused the 2026-05-20 incident.
Why repo:org/repo:* is a footgun
- Typos survive. A wildcard accepts any suffix, so a malformed
org/repoprefix isn’t caught by “it stopped working” — the role either over-grants or silently never matches, and nobody notices until later. - Forks inherit trust.
pull_requestevents from forks present asubofrepo:<org>/<repo>:pull_request. A:*wildcard accepts every event type, widening the surface to any workflow trigger on the repo. - Any branch can deploy.
:*lets a throwaway branch assume a role that was only ever meant formainorproduction, bypassing the review and environment-approval gates.
The fix is to make the allowed events explicit so the trust policy is a PR-reviewable list, not a catch-all.
The pinned patterns in production today
Every GitHub Actions OIDC role across the three repos, with its real subject:
| Repo / role | Subject(s) | Source |
|---|---|---|
hungryhub-terraform Terraform CI | repo:hungryhub-team/hungryhub-terraform:ref:refs/heads/main, ...:pull_request | oidc-iam-rule/main.tf |
hungryhub-iam Terraform CI | repo:hungryhub-team/hungryhub-iam:ref:refs/heads/main, ...:pull_request | oidc-iam-rule/main.tf |
hh-lion deploy | repo:hungryhub-team/hh-lion:environment:production | oidc-iam-rule/main.tf |
eagle-eye deploy | repo:hungryhub-team/eagle-eye:environment:production | oidc-iam-rule/main.tf |
hunger-games deploy | repo:hungryhub-team/hunger-games:environment:production | oidc-iam-rule/main.tf |
relay-eval-ci (genai) | repo:hungryhub-team/hh-relay:ref:refs/heads/main | hungry-hub-iam/stacks/relay-ci/main.tf |
github-actions-prod (secrets) | repo:hungryhub-team/<repo>:environment:production | hungry-hub-iam/stacks/secrets-manager-access/main.tf |
Note the split: Terraform CI roles trust main + pull_request (plan on
PRs, apply on main), while deploy roles trust only
environment:production, which forces the GitHub-managed manual approval gate
before any deploy.
The guardrail test
Two mirror tests fail loudly if anyone reintroduces a :* wildcard or a
malformed org/repo prefix:
hungry-hub-iam/tests/test_iam_guardrails.py—test_github_oidc_subject_patterns_are_pinnedscans every*.tffor"repo:...:*"and fails on any match.hungryhub-terraform/scripts/test_oidc_subject_pinning.py— the mirror.test_oidc_subjects_do_not_use_wildcard_colon_starcatches:*;test_oidc_subjects_only_reference_org_slash_repocatches malformedorg/repoprefixes (thehungry-hub-iamtypo class).
Run them before opening a PR that touches any trust block:
# hungry-hub-iam
python -m unittest tests.test_iam_guardrails
# hungryhub-terraform
python scripts/test_oidc_subject_pinning.py
What a failure means
| Failure message | Cause | Fix |
|---|---|---|
OIDC subjects must not end with ':*' | A trust subject uses the :* wildcard | Replace with ref:refs/heads/main, pull_request, or environment:production |
malformed sub '...' (expected 'repo:<org>/<repo>:<suffix>') | Fewer than 3 colon-separated parts | The subject is missing the event suffix |
<org>/<repo> must be 'org/repo' | The prefix isn’t repo:org/repo | Usually a typo in the repo name (e.g. hungry-hub-iam vs hungryhub-iam) |
Related
- OIDC Subject Typo Incident — the 2026-05-20 case study
- Relay CI Stack Pattern — the
relay-eval-cirole in detail - hungryhub-iam — Architecture and Operations
- hungryhub-terraform — Architecture and Operations