GCP GKE Hardening

Overview

This page covers hardening controls for Google Kubernetes Engine (GKE). Both Standard and Autopilot cluster modes are addressed — Autopilot/Standard differences are noted in per-control callouts immediately below each control header. Where a control is enforced by default in Autopilot, the callout identifies it; where Autopilot prevents manual configuration, the callout explains what Google manages on your behalf. See general/kubernetes.html for the cross-cutting threat model, cluster-baseline principles, and common misconfigurations that apply to all providers.

Controls are ordered by severity: CRITICAL first, then HIGH in control-number order, then MEDIUM. Terraform examples use hashicorp/google ~> 6.0. The sealed v1.0 GCP pages use ~> 5.0 — do not edit those pages. Supporting IAM prerequisites are on gcp/iam.html; VPC-native networking prerequisites are on gcp/network.html; audit log sink configuration is on gcp/logging.html.

gcp-k8s-01 ! CRITICAL PREVENTIVE

GKE Autopilot: Private nodes and a private control-plane endpoint are enforced by default in Autopilot clusters. Authorized networks for the control-plane endpoint should still be explicitly configured. GKE Standard: Pass --enable-private-nodes, --enable-private-endpoint, --master-ipv4-cidr-block, and --enable-master-authorized-networks at cluster creation. CIS GKE Autopilot Benchmark v1.3.0 documents Autopilot-specific defaults.

Enable a private GKE cluster so worker nodes have no public IP addresses, and enable a private control-plane endpoint so the kube-apiserver is not reachable from the public internet. Combine with master authorized networks to restrict which CIDR ranges can reach the control plane. A public kube-apiserver is the number-one Kubernetes breach vector — any leaked credential is immediately usable from the internet without network-level barriers.

Remediation — Terraform

# Terraform Google provider ~> 6.0
resource "google_container_cluster" "hardened" {
  name     = "hardened-cluster"
  location = var.region

  # Private cluster — no external IPs on nodes
  private_cluster_config {
    enable_private_nodes    = true
    enable_private_endpoint = true
    master_ipv4_cidr_block  = "172.16.0.0/28"
  }

  # Restrict control-plane access to authorized CIDR ranges
  master_authorized_networks_config {
    cidr_blocks {
      cidr_block   = var.management_cidr
      display_name = "management-network"
    }
  }

  # VPC-native cluster (alias IP ranges)
  ip_allocation_policy {}

  # Dataplane V2 enables NetworkPolicy enforcement via Cilium
  datapath_provider = "ADVANCED_DATAPATH"
}

Remediation — gcloud

gcloud container clusters create CLUSTER_NAME \
  --enable-private-nodes \
  --enable-private-endpoint \
  --master-ipv4-cidr-block 172.16.0.0/28 \
  --enable-master-authorized-networks \
  --master-authorized-networks MGMT_CIDR/32 \
  --region REGION

Remediation — Config Connector

apiVersion: container.cnrm.cloud.google.com/v1beta1
kind: ContainerCluster
metadata:
  name: hardened-gke
  namespace: config-control
spec:
  location: us-central1
  initialNodeCount: 1
  privateClusterConfig:
    enablePrivateNodes: true
    enablePrivateEndpoint: true
    masterIpv4CidrBlock: "172.16.0.0/28"
  masterAuthorizedNetworksConfig:
    cidrBlocks:
    - cidrBlock: "10.0.0.0/8"
      displayName: "corp-vpn"
  networkRef:
    name: gke-vpc
  subnetworkRef:
    name: gke-subnet

Remediation — Pulumi (TypeScript)

import * as gcp from "@pulumi/gcp";

// Private GKE cluster with private endpoint and master-authorized-networks lock.
const hardenedCluster = new gcp.container.Cluster("hardened-gke", {
    name: "hardened-gke",
    location: "us-central1",
    initialNodeCount: 1,
    removeDefaultNodePool: true,
    privateClusterConfig: {
        enablePrivateNodes: true,
        enablePrivateEndpoint: true,
        masterIpv4CidrBlock: "172.16.0.0/28",
    },
    masterAuthorizedNetworksConfig: {
        cidrBlocks: [{ cidrBlock: "10.0.0.0/8", displayName: "corp-vpn" }],
    },
    network: gkeVpc.id,
    subnetwork: gkeSubnet.id,
});

Compliance mapping

Control Severity Type Provider CIS Kubernetes Benchmark v1.11.0 CIS Google Kubernetes Engine (GKE) Benchmark v1.9.0 NIST SP 800-53 rev5 ISO/IEC 27001:2022 ISO/IEC 27017:2015 NIST SP 800-190 (Sep 2017) NSA/CISA Kubernetes Hardening Guide v1.2
gcp-k8s-01 CRITICAL PREVENTIVE GCP GKE n/a (managed control plane) n/a (verify against CIS GKE Benchmark v1.9.0 PDF) AC-17; SC-7; SC-8 A.8.20; A.8.22 CLD.13.1.4 NIST SP 800-190 §4.4.1 NSA/CISA Kubernetes Hardening Guide v1.2 §2 (Network separation)

Log signals

  • Cloud Audit Logs on container.googleapis.com with protoPayload.methodName="google.container.v1.ClusterManager.UpdateCluster" where the request body contains privateClusterConfig.enablePrivateEndpoint=false or removes masterAuthorizedNetworksConfig.
  • GKE control-plane endpoint visibility change events: desiredPrivateClusterConfig.publicEndpointEnabled flipped to true on an existing cluster.
  • Authorized-network CIDR drift: any cidrBlocks entry widening to 0.0.0.0/0 or a non-corporate range.

Query

logName=~"projects/.*/logs/cloudaudit.googleapis.com%2Factivity"
          AND resource.type="gke_cluster"
          AND protoPayload.methodName="google.container.v1.ClusterManager.UpdateCluster"
          AND (protoPayload.request.update.desiredPrivateClusterConfig.enablePrivateEndpoint=false
               OR protoPayload.request.update.desiredMasterAuthorizedNetworksConfig.cidrBlocks.cidrBlock="0.0.0.0/0")

Pin this Cloud Logging filter to a per-project log-based metric so any control-plane exposure regression is plotted next to cluster age; combine with a Cloud Asset Inventory feed on container.googleapis.com/Cluster to receive a push notification whenever the visibility field mutates.

Alert threshold

  • Page on any UpdateCluster that re-enables the public control-plane endpoint or removes the authorized-networks gate from a production cluster.
  • Page on any CIDR added to cidrBlocks outside the documented corporate-egress allow-list — the steady-state CIDR set is fixed by infra-as-code.

Initial response

  1. Identify the principal via protoPayload.authenticationInfo.principalEmail and verify the change ticket; if the change is unsanctioned, revert with gcloud container clusters update --enable-private-endpoint --master-authorized-networks=<corp-cidrs>.
  2. Inspect the cluster's API-server access logs for the period the public endpoint was exposed — pull k8s.io/api/v1/namespaces/*/pods/exec and other privileged endpoints from resource.type="k8s_cluster" entries during the window.
  3. Rotate cluster credentials (kubectl delete secret -n kube-system bootstrap-token-*; rotate node service-account binding) and re-issue any client certificates derived from the control plane during the exposed window.

