gcp-ncc-spoke-create-transit-pivot
A principal with effective networkconnectivity.spokes.create on a hub that does not require manual spoke acceptance (requireAcceptance=false) can attach an attacker-controlled VPC, VPN, or Interconnect to the hub's routing domain. The spoke becomes ACTIVE immediately, placing the attacker's network on the same routing fabric as all legitimate spoke- attached networks - a transit pivot without modifying any VPC firewall rule in the destination projects.
match (effective permission)
where
emit
| source type | Identity |
|---|---|
| target type | TransitGateway |
| source | <principal> |
| target | <NCC Hub (TransitGateway) node> |
| permissions | networkconnectivity.spokes.create networkconnectivity.hubs.get |
| conditions | iam_permission service_state |
| state logic | ACTIVE if ?principal holds networkconnectivity.spokes.create (effective) AND hub.requireAcceptance == false (confirmed from hub resource). CONDITIONAL(service_state) if hub.requireAcceptance == true or is unknown — the spoke enters PENDING_REVIEW and does not route until a hub admin explicitly accepts it (networkconnectivity.spokes.update on the hub side). CONDITIONAL(iam_permission) if the effective-permission evaluator cannot confirm both required permissions. BLOCKED if an org policy constraint prevents spoke creation in the project. |
Narrative
{principal.name} holds networkconnectivity.spokes.create on project {project.name} and can read NCC Hub {hub.name} (requireAcceptance=false). Creating a new spoke attaching an attacker-controlled VPC to {hub.name} immediately places the attacker's network on the hub's routing domain, giving network-layer routing to all other spoke-attached VPCs and on- premises networks - without modifying any VPC firewall rule in the destination projects. This is a transit-pivot across project boundaries.
Raw rule rules/derived/gcp/ncc.yaml
id: gcp-ncc-spoke-create-transit-pivot
emits: CanModifyConfiguration
description: "A principal with effective networkconnectivity.spokes.create on a hub that does not require\
\ manual spoke acceptance (requireAcceptance=false) can attach an attacker-controlled VPC, VPN, or Interconnect\
\ to the hub's routing domain. The spoke becomes ACTIVE immediately, placing the attacker's network\
\ on the same routing fabric as all legitimate spoke- attached networks \u2014 a transit pivot without\
\ modifying any VPC firewall rule in the destination projects."
applies_to:
- gcp
match_effective_permission:
action: networkconnectivity.spokes.create
resource_type: networkconnectivity.googleapis.com/Hub
where:
- hub.requireAcceptance == false (auto-accept; new spokes become ACTIVE immediately)
- ?principal also has networkconnectivity.hubs.get on the hub (required to read hub details before attaching
a spoke)
emit:
source_type: Identity
target_type: TransitGateway
source: <principal>
target: <NCC Hub (TransitGateway) node>
permissions:
- networkconnectivity.spokes.create
- networkconnectivity.hubs.get
conditions:
- iam_permission
- service_state
state_logic: "ACTIVE if ?principal holds networkconnectivity.spokes.create (effective) AND hub.requireAcceptance\
\ == false (confirmed from hub resource). CONDITIONAL(service_state) if hub.requireAcceptance == true\
\ or is unknown \u2014 the spoke enters PENDING_REVIEW and does not route until a hub admin explicitly\
\ accepts it (networkconnectivity.spokes.update on the hub side). CONDITIONAL(iam_permission) if the\
\ effective-permission evaluator cannot confirm both required permissions. BLOCKED if an org policy\
\ constraint prevents spoke creation in the project."
confidence: '0.85'
derived_from:
- '<effective permission: networkconnectivity.spokes.create on spoke project>'
- '<Hub resource: requireAcceptance=false>'
false_positive_note: "(1) requireAcceptance=true is the critical gate: if set, new spokes are PENDING_REVIEW\
\ and do NOT route until a hub admin accepts. Downgrade to CONDITIONAL(service_state) when requireAcceptance\
\ is true or unknown. (2) VPC spokes must be in the same GCP organization as the hub (API constraint).\
\ An attacker in a different org can only attach VPN or Interconnect spokes (not VPC spokes). Downgrade\
\ to POTENTIAL when the attacker's project org cannot be confirmed to match the hub's org for VPC-spoke\
\ scenarios. (3) networkconnectivity.spokes.create alone is insufficient if the principal lacks networkconnectivity.hubs.get\
\ on the target hub \u2014 hub visibility is required to complete the spoke attachment. Confirm both\
\ permissions. (4) GCP IAM Deny policies blocking networkconnectivity.spokes.create or VPC Service\
\ Controls perimeters restricting networkconnectivity API access should downgrade the edge to BLOCKED.\
\ (5) The CanModifyConfiguration edge here records the CAPABILITY to add an attacker spoke. The resulting\
\ RoutesTo facts (from gcp-ncc-active-*- spoke-routes-* rules) are then derivable once the spoke exists.\
\ Do not emit CanNetworkReach or RoutesTo from this rule \u2014 those come from the structural spoke-fact\
\ rules, composing via the network-chains linchpin."
narrative: "{principal.name} holds networkconnectivity.spokes.create on project {project.name} and can\
\ read NCC Hub {hub.name} (requireAcceptance=false). Creating a new spoke attaching an attacker-controlled\
\ VPC to {hub.name} immediately places the attacker's network on the hub's routing domain, giving\
\ network-layer routing to all other spoke-attached VPCs and on- premises networks \u2014 without\
\ modifying any VPC firewall rule in the destination projects. This is a transit-pivot across project\
\ boundaries."