Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.


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/repo prefix 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_request events from forks present a sub of repo:<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 for main or production, 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 / roleSubject(s)Source
hungryhub-terraform Terraform CIrepo:hungryhub-team/hungryhub-terraform:ref:refs/heads/main, ...:pull_requestoidc-iam-rule/main.tf
hungryhub-iam Terraform CIrepo:hungryhub-team/hungryhub-iam:ref:refs/heads/main, ...:pull_requestoidc-iam-rule/main.tf
hh-lion deployrepo:hungryhub-team/hh-lion:environment:productionoidc-iam-rule/main.tf
eagle-eye deployrepo:hungryhub-team/eagle-eye:environment:productionoidc-iam-rule/main.tf
hunger-games deployrepo:hungryhub-team/hunger-games:environment:productionoidc-iam-rule/main.tf
relay-eval-ci (genai)repo:hungryhub-team/hh-relay:ref:refs/heads/mainhungry-hub-iam/stacks/relay-ci/main.tf
github-actions-prod (secrets)repo:hungryhub-team/<repo>:environment:productionhungry-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.pytest_github_oidc_subject_patterns_are_pinned scans every *.tf for "repo:...:*" and fails on any match.
  • hungryhub-terraform/scripts/test_oidc_subject_pinning.py — the mirror. test_oidc_subjects_do_not_use_wildcard_colon_star catches :*; test_oidc_subjects_only_reference_org_slash_repo catches malformed org/repo prefixes (the hungry-hub-iam typo 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 messageCauseFix
OIDC subjects must not end with ':*'A trust subject uses the :* wildcardReplace with ref:refs/heads/main, pull_request, or environment:production
malformed sub '...' (expected 'repo:<org>/<repo>:<suffix>')Fewer than 3 colon-separated partsThe subject is missing the event suffix
<org>/<repo> must be 'org/repo'The prefix isn’t repo:org/repoUsually a typo in the repo name (e.g. hungry-hub-iam vs hungryhub-iam)