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.
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 type | Compute |
|---|---|
| target type | ServiceAccount |
| source | <node pool (VirtualMachine aggregate)> |
| target | <node pool SA (ServiceAccount)> |
| 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). |
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.'