gcp-wif-enter-project

Federating into a WIF-impersonated ServiceAccount gives the external principal a foothold in the SA's home project.

derived gcp emits CanEnterProject

match

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

{'ext': None} CanFederateAs {'sa': None}

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 typeIdentity
target typeProject
source?ext
targethome_boundary(?sa) (Project)
permissionsroles/iam.workloadIdentityUser iam.serviceAccounts.getAccessToken
conditionstrust_condition
state logicinherit 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.'
move · open · esc close