Skip to main content

Enable Customer-Managed Encryption Keys (Cmek) For Gke

More Info:

Use Customer-Managed Encryption Keys (Cmek) To Encrypt Node Boot And Dynamicallyprovisioned Attached Google Compute Engine Persistent Disks (Pds) Using Keys Managed Within Cloud Key Management Service

Risk Level

Low

Address

Security

Compliance Standards

  • CIS GKE
  • Securities and Exchange Board of India (SEBI) - Cloud Security Adoption Framework

Triage and Remediation

Remediation

Manual Steps
  1. Identify disks in use by the cluster (pods + node boot disks)

    • On any machine with gcloud access:
      PROJECT_ID="your-project-id"
      CLUSTER_NAME="your-cluster-name"
      CLUSTER_LOCATION="your-cluster-location" # zone or region

      gcloud container clusters describe "$CLUSTER_NAME" \
      --project "$PROJECT_ID" \
      --location "$CLUSTER_LOCATION" \
      --format="json|jq '.nodePools[].config.diskType,.nodePools[].config.bootDiskKmsKey'"
      gcloud compute instances list \
      --project "$PROJECT_ID" \
      --format="value(name,zone)" | while read NAME ZONE; do
      echo "== $NAME ($ZONE) =="
      gcloud compute instances describe "$NAME" \
      --project "$PROJECT_ID" \
      --zone "$ZONE" \
      --format="json|jq '.disks[].diskEncryptionKey.kmsKeyName'"
      done
  2. Review PDs backing PersistentVolumes for CMEK usage

    • For each PV in the cluster, get its disk name and location (any machine with kubectl and gcloud):
      kubectl get pv -o wide
    • For each PD found (example disk name and zone):
      PV_NAME="pd-disk-name"
      LOCATION="zone-or-region"
      PROJECT_ID="your-project-id"

      gcloud compute disks describe "$PV_NAME" \
      --location "$LOCATION" \
      --project "$PROJECT_ID" \
      --format json | jq '.diskEncryptionKey.kmsKeyName'
    • Decide: if .diskEncryptionKey.kmsKeyName is null, that disk is not CMEK-encrypted and should be considered for migration.
  3. Check StorageClasses used for dynamic PD provisioning

    • On any machine with kubectl access:
      kubectl get storageclass
      kubectl get storageclass -o yaml
    • For each StorageClass, look for:
      • parameters.kmsKeyName (GCE PD driver)
      • Or parameters.csi.storage.gke.io/kms-key (GKE PD CSI driver)
    • Decide: StorageClasses without a CMEK parameter will provision non‑CMEK disks and should be updated or replaced.
  4. Review KMS key configuration and IAM access

    • On any machine with gcloud access, list KMS keys intended for GKE use:
      LOCATION="kms-location" # e.g. us-central1
      KEYRING="your-keyring"

      gcloud kms keys list \
      --location "$LOCATION" \
      --keyring "$KEYRING"
    • Check that the GKE node service accounts and GKE PD CSI SA (if used) have appropriate cloudkms.cryptoKeyEncrypterDecrypter on the chosen key:
      KEY_NAME="your-key-name"

      gcloud kms keys get-iam-policy "$KEY_NAME" \
      --location "$LOCATION" \
      --keyring "$KEYRING"
    • Decide: ensure only intended principals are allowed and that GKE identities can use the key.
  5. Plan and apply configuration changes to enable CMEK

    • For new clusters / node pools, follow the GKE CMEK guide to create or update clusters with CMEK-specified bootDiskKmsKey and CMEK-enabled StorageClasses (via kmsKeyName/CSI parameter).
    • For existing non‑CMEK disks, plan a migration: create CMEK-enabled StorageClasses, create new CMEK-backed PVs, and migrate data (e.g., backup/restore, application-level replication) because encryption type cannot be changed in-place.
  6. Re-verify after changes

    • For newly created or migrated PVs, re-run:
      gcloud compute disks describe "$PV_NAME" \
      --location "$LOCATION" \
      --project "$PROJECT_ID" \
      --format json | jq '.diskEncryptionKey.kmsKeyName'
      Confirm it shows the intended KMS key.
    • Re-check StorageClass definitions to ensure all default / actively used classes specify the appropriate CMEK key parameter.
Using kubectl

kubectl cannot configure Customer-Managed Encryption Keys for GKE node boot disks or dynamically provisioned persistent disks; this is set at the GKE/Compute Engine and Cloud KMS level via the Google Cloud console, gcloud, or IaC. Use kubectl only to identify affected PersistentVolumes if needed, then follow the guidance in the Manual Steps section to apply CMEK at the cloud provider configuration layer.

Automation
#!/usr/bin/env bash
#
# Report GKE PD encryption (CMEK vs Google-managed) for all PersistentVolumes
# that use GCE Persistent Disk in the current cluster.
#
# REQUIREMENTS:
# - Run on: any machine with kubectl and gcloud configured
# - kubectl context set to target cluster
# - gcloud project set (or pass --project explicitly)
#
# USAGE:
# export PROJECT_ID="my-gcp-project" # optional if gcloud default is set
# ./check_gke_pv_cmek.sh

set -euo pipefail

PROJECT_ID="${PROJECT_ID:-$(gcloud config get-value project 2>/dev/null || true)}"

if [[ -z "$PROJECT_ID" || "$PROJECT_ID" == "(unset)" ]]; then
echo "ERROR: PROJECT_ID is not set and gcloud default project is unset."
echo "Set it with: export PROJECT_ID='my-gcp-project'"
exit 1
fi

echo "Using GCP project: $PROJECT_ID" >&2

