Which OIDC claim mapping strategy prevents privilege escalation in multi-tenant GitHub organizations?
0 reputation · 07 Aug 2024, 01:39 UTC
0 reputation · 07 Aug 2024, 01:39 UTC
Implementing least-privilege authentication in GitHub Actions via OpenID Connect (OIDC) allows workflows to exchange short-lived tokens for cloud IAM roles. This removes the need for long-lived secrets but introduces complexity in trust relationship configuration.
In a multi-tenant organization with numerous repositories, a broad trust policy based only on the organization claim can allow any repository in the org to assume a sensitive role. Conversely, mapping every single repository to a unique role creates significant administrative overhead.
When using the permissions key to restrict the GITHUB_TOKEN, the cloud provider still relies on the OIDC token claims to verify identity. There is a design tension between maintaining granular security and ensuring scalable role management.
sub claim to ensure only specific branches or environments can assume a role?35275 reputation · 07 Aug 2024, 03:04 UTC
The privilege-escalation safe mapping is to constrain the cloud trust policy on the full token.actions.githubusercontent.com:sub claim with StringEquals or StringLike, and never rely on organization or repository_owner alone. For branch or environment gating, include ref: or environment: in the sub pattern.
sub claim. Current format is repo:<owner>/<repo>:ref:<ref> for branch/tag runs and repo:<owner>/<repo>:environment:<name> when an environment is used.token.actions.githubusercontent.com.ref claim e.g. refs/heads/main and the environment claim are present in the token and can be used as additional condition keys.organization or repository_owner allows any repository in the org to assume the role.GitHub does not provide a native repository group object for OIDC trust. Logical grouping is achieved by crafting a StringLike condition on the sub prefix, e.g. repo:myorg/team-*. This avoids creating a separate IAM role per repository while still restricting the set of repositories that can assume the role. Wildcards can unintentionally match future repositories, so explicit prefixes are preferred for sensitive roles.
permissions:
id-token: write
contents: read
- name: Dump OIDC claims
run: |
echo "$ACTIONS_ID_TOKEN_REQUEST_TOKEN" | ... # use actions/github-script or jwt.io locally
Print sub, ref, environment, repository_owner from a test run.
sub, not org alone.Example condition pattern for a specific repo and branch:
Condition:
StringEquals:
token.actions.githubusercontent.com:sub: "repo:myorg/myrepo:ref:refs/heads/main"
Environment gated:
StringEquals:
token.actions.githubusercontent.com:sub: "repo:myorg/myrepo:environment:prod"
StringLike:
token.actions.githubusercontent.com:sub: "repo:myorg/team-*:ref:refs/heads/main"
sub value before deploying.repository_owner or organization without a sub constraint.One diagnostic detail that changes the recommendation: are you using reusable workflows? The sub claim reflects the calling repository, not the reusable workflow repository, so the trust policy must target the caller pattern.
Use comments to ask for clarification. Post a solution as an answer.
35,275 reputation · 07 Aug 2024, 07:05 UTC
One detail worth flagging: when a job calls a reusable workflow, the sub claim reflects the caller repository, not the repository that hosts the reusable workflow. In a multi-tenant org, this means a trust policy scoped to a shared workflow repository will not match the sub value; you must enumerate the caller repositories or use repository_owner plus the immutable repository_id claim where your cloud provider supports it. This avoids both wildcard StringLike patterns that grant unintended future repositories and the operational cost of parsing sub strings. If you rely on environment protection rules, remember the cloud provider only evaluates the claims in the issued token—GitHub enforces protection before issuance, but a misconfigured branch or environment rule does not invalidate an already-issued token. Verify the exact claim set by decoding the token in a test workflow and exercising the policy in your provider's simulator.