References

Equivalent controls in other providers: EKS private endpoint, AKS private cluster, OKE private API endpoint.

gcp-k8s-02 ! HIGH PREVENTIVE

GKE Autopilot: Workload Identity is mandatory — it cannot be disabled. Service accounts must be configured to use the workload pool. GKE Standard: Set workload_identity_config { workload_pool = "PROJECT_ID.svc.id.goog" } at cluster creation or update; bind Kubernetes ServiceAccounts to GCP IAM ServiceAccounts via roles/iam.workloadIdentityUser. CIS GKE Autopilot Benchmark v1.3.0 documents Autopilot-specific defaults.

Enable Workload Identity Federation for GKE so Kubernetes pods use GCP IAM ServiceAccounts instead of the default Compute Engine service account. The Compute Engine default SA carries broad project-level permissions including Compute viewer and logging writer — using it for pod workloads violates least-privilege. Workload Identity binds a Kubernetes ServiceAccount in a specific namespace to a GCP SA with the minimum necessary IAM roles, and eliminates the need for static SA key files mounted as secrets.

Remediation — Terraform

# Terraform Google provider ~> 6.0
resource "google_container_cluster" "hardened" {
  name     = "hardened-cluster"
  location = var.region

  workload_identity_config {
    workload_pool = "${var.project_id}.svc.id.goog"
  }
}

# Bind a Kubernetes ServiceAccount to a GCP IAM ServiceAccount
resource "google_service_account_iam_binding" "workload_identity" {
  service_account_id = google_service_account.app.name
  role               = "roles/iam.workloadIdentityUser"
  members = [
    "serviceAccount:${var.project_id}.svc.id.goog[${var.namespace}/${var.ksa_name}]"
  ]
}

Remediation — gcloud

# Enable Workload Identity on existing cluster
gcloud container clusters update CLUSTER_NAME \
  --workload-pool=PROJECT_ID.svc.id.goog \
  --region REGION

# Bind K8s ServiceAccount to GCP SA
gcloud iam service-accounts add-iam-policy-binding \
  APP_SA@PROJECT_ID.iam.gserviceaccount.com \
  --role roles/iam.workloadIdentityUser \
  --member "serviceAccount:PROJECT_ID.svc.id.goog[NAMESPACE/KSA_NAME]"

# Annotate the Kubernetes ServiceAccount
kubectl annotate serviceaccount KSA_NAME \
  --namespace NAMESPACE \
  iam.gke.io/gcp-service-account=APP_SA@PROJECT_ID.iam.gserviceaccount.com

Remediation — Config Connector

apiVersion: container.cnrm.cloud.google.com/v1beta1
kind: ContainerCluster
metadata:
  name: workload-identity-gke
  namespace: config-control
spec:
  location: us-central1
  initialNodeCount: 1
  workloadIdentityConfig:
    workloadPool: "PROJECT_ID.svc.id.goog"

Compliance mapping

Control Severity Type Provider CIS Kubernetes Benchmark v1.11.0 CIS Google Kubernetes Engine (GKE) Benchmark v1.9.0 NIST SP 800-53 rev5 ISO/IEC 27001:2022 ISO/IEC 27017:2015 NIST SP 800-190 (Sep 2017) NSA/CISA Kubernetes Hardening Guide v1.2
gcp-k8s-02 HIGH PREVENTIVE GCP GKE n/a (managed control plane) n/a (verify against CIS GKE Benchmark v1.9.0 PDF) IA-2; AC-6; IA-5 A.5.15; A.5.18 n/a NIST SP 800-190 §4.4.2 NSA/CISA Kubernetes Hardening Guide v1.2 §4 (IAM/RBAC)

Log signals

  • Cluster updates removing workloadIdentityConfig.workloadPool (back to legacy node-default credentials): protoPayload.request.update.desiredWorkloadIdentityConfig.workloadPool="".
  • Node-pool updates flipping nodeConfig.workloadMetadataConfig.mode from GKE_METADATA to EXPOSED — that exposes the GCE metadata server to pod workloads and undoes the WIF guarantee.
  • Kubernetes service-account annotations removing iam.gke.io/gcp-service-account — visible in resource.type="k8s_cluster" audit entries on core/v1/serviceaccounts.

Query

logName=~"projects/.*/logs/cloudaudit.googleapis.com%2Factivity"
          AND resource.type="gke_cluster"
          AND (protoPayload.methodName="google.container.v1.ClusterManager.UpdateCluster"
               OR protoPayload.methodName="google.container.v1.ClusterManager.UpdateNodePool")
          AND (protoPayload.request.update.desiredWorkloadIdentityConfig.workloadPool=""
               OR protoPayload.request.update.desiredNodePoolAutoConfig.networkTags.tags=~"legacy-metadata")

Stream this Cloud Logging filter into Cloud Monitoring as a log-based counter; in parallel run a Cloud Asset Inventory query over container.googleapis.com/Cluster to surface clusters where workloadIdentityConfig is unset — those are net-new exposure too.

Alert threshold

  • Page on any cluster update that clears workloadPool or any node-pool update that flips workloadMetadataConfig.mode away from GKE_METADATA.
  • Daily inventory diff: alert if a previously WIF-enabled cluster shows workloadIdentityConfig: null regardless of whether the audit-log update event was captured (covers replay drift).

Initial response

  1. Identify pods whose service accounts had annotations pointing at GCP service accounts via kubectl get sa -A -o json | jq; capture which pods were running during the exposed window.
  2. Re-enable WIF (gcloud container clusters update CLUSTER --workload-pool=PROJECT.svc.id.goog) and flip node-pools back to GKE_METADATA; rotate the GCP service-account credentials that the pods previously impersonated.
  3. Audit Cloud Audit Logs for compute.googleapis.com calls originating from the node-default service account during the gap window; any data-plane call from the node SA is suspect because pods now have raw node-IAM access.

References

Equivalent controls in other providers: EKS Pod Identity, AKS Workload Identity, OKE Workload Identity.

gcp-k8s-03 ! HIGH PREVENTIVE

GKE Autopilot: Autopilot uses Google-managed encryption for etcd by default. Customer-managed encryption keys (CMEK) via Cloud KMS are supported but require explicit configuration at cluster creation. GKE Standard: Configure database_encryption { state = "ENCRYPTED", key_name = KMS_KEY_RESOURCE_ID } at cluster creation or update. CIS GKE Autopilot Benchmark v1.3.0 documents Autopilot-specific defaults.

Enable application-layer secrets encryption so Kubernetes Secrets stored in etcd are encrypted with a Cloud KMS Customer-Managed Encryption Key (CMEK). This adds an envelope encryption layer on top of Google's default etcd encryption-at-rest, giving the customer control over the key lifecycle — including rotation and revocation. Without CMEK, Google holds the encryption key; with CMEK, the customer can revoke access to the cluster's etcd contents by disabling the key.

