gcp-gke-update-nodepool-sa
container.nodePools.update + iam.serviceAccounts.actAs on a privileged SA allows changing an existing node pool's service account - converts existing pod workloads to run as the new (more privileged) SA via legacy metadata access.
match
A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.
where
?principal has EFFECTIVE container.nodePools.update on the cluster
?sa is the new node SA being bound to an existing node pool
?principal has iam.serviceAccounts.actAs on ?sa (GKE requires actAs to bind a SA to a node pool)
the node pool uses EXPOSE_ALL_SCOPES metadata mode OR WI is disabled on the cluster (so pods can reach the node SA metadata)
emit
| source type | Identity |
|---|---|
| target type | KubernetesCluster |
| source | ?principal |
| target | <GKE cluster / node pool> |
| permissions | container.nodePools.update iam.serviceAccounts.actAs |
| conditions | iam_permission role_compatibility service_state |
| state logic | ACTIVE when both container.nodePools.update AND iam.serviceAccounts.actAs are confirmed and the node pool will use EXPOSE_ALL_SCOPES (or the cluster has WI disabled). The node SA change is applied immediately to the node pool metadata, but running pods only read the new token after restart or after the metadata server cache expires. POTENTIAL(role_compatibility) if the node pool SA permissibility is uncertain. CONDITIONAL(service_state) until running pods refresh their metadata-cached tokens (typically within seconds to minutes). |
Narrative
{principal.name} can update an existing node pool in {cluster.name} (container.nodePools.update + iam.serviceAccounts.actAs) to change its node SA to {sa.name}; pods on those nodes (in legacy metadata mode) immediately gain {sa.name}'s OAuth token and cloud IAM privileges, compromising existing workloads without creating new resources.
Raw rule rules/derived/gcp/gke.yaml
id: gcp-gke-update-nodepool-sa
emits: CanModifyConfiguration
description: "container.nodePools.update + iam.serviceAccounts.actAs on a privileged SA allows changing\
\ an existing node pool's service account \u2014 converts existing pod workloads to run as the new (more\
\ privileged) SA via legacy metadata access."
match:
- - principal: null
- CanPassIdentity
- sa: null
where:
- ?principal has EFFECTIVE container.nodePools.update on the cluster
- ?sa is the new node SA being bound to an existing node pool
- ?principal has iam.serviceAccounts.actAs on ?sa (GKE requires actAs to bind a SA to a node pool)
- the node pool uses EXPOSE_ALL_SCOPES metadata mode OR WI is disabled on the cluster (so pods can reach
the node SA metadata)
emit:
source_type: Identity
target_type: KubernetesCluster
source: ?principal
target: <GKE cluster / node pool>
permissions:
- container.nodePools.update
- iam.serviceAccounts.actAs
conditions:
- iam_permission
- role_compatibility
- service_state
state_logic: ACTIVE when both container.nodePools.update AND iam.serviceAccounts.actAs are confirmed
and the node pool will use EXPOSE_ALL_SCOPES (or the cluster has WI disabled). The node SA change
is applied immediately to the node pool metadata, but running pods only read the new token after restart
or after the metadata server cache expires. POTENTIAL(role_compatibility) if the node pool SA permissibility
is uncertain. CONDITIONAL(service_state) until running pods refresh their metadata-cached tokens (typically
within seconds to minutes).
confidence: 0.87
derived_from:
- <gcp/identity-escalation.yaml gcp-sa-act-as CanPassIdentity edge>
- '<effective iam_permission: container.nodePools.update on cluster>'
false_positive_note: "container.nodePools.update requires iam.serviceAccounts.actAs on the new SA to\
\ bind it to the node pool \u2014 without actAs GKE rejects the request. The node SA token is only\
\ accessible to pods if workloadMetadataConfig is NOT set to GKE_METADATA. This is a CanModifyConfiguration(target=cluster)\
\ emission because the attack modifies the cluster's node pool configuration; it differs from gcp-gke-create-nodepool-as-sa\
\ (CanCreateWorkloadAs) in that no new workload is created \u2014 existing pods are compromised in-place\
\ when they read the updated node SA token from metadata. GKE Autopilot clusters do NOT allow node\
\ pool mutations \u2014 BLOCKED for Autopilot."
narrative: '{principal.name} can update an existing node pool in {cluster.name} (container.nodePools.update
+ iam.serviceAccounts.actAs) to change its node SA to {sa.name}; pods on those nodes (in legacy metadata
mode) immediately gain {sa.name}''s OAuth token and cloud IAM privileges, compromising existing workloads
without creating new resources.'