gcp-wif-enter-project
Federating into a WIF-impersonated ServiceAccount gives the external principal a foothold in the SA's home project.
match
A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.
where
node_type(?sa) ==
ServiceAccount
node_class(?ext) in [Identity] # ExternalIdentity/FederatedIdentity are Identity subtypes
node_type(home_boundary(?sa)) == Project
home_boundary(?sa) is a project the external principal is not already in
emit
| source type | Identity |
|---|---|
| target type | Project |
| source | ?ext |
| target | home_boundary(?sa) (Project) |
| permissions | roles/iam.workloadIdentityUser iam.serviceAccounts.getAccessToken |
| conditions | trust_condition |
| state logic | inherit from the CanFederateAs input: ACTIVE only when the federation is ACTIVE (broad/unpinned WIF binding the attacker satisfies); CONDITIONAL(trust_condition) when the federation is CONDITIONAL (subject-pinned); BLOCKED if the federation is BLOCKED. The foothold is exactly as strong as the takeover that produces it. |
Narrative
{ext.name} can federate in as {sa.name}, which is homed in project {project.name}; the WIF takeover therefore lands the attacker inside {project.name}, operating with {sa.name}'s roles.
Raw rule rules/derived/gcp/wif.yaml
id: gcp-wif-enter-project
emits: CanEnterProject
description: Federating into a WIF-impersonated ServiceAccount gives the external principal a foothold
in the SA's home project.
applies_to:
- gcp
match:
- - ext: null
- CanFederateAs
- sa: null
where:
- node_type(?sa) == ServiceAccount
- 'node_class(?ext) in [Identity] # ExternalIdentity/FederatedIdentity are Identity subtypes'
- node_type(home_boundary(?sa)) == Project
- home_boundary(?sa) is a project the external principal is not already in
emit:
source_type: Identity
target_type: Project
source: ?ext
target: home_boundary(?sa) (Project)
permissions:
- roles/iam.workloadIdentityUser
- iam.serviceAccounts.getAccessToken
conditions:
- trust_condition
state_logic: 'inherit from the CanFederateAs input: ACTIVE only when the federation is ACTIVE (broad/unpinned
WIF binding the attacker satisfies); CONDITIONAL(trust_condition) when the federation is CONDITIONAL
(subject-pinned); BLOCKED if the federation is BLOCKED. The foothold is exactly as strong as the takeover
that produces it.'
confidence: min(contributing_confidences)
derived_from:
- ?ext CanFederateAs ?sa
- home_boundary(?sa) == Project
false_positive_note: "The foothold is entry AS ?sa \u2014 it grants whatever the SA can do in the project,\
\ NOT the project's full privilege (Controls only if the SA is itself a project admin; that upgrade\
\ is hierarchy-chains enter-boundary-rollups, not here). Dedupes with federation-chains federate-then-enter-project\
\ on (CanEnterProject, ?ext, project) \u2014 do not double-count weight. Never upgrade a CONDITIONAL\
\ federation to an ACTIVE foothold. Do not emit if the SA is homed in a project the principal already\
\ controls (no boundary crossed)."
narrative: '{ext.name} can federate in as {sa.name}, which is homed in project {project.name}; the WIF
takeover therefore lands the attacker inside {project.name}, operating with {sa.name}''s roles.'