gcp-wif-federate-into-sa
An external identity the WIF provider accepts and a workloadIdentityUser binding resolves can federate into the impersonated ServiceAccount.
match
A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.
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 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) — 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."