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
Remediation
Manual Steps
-
Identify disks in use by the cluster (pods + node boot disks)
- On any machine with
gcloudaccess:PROJECT_ID="your-project-id"CLUSTER_NAME="your-cluster-name"CLUSTER_LOCATION="your-cluster-location" # zone or regiongcloud 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; doecho "== $NAME ($ZONE) =="gcloud compute instances describe "$NAME" \--project "$PROJECT_ID" \--zone "$ZONE" \--format="json|jq '.disks[].diskEncryptionKey.kmsKeyName'"done
- On any machine with
-
Review PDs backing PersistentVolumes for CMEK usage
- For each PV in the cluster, get its disk name and location (any machine with
kubectlandgcloud):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.kmsKeyNameisnull, that disk is not CMEK-encrypted and should be considered for migration.
- For each PV in the cluster, get its disk name and location (any machine with
-
Check StorageClasses used for dynamic PD provisioning
- On any machine with
kubectlaccess:kubectl get storageclasskubectl 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.
- On any machine with
-
Review KMS key configuration and IAM access
- On any machine with
gcloudaccess, list KMS keys intended for GKE use:LOCATION="kms-location" # e.g. us-central1KEYRING="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.cryptoKeyEncrypterDecrypteron 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.
- On any machine with
-
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
bootDiskKmsKeyand CMEK-enabled StorageClasses (viakmsKeyName/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.
- For new clusters / node pools, follow the GKE CMEK guide to create or update clusters with CMEK-specified
-
Re-verify after changes
- For newly created or migrated PVs, re-run:
Confirm it shows the intended KMS key.gcloud compute disks describe "$PV_NAME" \--location "$LOCATION" \--project "$PROJECT_ID" \--format json | jq '.diskEncryptionKey.kmsKeyName'
- Re-check
StorageClassdefinitions to ensure all default / actively used classes specify the appropriate CMEK key parameter.
- For newly created or migrated PVs, re-run:
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_namecontains 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 yamlgcloud compute disks describe <disk> --zone <zone> --project <project>