Skip to content

PATTERN Cited by 1 source

Sub-field scoping for role trust

Pattern

Structure your OIDC IdP's sub claim as a hierarchy of identity components separated by a delimiter (e.g. <org>:<app>:<machine>, or repo:<owner>/<name>:ref:<branch> in GitHub Actions), so that cloud trust policies can scope Role assumption along any prefix of the hierarchy using string-match conditions like StringLike.

Canonical example, from Fly.io:

"StringLike": {
  "oidc.fly.io/example:sub": "example:weather-cat:*"
}

From the post:

Every OIDC token Fly.io generates is going to have a sub field formatted org:app:machine, so we can lock IAM Roles down to organizations, or to specific Fly Apps, or even specific Fly Machine instances. (Source: sources/2024-06-19-flyio-aws-without-access-keys)

Why structure the sub

The OIDC spec allows sub to be any per-user-unique string. A Fly.io machine could have sub = "ancient-snow-4824" and that would satisfy the protocol. The design choice to make it <org>:<app>:<machine> is what makes trust policies expressive.

With opaque sub values:

  • Trust policies need StringEquals on an exact value → one Role per Machine, or one Role trusted for every Machine.
  • To scope to "any Machine in the weather-cat app," you'd need a second API (a name-to-sub-list registry) or you'd end up with a wildcard that's too permissive.

With hierarchical sub:

  • "example:*" → any Machine in the example org.
  • "example:weather-cat:*" → any Machine in the weather-cat app.
  • "example:weather-cat:ancient-snow-4824" → one specific Machine.

The scope is a property of the trust policy, driven by the identity shape of the IdP.

The prefix-hierarchy convention

Common sub shapes across platforms:

Platform sub shape
Fly.io <org>:<app>:<machine>
GitHub Actions repo:<owner>/<name>:ref:refs/heads/<branch>
GitLab CI project_path:<group>/<project>:ref_type:branch:ref:<branch>
EKS IRSA system:serviceaccount:<ns>:<sa-name>

Each of these is a canonical sub-field scoping instance. The structure is chosen by the IdP; the scoping is enforced by the cloud trust policy's StringLike condition.

Trust-policy idiom

On AWS, the two idiomatic conditions are:

"Condition": {
  "StringEquals": {
    "<idp-issuer>:aud": "sts.amazonaws.com"
  },
  "StringLike": {
    "<idp-issuer>:sub": "<prefix-or-exact>"
  }
}

Two load-bearing conditions:

  • aud StringEquals <expected-aud> — guarantees the token was minted specifically for this audience. Without this, a token minted for a different service could be cross-used to assume this Role.
  • sub StringLike <scoping-prefix> — the scoping pivot.

Use StringEquals (not StringLike) on sub for the tightest possible scope (one specific Machine / one specific repo+branch). Use StringLike with * when you want to allow a prefix class.

Anti-pattern: wildcard the whole sub

"StringLike": {
  "oidc.fly.io/example:sub": "*"
}

This trusts any token signed by the IdP — i.e., any workload in the Fly.io org can assume the Role. Effectively: "my org has Role X" rather than "this specific app has Role X." Useful occasionally for a well-audited platform-admin Role, but the default should be tight prefix-matching.

The "scope your subject" discipline

Like CIDR blocks in VPCs or resource ARNs in IAM policies, sub prefixes give teams a structured way to express who within the IdP's population gets access. The hierarchy is the policy author's API surface — a well-chosen delimiter and component order makes expressive policies easy; a poorly-chosen one makes them impossible.

Fly.io's org:app:machine order goes from coarsest to finest, which matches the usual scoping question ("everything in this org? or this app? or just this one machine?").

  • GCP Workload Identity Federation — uses attribute mapping from OIDC claims to Google identity, and conditions via CEL (Common Expression Language). Structured claims similarly drive scoping expressions.
  • Azure workload-identity federation — uses federated- credential records that match on subject string.
  • HashiCorp Vault's OIDC auth — maps claims to roles via user/group_claim + bound_claims / bound_subject patterns.

Seen in

  • sources/2024-06-19-flyio-aws-without-access-keys — canonical wiki instance. sub = <org>:<app>:<machine>; StringLike on the sub prefix; "we can lock IAM Roles down to organizations, or to specific Fly Apps, or even specific Fly Machine instances."
Last updated · 200 distilled / 1,178 read