Remediation — Terraform

# Terraform Google provider ~> 6.0
resource "google_kms_key_ring" "k8s_keyring" {
  name     = "gke-secrets-keyring"
  location = var.region
}

resource "google_kms_crypto_key" "k8s_secrets" {
  name     = "gke-secrets-key"
  key_ring = google_kms_key_ring.k8s_keyring.id

  rotation_period = "7776000s" # 90 days
}

resource "google_container_cluster" "hardened" {
  name     = "hardened-cluster"
  location = var.region

  database_encryption {
    state    = "ENCRYPTED"
    key_name = google_kms_crypto_key.k8s_secrets.id
  }
}

Remediation — gcloud

gcloud container clusters create CLUSTER_NAME \
  --database-encryption-key=projects/PROJECT_ID/locations/REGION/keyRings/KEYRING/cryptoKeys/KEY_NAME \
  --region REGION

Remediation — Config Connector

apiVersion: container.cnrm.cloud.google.com/v1beta1
kind: ContainerCluster
metadata:
  name: secrets-encrypted-gke
  namespace: config-control
spec:
  location: us-central1
  initialNodeCount: 1
  databaseEncryption:
    state: ENCRYPTED
    keyName: "projects/PROJECT_ID/locations/us-central1/keyRings/gke-kr/cryptoKeys/etcd-key"

Compliance mapping

Control Severity Type Provider CIS Kubernetes Benchmark v1.11.0 CIS Google Kubernetes Engine (GKE) Benchmark v1.9.0 NIST SP 800-53 rev5 ISO/IEC 27001:2022 ISO/IEC 27017:2015 NIST SP 800-190 (Sep 2017) NSA/CISA Kubernetes Hardening Guide v1.2
gcp-k8s-03 HIGH PREVENTIVE GCP GKE §1.2 (etcd — verify against CIS Kubernetes Benchmark v1.11.0 PDF) n/a (verify against CIS GKE Benchmark v1.9.0 PDF) SC-28; IA-5 A.8.24; A.8.10 n/a NIST SP 800-190 §4.3.2 NSA/CISA Kubernetes Hardening Guide v1.2 §5 (Secrets)

Log signals

  • Cluster updates where databaseEncryption.state transitions from ENCRYPTED to DECRYPTED, or where databaseEncryption.keyName is cleared.
  • Cloud KMS audit events on the wrapping key: cloudkms.googleapis.com CryptoKeyVersion.disable or CryptoKey.update changing the IAM binding for the GKE control-plane service identity.
  • Pre-existing Secret-resource access patterns shifting: a spike in k8s.io/api/v1/namespaces/*/secrets reads after a KMS binding edit is a correlated signal.

Query

(logName=~"projects/.*/logs/cloudaudit.googleapis.com%2Factivity"
           AND resource.type="gke_cluster"
           AND protoPayload.methodName="google.container.v1.ClusterManager.UpdateCluster"
           AND protoPayload.request.update.desiredDatabaseEncryption.state="DECRYPTED")
          OR
          (logName=~"projects/.*/logs/cloudaudit.googleapis.com%2Factivity"
           AND protoPayload.serviceName="cloudkms.googleapis.com"
           AND protoPayload.methodName=~".*CryptoKey.*"
           AND protoPayload.request.cryptoKey.name=~".*gke-etcd.*")

This two-leg Cloud Logging query catches both the cluster-side opt-out and the KMS-side key disable; join on the cluster's databaseEncryption.keyName attribute via Cloud Asset Inventory to maintain a live mapping of cluster→KMS key.

Alert threshold

  • Page on any cluster transitioning out of envelope-encrypted state; the constraint is project-wide and there is no acceptable steady-state for production clusters to be unencrypted.
  • Page on KMS key-version disable for any key listed in the cluster→KMS map; secrets become unreadable and a rollover plan must be executed in minutes.

Initial response

  1. Capture the principal and the change ticket; if the disable was malicious, restore the key version (gcloud kms keys versions enable) before the control plane fails to read secrets.
  2. Re-enable envelope encryption (gcloud container clusters update --database-encryption-key=…); if the cluster ran briefly in decrypted state, treat every Secret resource read during the window as candidate-exposed and rotate the underlying credentials.
  3. Audit the KMS key's IAM bindings — particularly removals of roles/cloudkms.cryptoKeyEncrypterDecrypter from the GKE service identity — and pin the binding back via the captured baseline policy.

References

Equivalent controls in other providers: EKS KMS envelope encryption, AKS KMS etcd encryption, OKE Vault secrets encryption.

gcp-k8s-04 ! HIGH PREVENTIVE

GKE Autopilot: Binary Authorization is NOT enforced by default in Autopilot clusters — it must be explicitly configured. Both Autopilot and Standard clusters require the Binary Authorization API to be enabled and an admission policy to be set. GKE Standard: Same configuration required — enable the Binary Authorization API, create an attestor, and set the cluster evaluation mode. CIS GKE Autopilot Benchmark v1.3.0 documents Autopilot-specific defaults.

Enable Binary Authorization with an attestor-required policy so only container images that have been cryptographically verified and attested by a trusted build pipeline can be deployed to the cluster. This is a GKE-unique differentiator — it prevents unsigned, unverified, or tampered images from running regardless of which tag or digest is referenced at deploy time. The default policy (evaluationMode: ALWAYS_ALLOW) provides no protection; change to REQUIRE_ATTESTATION in ENFORCED_BLOCK_AND_AUDIT_LOG mode. Mutable image tags are the silent supply-chain risk: Binary Authorization locks image identity to a cryptographic attestation, not a tag string.

Remediation — Binary Authorization Policy (YAML)

# Binary Authorization policy — attestor-required
# Save as policy.yaml, import with: gcloud container binauthz policy import policy.yaml
admissionWhitelistPatterns: []
defaultAdmissionRule:
  evaluationMode: REQUIRE_ATTESTATION
  enforcementMode: ENFORCED_BLOCK_AND_AUDIT_LOG
  requireAttestationsBy:
    - projects/MY_PROJECT/attestors/build-attestor
clusterAdmissionRules:
  REGION.CLUSTER_NAME:
    evaluationMode: REQUIRE_ATTESTATION
    enforcementMode: ENFORCED_BLOCK_AND_AUDIT_LOG
    requireAttestationsBy:
      - projects/MY_PROJECT/attestors/build-attestor

Remediation — Terraform

# Terraform Google provider ~> 6.0
# Note: google_binary_authorization_policy may require hashicorp/google-beta
# at authoring time — verify provider tier at registry.terraform.io/providers/hashicorp/google
resource "google_binary_authorization_attestor" "build_attestor" {
  name = "build-attestor"
  attestation_authority_note {
    note_reference = google_container_note.build_note.name
  }
}

# Enable Binary Authorization enforcement on the cluster
resource "google_container_cluster" "hardened" {
  name     = "hardened-cluster"
  location = var.region

  binary_authorization {
    evaluation_mode = "PROJECT_SINGLETON_POLICY_ENFORCE"
  }
}