# List all GCE PD-backed PVs
echo "Collecting PersistentVolumes using GCE Persistent Disks..." >&2
pv_json=$(kubectl get pv -o json)

# Extract relevant fields: PV name, storage class, and PD identifier
# Supports both:
# - pdName (kubernetes.io/gce-pd)
# - volumeHandle (CSI pd.csi.storage.gke.io)
echo "pv_name,storage_class,driver,type,pd_identifier,zone_or_region,kms_key_name"

echo "$pv_json" | jq -r '.items[]
| {
pv_name: .metadata.name,
storage_class: (.spec.storageClassName // ""),
gce_pd_name: (
( .spec.gcePersistentDisk.pdName // "" ) as $pd
| if $pd == "" then null else $pd end
),
csi_driver: (.spec.csi.driver // ""),
csi_handle: (.spec.csi.volumeHandle // "")
}
| select(.gce_pd_name != null or .csi_driver == "pd.csi.storage.gke.io")
| if .gce_pd_name != null then
{
pv_name: .pv_name,
storage_class: .storage_class,
driver: "in-tree-gce-pd",
type: "zonal-or-regional-unknown",
pd_identifier: .gce_pd_name
}
else
# CSI PD handles look like:
# projects/PROJECT/zones/ZONE/disks/DISK
# projects/PROJECT/regions/REGION/disks/DISK
# or similar variants
( .csi_handle
| capture("^projects/(?<project>[^/]+)/(?<loc_type>zones|regions)/(?<location>[^/]+)/disks/(?<disk>.+)$")
) as $h
| {
pv_name: .pv_name,
storage_class: .storage_class,
driver: "csi-pd",
type: $h.loc_type,
pd_identifier: $h.disk,
location: $h.location
}
end
| @tsv' | while IFS=$'\t' read -r PV_NAME STORAGE_CLASS DRIVER TYPE PD_ID LOCATION; do

# Determine API location flag and value for gcloud based on TYPE
LOCATION_FLAG=""
LOCATION_VALUE=""

case "$DRIVER" in
in-tree-gce-pd)
# For in-tree PD we don't know zone/region from PV alone.
# Try to find from an attached node (best-effort); if not found, skip.
# This is a heuristic: we inspect nodes with this PV mounted.
NODE=$(kubectl get pods --all-namespaces -o json \
| jq -r --arg pv "$PV_NAME" '
.items[]
| select(.spec.volumes[]? | .persistentVolumeClaim? | .persistentVolumeClaim.claimName != null)
| .spec.nodeName' \
| head -n1)

if [[ -z "$NODE" || "$NODE" == "null" ]]; then
echo "$PV_NAME,$STORAGE_CLASS,$DRIVER,$TYPE,$PD_ID,UNKNOWN,UNKNOWN (node/zone not inferred)"
continue
fi

ZONE=$(kubectl get node "$NODE" -o json \
| jq -r '.metadata.labels["topology.gke.io/zone"] // .metadata.labels["topology.kubernetes.io/zone"] // empty')

if [[ -z "$ZONE" ]]; then
echo "$PV_NAME,$STORAGE_CLASS,$DRIVER,$TYPE,$PD_ID,UNKNOWN,UNKNOWN (zone label missing)"
continue
fi

LOCATION_FLAG="--zone"
LOCATION_VALUE="$ZONE"
;;
csi-pd)
if [[ "$TYPE" == "zones" ]]; then
LOCATION_FLAG="--zone"
LOCATION_VALUE="$LOCATION"
elif [[ "$TYPE" == "regions" ]]; then
LOCATION_FLAG="--region"
LOCATION_VALUE="$LOCATION"
else
echo "$PV_NAME,$STORAGE_CLASS,$DRIVER,$TYPE,$PD_ID,UNKNOWN,UNKNOWN (unrecognized CSI handle type)"
continue
fi
;;
*)
echo "$PV_NAME,$STORAGE_CLASS,$DRIVER,$TYPE,$PD_ID,UNKNOWN,UNKNOWN (unsupported driver)"
continue
;;
esac

# Query GCE disk encryption key
KMS_KEY_NAME=$(gcloud compute disks describe "$PD_ID" \
$LOCATION_FLAG "$LOCATION_VALUE" \
--project "$PROJECT_ID" \
--format json 2>/dev/null \
| jq -r '.diskEncryptionKey.kmsKeyName // "GOOGLE-MANAGED-DEFAULT"')

echo "$PV_NAME,$STORAGE_CLASS,$DRIVER,$TYPE,$PD_ID,${LOCATION_VALUE:-UNKNOWN},$KMS_KEY_NAME"
done

Explanation of output and what indicates a problem:

  • The script prints a CSV header:
    • pv_name,storage_class,driver,type,pd_identifier,zone_or_region,kms_key_name
  • For each GCE PD-backed PersistentVolume:
    • kms_key_name = GOOGLE-MANAGED-DEFAULT
      → The disk is not using CMEK (uses Google-managed keys) and is non-compliant with the intent of CIS GKE 5.9.1.
    • kms_key_name contains a full KMS resource, for example:
      projects/PROJECT/locations/LOCATION/keyRings/RING/cryptoKeys/KEY
      → The disk is using CMEK (candidate for compliance; you still must review that the key/rotation/policy meet your org’s requirements).
  • Lines with UNKNOWN (...) in the last field mean the script could not determine encryption status for that PV (for example, missing zone or node information). These need manual investigation with:
    • kubectl get pv <pv_name> -o yaml
    • gcloud compute disks describe <disk> --zone <zone> --project <project>

Additional Reading: