gcp-gke-node-sa-legacy-exposure

A GKE node pool with workloadMetadataConfig.mode != GKE_METADATA allows pods to reach the GCE metadata server and read the node pool SA's OAuth token - the node SA credential is exposed to every pod on those nodes.

derived gcp emits ExposesCredential

match (record)

{ "field": "config.workloadMetadataConfig.mode", "resource_type": "container.googleapis.com/NodePool" }

where

config.workloadMetadataConfig.mode != 'GKE_METADATA' (i.e., EXPOSE_ALL_SCOPES, UNSPECIFIED, or legacy) ?node_pool has a service account in config.serviceAccount

emit

source typeCompute
target typeServiceAccount
source<node pool (VirtualMachine aggregate)>
target<node pool SA (ServiceAccount)>
conditionsservice_state network_reachability
state logicACTIVE when config.workloadMetadataConfig.mode is EXPOSE_ALL_SCOPES (or unset / legacy) AND the node pool SA is non-empty AND nodes are running. BLOCKED when GKE_METADATA mode is enforced (the GKE metadata proxy intercepts metadata requests and scopes tokens to the pod's WI identity, blocking node SA access from pods). CONDITIONAL(service_state) if node pool nodes are not yet running (scale-up in progress).

Narrative

{node_pool.name}'s workload metadata is configured with EXPOSE_ALL_SCOPES (not GKE_METADATA); pods on these nodes can reach the GCE metadata server at http://metadata.google.internal and read the node SA {sa.name}'s OAuth token, effectively executing as {sa.name} without any GCP IAM permission from within the pod.

Raw rule rules/derived/gcp/gke.yaml

id: gcp-gke-node-sa-legacy-exposure
emits: ExposesCredential
description: "A GKE node pool with workloadMetadataConfig.mode != GKE_METADATA allows pods to reach the\
  \ GCE metadata server and read the node pool SA's OAuth token \u2014 the node SA credential is exposed\
  \ to every pod on those nodes."
match_record:
  resource_type: container.googleapis.com/NodePool
  field: config.workloadMetadataConfig.mode
where:
- config.workloadMetadataConfig.mode != 'GKE_METADATA'  (i.e., EXPOSE_ALL_SCOPES, UNSPECIFIED, or legacy)
- ?node_pool has a service account in config.serviceAccount
emit:
  source_type: Compute
  target_type: ServiceAccount
  source: <node pool (VirtualMachine aggregate)>
  target: <node pool SA (ServiceAccount)>
  permissions: []
  conditions:
  - service_state
  - network_reachability
  state_logic: ACTIVE when config.workloadMetadataConfig.mode is EXPOSE_ALL_SCOPES (or unset / legacy)
    AND the node pool SA is non-empty AND nodes are running. BLOCKED when GKE_METADATA mode is enforced
    (the GKE metadata proxy intercepts metadata requests and scopes tokens to the pod's WI identity, blocking
    node SA access from pods). CONDITIONAL(service_state) if node pool nodes are not yet running (scale-up
    in progress).
  confidence: 0.9
  derived_from:
  - <node pool config.workloadMetadataConfig.mode collected from GKE API>
  - <node pool config.serviceAccount>
  false_positive_note: "This edge is BLOCKED whenever workloadMetadataConfig.mode == GKE_METADATA \u2014\
    \ the GKE Metadata Proxy prevents pods from reaching the raw node metadata endpoint. Do NOT emit ACTIVE\
    \ for GKE_METADATA mode clusters. Also check that the node pool SA is not the GKE-managed service\
    \ account (which has limited scope); the Compute Engine default SA (PROJECT_NUMBER-compute@developer.\
    \ gserviceaccount.com) historically holds roles/editor and is a high-value target. access scopes on\
    \ the node pool constrain the token even if the SA holds the role \u2014 note as false-positive risk\
    \ (scopes are not an enumerated condition_type)."
  narrative: '{node_pool.name}''s workload metadata is configured with EXPOSE_ALL_SCOPES (not GKE_METADATA);
    pods on these nodes can reach the GCE metadata server at http://metadata.google.internal and read
    the node SA {sa.name}''s OAuth token, effectively executing as {sa.name} without any GCP IAM permission
    from within the pod.'
move · open · esc close