Remediation — gcloud

# Import the policy
gcloud container binauthz policy import policy.yaml --project=PROJECT_ID

# Enable enforcement on the cluster
gcloud container clusters update CLUSTER_NAME \
  --binauthz-evaluation-mode=PROJECT_SINGLETON_POLICY_ENFORCE \
  --region REGION

Remediation — Config Connector

apiVersion: binaryauthorization.cnrm.cloud.google.com/v1beta1
kind: BinaryAuthorizationPolicy
metadata:
  name: binauthz-policy
  namespace: config-control
spec:
  projectRef:
    external: "projects/PROJECT_ID"
  defaultAdmissionRule:
    evaluationMode: REQUIRE_ATTESTATION
    enforcementMode: ENFORCED_BLOCK_AND_AUDIT_LOG
    requireAttestationsBy:
    - "projects/PROJECT_ID/attestors/prod-build-attestor"
  globalPolicyEvaluationMode: ENABLE

Compliance mapping

Control Severity Type Provider CIS Kubernetes Benchmark v1.11.0 CIS Google Kubernetes Engine (GKE) Benchmark v1.9.0 NIST SP 800-53 rev5 ISO/IEC 27001:2022 ISO/IEC 27017:2015 NIST SP 800-190 (Sep 2017) NSA/CISA Kubernetes Hardening Guide v1.2
gcp-k8s-04 HIGH PREVENTIVE GCP GKE n/a (GKE-specific — no CIS Kubernetes Benchmark v1.11.0 section) n/a (verify against CIS GKE Benchmark v1.9.0 PDF) CM-14; SA-10; SI-7 A.8.9; A.8.29 CLD.9.5.2 NIST SP 800-190 §4.1 (Image risks) NSA/CISA Kubernetes Hardening Guide v1.2 §3 (Pod security)

Log signals

  • Cloud Audit Logs on binaryauthorization.googleapis.com for UpdatePolicy calls where defaultAdmissionRule.evaluationMode transitions to ALWAYS_ALLOW.
  • Per-cluster admission-rule edits where attestor lists shrink or are cleared via clusterAdmissionRules patches.
  • Container Analysis attestation deletes or signing-key rotations without the corresponding rebuild signal in CI — visible via containeranalysis.googleapis.com Notes.delete and KMS asymmetric-key operations on the signing key.

Query

logName=~"projects/.*/logs/cloudaudit.googleapis.com%2Factivity"
          AND protoPayload.serviceName="binaryauthorization.googleapis.com"
          AND (protoPayload.methodName=~".*UpdatePolicy"
               OR protoPayload.methodName=~".*DeleteAttestor")
          AND (protoPayload.request.policy.defaultAdmissionRule.evaluationMode="ALWAYS_ALLOW"
               OR resource.type="binaryauthorization.googleapis.com/Attestor")

Pair with a Cloud Logging filter on resource.type="k8s_cluster" with protoPayload.response.status.message=~".*denied by Binary Authorization.*" — a sustained drop in denials is the signal that the policy is effectively bypassed even when no explicit policy update is logged.

Alert threshold

  • Page on any policy-update event that switches the default or a per-cluster admission rule to ALWAYS_ALLOW.
  • Page on attestor deletes; an attestor is a long-lived security artefact, deletion should match a documented cluster decommission.

Initial response

  1. Pull the prior policy via Cloud Asset Inventory history; identify which admission rule was loosened and which images were admitted in the gap window via k8s_cluster pods.create entries.
  2. Restore the policy with gcloud container binauthz policy import against the captured baseline; quarantine any pod whose image was not previously attested by isolating the namespace via NetworkPolicy.
  3. Re-attest only post-quarantine; rebuild from source if the image provenance chain cannot be verified, and rotate the signing key if the attestor delete was paired with a KMS asymmetric-key operation.

References

gcp-k8s-05 ! HIGH PREVENTIVE

GKE Autopilot: Shielded Nodes are enabled by default and are not individually configurable — Google manages the node boot process. Autopilot nodes run with Secure Boot, vTPM, and integrity monitoring automatically. GKE Standard: Enable Shielded Nodes at the node-pool level with --shielded-secure-boot, --shielded-vtpm, and --shielded-integrity-monitoring. CIS GKE Autopilot Benchmark v1.3.0 documents Autopilot-specific defaults.

Enable Shielded GKE Nodes to protect the node boot process using Secure Boot (prevents loading unsigned kernel modules at boot), vTPM (virtual Trusted Platform Module for cryptographic attestation of the boot sequence), and integrity monitoring (detects changes to the measured boot baseline by comparing boot measurements against known-good values). This prevents rootkit installation at the node level — an attacker who escapes a container cannot silently persist a kernel-level implant across node restarts. This is a GKE-unique hardware-based control with no direct equivalent in EKS or AKS standard configurations.

Remediation — Terraform

# Terraform Google provider ~> 6.0
resource "google_container_node_pool" "hardened_nodes" {
  name       = "hardened-node-pool"
  cluster    = google_container_cluster.hardened.name
  location   = var.region

  node_config {
    shielded_instance_config {
      enable_secure_boot          = true
      enable_integrity_monitoring = true
    }

    # Container-Optimized OS with containerd runtime
    image_type = "COS_CONTAINERD"
  }
}

Remediation — gcloud

gcloud container node-pools create NODE_POOL \
  --cluster=CLUSTER_NAME \
  --shielded-secure-boot \
  --shielded-integrity-monitoring \
  --image-type=COS_CONTAINERD \
  --region REGION

Remediation — Config Connector

apiVersion: container.cnrm.cloud.google.com/v1beta1
kind: ContainerNodePool
metadata:
  name: shielded-pool
  namespace: config-control
spec:
  location: us-central1
  clusterRef:
    name: hardened-gke
  nodeConfig:
    shieldedInstanceConfig:
      enableSecureBoot: true
      enableIntegrityMonitoring: true
    workloadMetadataConfig:
      mode: GKE_METADATA

Compliance mapping

Control Severity Type Provider CIS Kubernetes Benchmark v1.11.0 CIS Google Kubernetes Engine (GKE) Benchmark v1.9.0 NIST SP 800-53 rev5 ISO/IEC 27001:2022 ISO/IEC 27017:2015 NIST SP 800-190 (Sep 2017) NSA/CISA Kubernetes Hardening Guide v1.2
gcp-k8s-05 HIGH PREVENTIVE GCP GKE n/a (GKE-specific — no CIS Kubernetes Benchmark v1.11.0 section) n/a (verify against CIS GKE Benchmark v1.9.0 PDF) SI-7; SC-28; CM-6 A.8.9; A.7.8 CLD.9.5.2 NIST SP 800-190 §4.4 (Container runtime) NSA/CISA Kubernetes Hardening Guide v1.2 §4 (Worker node security)

Log signals

  • Node-pool updates where shieldedInstanceConfig.enableSecureBoot or enableIntegrityMonitoring transitions to false.
  • New node-pool creates that omit the shieldedInstanceConfig block entirely — visible on container.googleapis.com CreateNodePool with an absent shieldedInstanceConfig field.
  • Integrity-monitoring violations from individual nodes surfaced in Cloud Logging via compute.googleapis.com/integrity log entries — these are the runtime correlate, not just the config drift.

Query

logName=~"projects/.*/logs/cloudaudit.googleapis.com%2Factivity"
          AND resource.type="gke_node_pool"
          AND (protoPayload.methodName="google.container.v1.ClusterManager.UpdateNodePool"
               OR protoPayload.methodName="google.container.v1.ClusterManager.CreateNodePool")
          AND (protoPayload.request.update.desiredNodePoolAutoConfig.shieldedInstanceConfig.enableSecureBoot=false
               OR protoPayload.request.nodePool.config.shieldedInstanceConfig.enableIntegrityMonitoring=false)

Run this Cloud Logging filter alongside a saved query on compute.googleapis.com/integrity entries to bring node-launch posture and node-runtime posture into the same dashboard pane; pin both to a single Cloud Monitoring alert policy keyed on node-pool name.

Alert threshold

  • Page on any node-pool that ships without Secure Boot + Integrity Monitoring; the inventory-wide rate of mis-configured pools should be zero.
  • Page on the first integrity-monitoring violation from any node — vTPM measurements diverging from the launch baseline indicate boot-stage tampering.

Initial response

  1. Capture the node-pool spec and the principal who issued the update; if the change is unsanctioned, recreate the pool with shielded config restored and migrate workloads via a draining rollout.
  2. Quarantine any node showing an integrity-monitoring violation: cordon, drain, snapshot the boot disk for forensics, then delete and re-bootstrap via the node-pool's standard image.
  3. If multiple nodes diverged, audit the image stream supplying the node-pool — a tampered node image is the upstream root cause and any deployed workload in the affected window is candidate-compromised.

References

gcp-k8s-06 ! HIGH DETECTIVE

GKE Autopilot: Cloud Audit Log Admin Activity logs are enabled by default in both Autopilot and Standard clusters. Data Access audit logs for container.googleapis.com are OFF by default in BOTH modes and must be explicitly enabled. GKE Standard: Same requirement — enable Data Access logs via the project IAM audit configuration. CIS GKE Autopilot Benchmark v1.3.0 documents Autopilot-specific defaults.

Enable Cloud Audit Logs Data Access for container.googleapis.com in your GCP project IAM audit configuration. Data Access logs — covering ADMIN_READ, DATA_READ, and DATA_WRITE operations — are disabled by default and must be explicitly enabled. Without Data Access logs, API operations such as kubectl exec, kubectl log, pod creation, and secret reads are not logged to Cloud Audit Logs. Admin Activity logs (cluster creation, deletion, RBAC changes) are automatically enabled and cannot be disabled.

Remediation — Terraform

# Terraform Google provider ~> 6.0
resource "google_project_iam_audit_config" "gke_audit" {
  project = var.project_id
  service = "container.googleapis.com"

  audit_log_config {
    log_type = "DATA_READ"
  }
  audit_log_config {
    log_type = "DATA_WRITE"
  }
  audit_log_config {
    log_type = "ADMIN_READ"
  }
}

Remediation — gcloud

# Check current audit configuration
gcloud projects get-iam-policy PROJECT_ID --format=json | jq '.auditConfigs'

# Enable Data Access logs via policy file
# Create audit-policy.json with the auditConfigs block, then:
gcloud projects set-iam-policy PROJECT_ID audit-policy.json

Remediation — Config Connector

apiVersion: logging.cnrm.cloud.google.com/v1beta1
kind: LoggingLogSink
metadata:
  name: gke-audit-sink
  namespace: config-control
spec:
  projectRef:
    external: "projects/PROJECT_ID"
  destination:
    bigQueryDatasetRef:
      external: "projects/PROJECT_ID/datasets/gke_audit"
  filter: 'resource.type="k8s_cluster" AND logName:"cloudaudit.googleapis.com"'

Compliance mapping

Control Severity Type Provider CIS Kubernetes Benchmark v1.11.0 CIS Google Kubernetes Engine (GKE) Benchmark v1.9.0 NIST SP 800-53 rev5 ISO/IEC 27001:2022 ISO/IEC 27017:2015 NIST SP 800-190 (Sep 2017) NSA/CISA Kubernetes Hardening Guide v1.2
gcp-k8s-06 HIGH DETECTIVE GCP GKE §1.2.22 (audit log — verify against CIS Kubernetes Benchmark v1.11.0 PDF) n/a (verify against CIS GKE Benchmark v1.9.0 PDF) AU-2; AU-12; SI-4 A.8.15; A.8.16 CLD.12.4.5 NIST SP 800-190 §4.4.3 NSA/CISA Kubernetes Hardening Guide v1.2 §6 (Audit logging)

Log signals

  • Log Router sink edits on the GKE/container audit feed: logging.googleapis.com ConfigServiceV2.UpdateSink or DeleteSink on sinks whose filter includes resource.type="k8s_cluster".
  • Data-access log opt-outs on the project: SetIamPolicy against the audit-log config removing DATA_READ/DATA_WRITE exempted-members lists that previously captured GKE control-plane data access.
  • Volume-based detection: a sustained drop in k8s_cluster log-line rate against rolling baseline indicates ingestion has been blocked even when no explicit sink edit is logged.

Query

logName=~"projects/.*/logs/cloudaudit.googleapis.com%2Factivity"
          AND protoPayload.serviceName="logging.googleapis.com"
          AND (protoPayload.methodName="google.logging.v2.ConfigServiceV2.UpdateSink"
               OR protoPayload.methodName="google.logging.v2.ConfigServiceV2.DeleteSink")
          AND protoPayload.request.sink.filter=~".*k8s_cluster.*"

Stream the Cloud Logging filter into a log-based metric counted per sink name; pair with a Cloud Monitoring alert that fires when the rolling 1h ingestion rate from any resource.type="k8s_cluster" source drops below 30% of the 30-day baseline — the absence-of-signal pattern catches stealthy filter relaxations.

Alert threshold

  • Page on any sink delete or filter mutation that removes k8s_cluster from the GKE audit feed; sinks are configuration-as-code artefacts and console edits warrant immediate review.
  • Page when the 1h ingestion-rate ratio falls below 30% of baseline across any production cluster.

Initial response

  1. Restore the sink from Cloud Asset Inventory history via gcloud logging sinks update with the captured filter; verify ingestion resumes by tailing the sink destination (BigQuery dataset or Cloud Storage bucket).
  2. Treat the gap window as a forensic blackout: pull node-local kubelet logs from any affected node (if still present) and reconstruct API-server activity from etcd audit if available; document the gap in the incident timeline.
  3. Re-pin sink IAM to deny logging.sinks.update to all but a break-glass principal, and add a Cloud Asset Inventory feed so future sink-config changes generate a Pub/Sub event independent of the audit log itself.

References

Equivalent controls in other providers: EKS control-plane logs, AKS control-plane audit logs, OKE OCI audit logging.

gcp-k8s-07 ! HIGH PREVENTIVE

GKE Autopilot: VPC-native networking is enforced by default. Dataplane V2 (Cilium-based) is also the default in Autopilot, making NetworkPolicy enforcement available without additional configuration. GKE Standard: Create the cluster with --enable-dataplane-v2 (or legacy --enable-network-policy for Calico) and --enable-ip-alias for VPC-native networking. NetworkPolicy objects are not enforced without a CNI that supports them. CIS GKE Autopilot Benchmark v1.3.0 documents Autopilot-specific defaults.

Create a VPC-native GKE cluster (alias IP ranges — NOT routes-based legacy networking) and enable GKE Dataplane V2 for NetworkPolicy enforcement via Cilium. Routes-based networking is deprecated and does not support NetworkPolicy. VPC-native enables private IP ranges for pods, reducing the routing blast radius of a compromised pod. Without a NetworkPolicy-capable CNI and explicit deny rules, every pod in the cluster can reach every other pod and the node metadata endpoint on the flat pod network.

Remediation — Terraform

# Terraform Google provider ~> 6.0
resource "google_container_cluster" "hardened" {
  name     = "hardened-cluster"
  location = var.region

  # VPC-native cluster (alias IP ranges — required for NetworkPolicy)
  ip_allocation_policy {}

  # Dataplane V2 enables NetworkPolicy enforcement via Cilium
  datapath_provider = "ADVANCED_DATAPATH"

  private_cluster_config {
    enable_private_nodes    = true
    enable_private_endpoint = true
    master_ipv4_cidr_block  = "172.16.0.0/28"
  }
}

Remediation — gcloud

gcloud container clusters create CLUSTER_NAME \
  --enable-ip-alias \
  --enable-dataplane-v2 \
  --region REGION

# Apply default-deny NetworkPolicy to every namespace
kubectl apply -f default-deny-all.yaml --namespace NAMESPACE

Default-deny NetworkPolicy

# Default-deny NetworkPolicy — apply in every namespace before adding allows
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress

Remediation — Config Connector

apiVersion: container.cnrm.cloud.google.com/v1beta1
kind: ContainerCluster
metadata:
  name: dpv2-gke
  namespace: config-control
spec:
  location: us-central1
  initialNodeCount: 1
  datapathProvider: ADVANCED_DATAPATH
  networkPolicy:
    enabled: true
    provider: PROVIDER_UNSPECIFIED

Compliance mapping

Control Severity Type Provider CIS Kubernetes Benchmark v1.11.0 CIS Google Kubernetes Engine (GKE) Benchmark v1.9.0 NIST SP 800-53 rev5 ISO/IEC 27001:2022 ISO/IEC 27017:2015 NIST SP 800-190 (Sep 2017) NSA/CISA Kubernetes Hardening Guide v1.2
gcp-k8s-07 HIGH PREVENTIVE GCP GKE §5.3 (Network policies — verify against CIS Kubernetes Benchmark v1.11.0 PDF) n/a (verify against CIS GKE Benchmark v1.9.0 PDF) SC-7; SC-5; AC-4 A.8.20; A.8.22 CLD.9.5.1 NIST SP 800-190 §4.4.1 NSA/CISA Kubernetes Hardening Guide v1.2 §2 (Network separation)

Log signals

  • Cluster updates where networkPolicy.enabled transitions to false, or datapathProvider moves away from ADVANCED_DATAPATH.
  • NetworkPolicy resource deletes at scale within a namespace — visible on resource.type="k8s_cluster" for networking.k8s.io/v1/namespaces/*/networkpolicies DELETE verbs.
  • Dataplane V2 flow-log entries dropping below a baseline rate while pod traffic remains constant — signals that flow visibility has been silently disabled.

Query

logName=~"projects/.*/logs/cloudaudit.googleapis.com%2Factivity"
          AND resource.type="gke_cluster"
          AND protoPayload.methodName="google.container.v1.ClusterManager.UpdateCluster"
          AND (protoPayload.request.update.desiredDatapathProvider="LEGACY_DATAPATH"
               OR protoPayload.request.update.desiredNetworkPolicy.enabled=false)

Pair this Cloud Logging filter with a Dataplane V2 flow-log query (resource.type="k8s_cluster" with log_id("dataplane-v2-flows")) so dataplane regressions show up alongside config regressions in the same alert policy.

Alert threshold

  • Page on any cluster regressing from advanced dataplane or disabling NetworkPolicy enforcement; both decisions remove the enforcement plane that holds pod-to-pod isolation.
  • Surface (do not page) bulk NetworkPolicy deletes inside a namespace — confirm with the namespace owner whether the deletes match an intentional CRD refactor or are unauthorised lateral-movement preparation.

Initial response

  1. Re-enable NetworkPolicy enforcement (gcloud container clusters update --enable-network-policy) and recreate the canonical default-deny policies in every namespace from the captured GitOps baseline.
  2. If Dataplane V2 was downgraded, plan a controlled rollback to the advanced datapath; the migration is in-place but requires node-pool rolling restart and may take an hour per pool.
  3. Audit pod-to-pod connections during the gap window via VPC Flow Logs and Dataplane V2 flow-log history; treat any pod that initiated a connection outside its documented service-graph as candidate-compromised.

References

Equivalent controls in other providers: EKS default-deny NetworkPolicy, AKS Azure CNI network policy, OKE Calico network policy.

gcp-k8s-08 ! HIGH PREVENTIVE

GKE Autopilot: Autopilot enforces the Baseline Pod Security Standards profile by default on all namespaces — privileged containers, hostNetwork, hostPID, and hostPath mounts are blocked without explicit configuration. GKE Standard: Apply namespace labels to enforce PSS profiles: pod-security.kubernetes.io/enforce: restricted. The Privileged profile should only be permitted in kube-system with documented justification. CIS GKE Autopilot Benchmark v1.3.0 documents Autopilot-specific defaults.

Enforce Pod Security Standards (PSS) via the built-in PodSecurity admission controller (available and stable since Kubernetes 1.23). Use namespace labels to declare the security profile. Target the restricted profile for all application namespaces; baseline as the minimum for legacy workloads that cannot yet be ported to restricted. Avoid the privileged profile outside kube-system. PSS is the successor to the PSP admission controller (removed in Kubernetes 1.25) — use only the PodSecurity namespace label approach for all new and migrated configurations.

Remediation — Namespace PSS labels (YAML)

# Enforce PSS Restricted profile on namespace (gcp/kubernetes.html GKE-08)
apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted

Remediation — gcloud / kubectl

# Label namespace to enforce restricted PSS profile
kubectl label namespace NAMESPACE \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/enforce-version=latest

# Verify applied labels
kubectl get namespace NAMESPACE -o jsonpath='{.metadata.labels}'

Remediation — Config Connector

