gcp-wif-federate-into-sa

An external identity the WIF provider accepts and a workloadIdentityUser binding resolves can federate into the impersonated ServiceAccount.

derived gcp emits CanFederateAs

match

A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.

{'ext': None} ExternalIdentityMapsTo {'sa': None}

where

node_type(?ext) in [ExternalIdentity, FederatedIdentity] node_type(?sa) == ServiceAccount exists TrustsExternalIdentity(?anchor -> ?ext) backing this mapping, with the WIF condition C (issuer/audience/attribute-condition/subject-pin) recorded on the edge

optional

Present → upgrade the emitted state; absent → downgrade (not a hard requirement).

{'ext': None} TrustsExternalIdentity {'sa': None}

emit

source typeFederatedIdentity
target typeServiceAccount
source?ext
target?sa
permissionsroles/iam.workloadIdentityUser iam.serviceAccounts.getAccessToken sts.googleapis.com token-exchange
conditionstrust_relationship condition_expression trust_condition
state logicif the WIF trust is BROAD/UNPINNED so an attacker can present a matching external identity RIGHT NOW (workloadIdentityUser principalSet:// scoped to an attribute only / .../subject/* / .../* ; OR attributeCondition absent/broad; OR allowedAudiences accepts an attacker-mintable aud) OR the attacker demonstrably controls a matching ?ext (a repo/project/pod under the pinned issuer): ACTIVE. If the binding pins a specific google.subject / repo:ref / attribute value the attacker does NOT control: CONDITIONAL(trust_condition) — realizable only once the attacker obtains a matching external identity (e.g. runs a workflow on that repo/ref). BLOCKED if the provider/pool is DISABLED/DELETED or an IAM deny policy denies getAccessToken at the SA scope (in which case the explicit trust is not emitted / emitted BLOCKED).

Narrative

{ext.name} is accepted by the workload identity pool provider {anchor.name} (issuer/audience/attribute condition) and resolved to {sa.name} by a roles/iam.workloadIdentityUser binding; because that binding's condition is {trust_condition_summary}, an identity the attacker controls satisfies it and federates in as {sa.name} via the STS token exchange - obtaining the SA's access token with no static key.

Raw rule rules/derived/gcp/wif.yaml

id: gcp-wif-federate-into-sa
emits: CanFederateAs
description: An external identity the WIF provider accepts and a workloadIdentityUser binding resolves
  can federate into the impersonated ServiceAccount.
applies_to:
- gcp
match:
- - ext: null
  - ExternalIdentityMapsTo
  - sa: null
where:
- node_type(?ext) in [ExternalIdentity, FederatedIdentity]
- node_type(?sa) == ServiceAccount
- exists TrustsExternalIdentity(?anchor -> ?ext) backing this mapping, with the WIF condition C (issuer/audience/attribute-condition/subject-pin)
  recorded on the edge
optional:
- - ext: null
  - TrustsExternalIdentity
  - sa: null
emit:
  source_type: FederatedIdentity
  target_type: ServiceAccount
  source: ?ext
  target: ?sa
  permissions:
  - roles/iam.workloadIdentityUser
  - iam.serviceAccounts.getAccessToken
  - sts.googleapis.com token-exchange
  conditions:
  - trust_relationship
  - condition_expression
  - trust_condition
  state_logic: "if the WIF trust is BROAD/UNPINNED so an attacker can present a matching external identity\
    \ RIGHT NOW (workloadIdentityUser principalSet:// scoped to an attribute only / .../subject/* / .../*\
    \ ; OR attributeCondition absent/broad; OR allowedAudiences accepts an attacker-mintable aud) OR the\
    \ attacker demonstrably controls a matching ?ext (a repo/project/pod under the pinned issuer): ACTIVE.\
    \ If the binding pins a specific google.subject / repo:ref / attribute value the attacker does NOT\
    \ control: CONDITIONAL(trust_condition) \u2014 realizable only once the attacker obtains a matching\
    \ external identity (e.g. runs a workflow on that repo/ref). BLOCKED if the provider/pool is DISABLED/DELETED\
    \ or an IAM deny policy denies getAccessToken at the SA scope (in which case the explicit trust is\
    \ not emitted / emitted BLOCKED)."
  confidence: min(contributing_confidences)
  derived_from:
  - ?ext ExternalIdentityMapsTo ?sa
  - ?anchor TrustsExternalIdentity ?ext (backing WIF provider trust, condition C)
  false_positive_note: "The whole judgment is the WIF trust CONDITION. UNPINNED => ACTIVE: a roles/iam.workloadIdentityUser\
    \ binding on principalSet://.../attribute.aud/<v>, .../attribute.repository/* or .../subject/* (any\
    \ workload from the issuer), or a provider with no/broad attributeCondition. PINNED => CONDITIONAL:\
    \ the member is principal://.../subject/<exact> or principalSet://.../attribute.repository/<org>/<repo>\
    \ WITH an attributeCondition on the ref the attacker cannot present. Empty allowedAudiences is NOT\
    \ a wildcard \u2014 it still requires aud == the provider resource URL (a pin); do not treat it as\
    \ broad. Requires BOTH the provider to accept ?ext AND an SA to carry the workloadIdentityUser binding\
    \ \u2014 a broad provider with no such binding yields NO federation. A DISABLED/DELETED pool or provider,\
    \ or a deny policy on getAccessToken, => no ACTIVE edge. Do NOT treat the mere existence of WIF as\
    \ compromise \u2014 a correctly pinned trust is a legitimate control. Dedupe with federation-chains\
    \ federation-maps-to-identity and Phase-2 gcp-workload-identity-federation on (CanFederateAs, ?ext,\
    \ ?sa)."
  narrative: "{ext.name} is accepted by the workload identity pool provider {anchor.name} (issuer/audience/attribute\
    \ condition) and resolved to {sa.name} by a roles/iam.workloadIdentityUser binding; because that binding's\
    \ condition is {trust_condition_summary}, an identity the attacker controls satisfies it and federates\
    \ in as {sa.name} via the STS token exchange \u2014 obtaining the SA's access token with no static\
    \ key."
move · open · esc close