apiVersion: container.cnrm.cloud.google.com/v1beta1
kind: ContainerCluster
metadata:
  name: pss-gke
  namespace: config-control
spec:
  location: us-central1
  initialNodeCount: 1
  podSecurityPolicyConfig:
    enabled: false  # PSP deprecated — enforce PSS via admission controller
  enableShieldedNodes: true

Compliance mapping

Control Severity Type Provider CIS Kubernetes Benchmark v1.11.0 CIS Google Kubernetes Engine (GKE) Benchmark v1.9.0 NIST SP 800-53 rev5 ISO/IEC 27001:2022 ISO/IEC 27017:2015 NIST SP 800-190 (Sep 2017) NSA/CISA Kubernetes Hardening Guide v1.2
gcp-k8s-08 HIGH PREVENTIVE GCP GKE §5.2 (Pod security — verify against CIS Kubernetes Benchmark v1.11.0 PDF) n/a (verify against CIS GKE Benchmark v1.9.0 PDF) CM-6; AC-6; SI-3 A.8.9; A.8.28 CLD.6.3.1 NIST SP 800-190 §4.2 (Container risks) NSA/CISA Kubernetes Hardening Guide v1.2 §3 (Pod security)

Log signals

  • K8s API audit entries (resource.type="k8s_cluster") where the PodSecurity admission controller emits warn/audit/enforce annotations on pod creates — particularly pod-security.kubernetes.io/enforce-policy: privileged on namespaces previously labelled restricted.
  • Namespace patches removing pod-security.kubernetes.io/enforce: restricted labels — these are silent profile downgrades.
  • Pod specs in admit log carrying securityContext.privileged=true, hostNetwork=true, or hostPID=true in production namespaces.

Query

logName=~"projects/.*/logs/cloudaudit.googleapis.com%2Fk8s_cluster"
          AND resource.type="k8s_cluster"
          AND (protoPayload.methodName="io.k8s.core.v1.pods.create"
               OR protoPayload.methodName="io.k8s.core.v1.namespaces.patch")
          AND (protoPayload.request.metadata.labels."pod-security.kubernetes.io/enforce"="privileged"
               OR protoPayload.request.spec.containers.securityContext.privileged=true
               OR protoPayload.request.spec.hostNetwork=true)

Stream this Cloud Logging filter into a log-based metric grouped by namespace; combine with a Policy Controller (Anthos Config Management) constraint-violation audit feed so both admission-deny and admission-warn events appear in the same Cloud Monitoring dashboard.

Alert threshold

  • Page on any namespace label drop from restricted to baseline or privileged on production namespaces; the label map is a configuration-as-code artefact.
  • Page on any pod create admitting privileged: true or hostNetwork: true outside a documented system namespace allow-list.

Initial response

  1. Identify the pod, its container image, and the principal that created it via protoPayload.authenticationInfo.principalEmail; capture command-line history from container stdout if the workload was serving traffic.
  2. Evict the pod, re-apply the namespace label, and quarantine the principal's RBAC binding pending review — privileged-pod creation is typically the kill-chain final step of an admission-control bypass.
  3. Trace the principal back to the credential that issued the create; rotate any Kubernetes service-account token or kubeconfig credential used and audit lateral activity via API-server logs.

References

Equivalent controls in other providers: EKS PSS, AKS Azure Policy add-on, OKE PSS.

gcp-k8s-09 ! HIGH PREVENTIVE

GKE Autopilot: Legacy ABAC, kubelet read-only port, and legacy metadata endpoints are all disabled by default in Autopilot. GKE Standard: Explicitly configure --no-enable-legacy-authorization, disable the kubelet read-only port, and use Container-Optimized OS as the node image type. CIS GKE Autopilot Benchmark v1.3.0 documents Autopilot-specific defaults.

This control bundles three node and cluster hardening items that share a common theme — disabling legacy or insecure defaults:

  1. Disable legacy ABAC (--enable-legacy-authorization=false) — ABAC was superseded by RBAC in Kubernetes 1.6. Legacy ABAC allows any authenticated user unrestricted access to the API by default. Disable it on any cluster where it may have been enabled for compatibility.
  2. Disable kubelet read-only port — kubelet port 10255 is unauthenticated and exposes pod metadata, resource consumption, and running container information without any credential. An attacker with network access to the node can enumerate all workloads and their configurations without authentication.
  3. Use Container-Optimized OS (COS) — COS is Google's hardened node operating system with a locked-down kernel, read-only root filesystem, and verified boot process. Use COS instead of Ubuntu or custom images unless a specific compatibility requirement mandates otherwise.

Remediation — Terraform

# Terraform Google provider ~> 6.0
resource "google_container_cluster" "hardened" {
  name     = "hardened-cluster"
  location = var.region

  # Disable legacy ABAC — RBAC is the only authorization mechanism
  enable_legacy_abac = false
}

resource "google_container_node_pool" "hardened_nodes" {
  name    = "hardened-node-pool"
  cluster = google_container_cluster.hardened.name

  node_config {
    # Container-Optimized OS with containerd (COS_CONTAINERD)
    image_type = "COS_CONTAINERD"

    # Shielded instance config (see GKE-05 for full shielded node config)
    shielded_instance_config {
      enable_secure_boot          = true
      enable_integrity_monitoring = true
    }
  }
}

Remediation — gcloud

gcloud container clusters create CLUSTER_NAME \
  --no-enable-legacy-authorization \
  --image-type=COS_CONTAINERD \
  --region REGION

# Verify legacy ABAC is disabled on an existing cluster
gcloud container clusters describe CLUSTER_NAME \
  --region REGION \
  --format="value(legacyAbac.enabled)"

Remediation — Config Connector

apiVersion: container.cnrm.cloud.google.com/v1beta1
kind: ContainerCluster
metadata:
  name: rbac-only-gke
  namespace: config-control
spec:
  location: us-central1
  initialNodeCount: 1
  enableLegacyAbac: false
  masterAuth:
    clientCertificateConfig:
      issueClientCertificate: false

Compliance mapping

Control Severity Type Provider CIS Kubernetes Benchmark v1.11.0 CIS Google Kubernetes Engine (GKE) Benchmark v1.9.0 NIST SP 800-53 rev5 ISO/IEC 27001:2022 ISO/IEC 27017:2015 NIST SP 800-190 (Sep 2017) NSA/CISA Kubernetes Hardening Guide v1.2
gcp-k8s-09 HIGH PREVENTIVE GCP GKE §4.2 (kubelet — verify against CIS Kubernetes Benchmark v1.11.0 PDF) n/a (verify against CIS GKE Benchmark v1.9.0 PDF) CM-7; AC-17; SI-3 A.8.9; A.8.20 CLD.9.5.2 NIST SP 800-190 §4.4.4 NSA/CISA Kubernetes Hardening Guide v1.2 §4 (Worker node hardening)

Log signals

  • Cluster updates flipping legacyAbac.enabled=true — that re-introduces the deprecated ABAC authoriser that overrides RBAC bindings.
  • Cluster updates setting enableKubernetesAlpha=true — alpha clusters disable RBAC API stability guarantees and are unsupported for production.
  • Basic-auth / client-cert legacy auth re-enablement on existing clusters: masterAuth.username set to a non-empty string after the documented removal.

Query

logName=~"projects/.*/logs/cloudaudit.googleapis.com%2Factivity"
          AND resource.type="gke_cluster"
          AND protoPayload.methodName=~".*UpdateCluster.*"
          AND (protoPayload.request.update.desiredLegacyAbac.enabled=true
               OR protoPayload.request.update.desiredEnableKubernetesAlpha=true
               OR protoPayload.request.update.desiredMasterAuth.username!="")

Use this Cloud Logging filter at organisation scope; combine with a Cloud Asset Inventory point-in-time snapshot of all clusters' legacyAbac/masterAuth fields to catch the steady-state population alongside the change stream.

Alert threshold

  • Page on any cluster update that re-enables legacy ABAC, Kubernetes Alpha, or basic-auth — these are deprecated for years and a re-enable is almost certainly malicious or misconfigured.
  • Daily inventory cron: alert on any cluster where the field state diverges from the documented zero-baseline.

Initial response

  1. Disable legacy authoriser/auth via gcloud container clusters update --no-enable-legacy-authorization and force-rotate the master credentials.
  2. Audit RBAC bindings issued during the gap window — ABAC overrides RBAC denies, so any role grant in the window may have taken effect even if RBAC alone would have refused it.
  3. If alpha mode was enabled, replace the cluster — alpha is non-upgradeable and Google does not patch security CVEs on alpha clusters; data-plane workloads must be re-deployed to a fresh stable cluster.

References

Equivalent controls in other providers: EKS IMDSv2 enforcement, AKS managed cluster identity, OKE IAM least privilege.

gcp-k8s-10 ! MEDIUM PREVENTIVE

GKE Autopilot: gVisor (runsc) is NOT available for standard Autopilot pods — Autopilot's isolation model differs from Standard. GKE Sandbox (which uses gVisor) is a separate Autopilot configuration requiring runtimeClassName: gvisor-autopilot. GKE Standard: Create a node pool with --sandbox-type=gvisor and create a RuntimeClass object with handler: runsc; assign runtimeClassName: gvisor in the pod spec. CIS GKE Autopilot Benchmark v1.3.0 documents Autopilot-specific defaults.

Enable GKE Sandbox (gVisor) to provide an additional isolation layer for untrusted or externally-sourced workloads. gVisor (runsc) intercepts system calls from the container at the user-space level, running a guest kernel in user space that translates syscalls before they reach the host kernel. This significantly reduces the host kernel attack surface for container workloads. MEDIUM severity reflects that gVisor is a defense-in-depth additive layer — it does not replace Pod Security Standards enforcement, network policy, or Workload Identity, but reduces the blast radius of a zero-day container exploit. Not all workloads are compatible with gVisor's system-call subset; test compatibility before enabling in production.

Remediation — RuntimeClass (YAML)

# RuntimeClass for gVisor-isolated workloads (GKE Sandbox)
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: gvisor
handler: runsc
---
# Apply to a Pod — assigns gVisor as the container runtime
apiVersion: v1
kind: Pod
metadata:
  name: sandboxed-workload
spec:
  runtimeClassName: gvisor
  containers:
    - name: app
      image: gcr.io/MY_PROJECT/my-app:DIGEST

Remediation — Terraform

# Terraform Google provider ~> 6.0
resource "google_container_node_pool" "sandbox_pool" {
  name    = "gvisor-sandbox-pool"
  cluster = google_container_cluster.hardened.name

  node_config {
    sandbox_config {
      sandbox_type = "gvisor"
    }
    image_type = "COS_CONTAINERD"
  }
}

Remediation — gcloud

gcloud container node-pools create SANDBOX_POOL \
  --cluster=CLUSTER_NAME \
  --sandbox-type=gvisor \
  --image-type=COS_CONTAINERD \
  --region REGION

Remediation — Config Connector

apiVersion: container.cnrm.cloud.google.com/v1beta1
kind: ContainerNodePool
metadata:
  name: gvisor-sandbox-pool
  namespace: config-control
spec:
  location: us-central1
  clusterRef:
    name: hardened-gke
  nodeConfig:
    sandboxConfig:
      sandboxType: gvisor
    machineType: e2-standard-4

Compliance mapping

Control Severity Type Provider CIS Kubernetes Benchmark v1.11.0 CIS Google Kubernetes Engine (GKE) Benchmark v1.9.0 NIST SP 800-53 rev5 ISO/IEC 27001:2022 ISO/IEC 27017:2015 NIST SP 800-190 (Sep 2017) NSA/CISA Kubernetes Hardening Guide v1.2
gcp-k8s-10 MEDIUM PREVENTIVE GCP GKE n/a (GKE-specific — no CIS Kubernetes Benchmark v1.11.0 section) n/a (verify against CIS GKE Benchmark v1.9.0 PDF) SI-3; SC-39 A.8.28 CLD.9.5.2 NIST SP 800-190 §4.2 (Container risks) NSA/CISA Kubernetes Hardening Guide v1.2 §3 (Pod isolation)

Log signals

  • Node-pool updates clearing sandboxConfig.type=gvisor on pools intended to host untrusted workloads.
  • Pod creates landing on non-sandboxed pools when the pod spec lacks the runtimeClassName: gvisor hint — visible in k8s_cluster audit entries on pods.create.
  • RuntimeClass deletes on the cluster removing the gvisor class entirely — surfaced via node.k8s.io/v1/runtimeclasses DELETE verbs.

Query

logName=~"projects/.*/logs/cloudaudit.googleapis.com%2Factivity"
          AND resource.type="gke_node_pool"
          AND protoPayload.methodName="google.container.v1.ClusterManager.UpdateNodePool"
          AND NOT protoPayload.request.update.desiredNodePoolAutoConfig.sandboxConfig.type="gvisor"

This Cloud Logging filter catches node-pool config drift; pair with a saved query on resource.type="k8s_cluster" for pod creates in tagged-untrusted namespaces that omit the runtimeClassName field, so config drift and admission-time bypass surface together.

Alert threshold

  • Page on any update that removes gvisor from a node-pool whose name carries the documented untrusted-workload prefix.
  • Page on RuntimeClass deletes that target the gvisor class; the class is meant to live for the lifetime of the cluster.

Initial response

  1. Recreate the node-pool with --sandbox=type=gvisor and re-apply the cluster RuntimeClass manifest from GitOps; cordon and drain the affected pool to evict any pod that started under the standard runtime.
  2. Identify untrusted workloads that ran on the regular kernel during the gap window — assume the kernel-isolation guarantee was unavailable and treat any kernel-syscall-reach (e.g. ptrace, perf_event_open) telemetry from those pods as candidate-exploitation.
  3. Rotate any host-level credential the pod might have reached via the lapsed sandbox boundary (node service-account tokens, host-network port secrets) and snapshot the node boot disk for forensic review.

References

Sources