Enable Customer-Managed Encryption Keys (CMEK) For Boot
More Info:
Encrypt GKE node boot disks with Customer-Managed Encryption Keys so the key lifecycle for node disks is under your control. CMEK provides stronger governance than default encryption.
Risk Level
Medium
Address
Security
Compliance Standards
- CIS GKE
Triage and Remediation
- Remediation
Remediation
Manual Steps
-
Identify node pools and check current boot disk CMEK use
- On any machine with
gcloudconfigured for the project:For each node pool name returned:PROJECT_ID="your-project-id"LOCATION="your-cluster-location" # e.g. us-central1, europe-west1-bCLUSTER_NAME="your-cluster-name"gcloud container node-pools list \--cluster "${CLUSTER_NAME}" \--location "${LOCATION}" \--project "${PROJECT_ID}" \--format="table(name)"NODE_POOL="name-from-above"gcloud container node-pools describe "${NODE_POOL}" \--cluster "${CLUSTER_NAME}" \--location "${LOCATION}" \--project "${PROJECT_ID}" \--format="table(name, config.diskType, config.bootDiskKmsKey)"- If
config.bootDiskKmsKeyis empty, that node pool’s boot disks are not using CMEK.
- If
- On any machine with
-
Review CMEK policy/requirements with security/compliance
- Confirm which clusters/node pools must use CMEK, what key project/location/ring/key must be used, and whether any node pools are exempt (e.g. non‑prod, short‑lived test).
- Decide whether to migrate all non‑CMEK node pools now or in phases, based on risk and change windows.
-
Verify Cloud KMS key readiness and permissions
- On any machine with
gcloudaccess:KEY_PROJECT_ID="kms-project-id"KEY_LOCATION="kms-location" # e.g. us-central1KEY_RING="kms-ring-name"KEY_NAME="kms-key-name"gcloud kms keys describe "${KEY_NAME}" \--location "${KEY_LOCATION}" \--keyring "${KEY_RING}" \--project "${KEY_PROJECT_ID}" - Ensure the key is
ENABLED, in the correct location/region for your cluster, and that the GKE node service accounts (or the project’s Compute Engine default service account) haveroles/cloudkms.cryptoKeyEncrypterDecrypteron this key.
- On any machine with
-
Plan node pool replacement/migration strategy
- Determine disk type (
pd-standardorpd-ssd) and size currently in use for each non‑CMEK node pool:gcloud container node-pools describe "${NODE_POOL}" \--cluster "${CLUSTER_NAME}" \--location "${LOCATION}" \--project "${PROJECT_ID}" \--format="yaml(config.diskType, config.diskSizeGb, config.machineType, maxPodsConstraint)" - Decide per node pool whether you will:
- Create a new CMEK‑enabled node pool and cordon/drain/migrate workloads, then delete the old pool (recommended), or
- Recreate the entire cluster with CMEK from the start if many pools/configs must change.
- Determine disk type (
-
Create replacement CMEK‑enabled node pools and migrate workloads
- For each node pool that must be CMEK‑encrypted, on any machine with
gcloudaccess:NEW_NODE_POOL="cmeq-${NODE_POOL}"gcloud container node-pools create "${NEW_NODE_POOL}" \--cluster "${CLUSTER_NAME}" \--location "${LOCATION}" \--project "${PROJECT_ID}" \--disk-type "pd-ssd" \--boot-disk-kms-key "projects/${KEY_PROJECT_ID}/locations/${KEY_LOCATION}/keyRings/${KEY_RING}/cryptoKeys/${KEY_NAME}" \--machine-type "same-machine-type-as-old" \--num-nodes 3 - Cordon and drain old nodes, then delete the old node pool when workloads are stable on the new one:
# list nodes in old poolkubectl get nodes -l cloud.google.com/gke-nodepool="${NODE_POOL}"# cordon and drain each node (run on any machine with kubectl access)kubectl cordon <node-name>kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data# delete old pool (gcloud)gcloud container node-pools delete "${NODE_POOL}" \--cluster "${CLUSTER_NAME}" \--location "${LOCATION}" \--project "${PROJECT_ID}"
- For each node pool that must be CMEK‑encrypted, on any machine with
-
Verify remediation and document exceptions
- Re-run the audit for the cluster:
gcloud container node-pools list \--cluster "${CLUSTER_NAME}" \--location "${LOCATION}" \--project "${PROJECT_ID}" \--format="value(name)" | while read NP; dogcloud container node-pools describe "${NP}" \--cluster "${CLUSTER_NAME}" \--location "${LOCATION}" \--project "${PROJECT_ID}" \--format="table(name, config.diskType, config.bootDiskKmsKey)"done
- Confirm all required node pools now show the correct non‑empty
config.bootDiskKmsKey. - For any node pool intentionally left without CMEK, record the justification and approval per your governance process.
- Re-run the audit for the cluster:
Using kubectl
kubectl cannot configure Customer-Managed Encryption Keys (CMEK) for GKE node boot disks, because this is a Google Cloud / cluster provisioning setting, not a Kubernetes API object. To remediate, use the Google Cloud Console, gcloud CLI, or your IaC as described in the Manual Steps section.
Automation
#!/usr/bin/env bash
#
# Report GKE node pool boot disk CMEK usage for all clusters in a project.
# REQUIREMENTS:
# - gcloud and jq installed
# - Authenticated to the desired project
#
# USAGE:
# PROJECT_ID="my-project" ./report_gke_bootdisk_cmek.sh
set -euo pipefail
PROJECT_ID="${PROJECT_ID:-}"
if [[ -z "${PROJECT_ID}" ]]; then
echo "ERROR: PROJECT_ID environment variable must be set" >&2
exit 1
fi
gcloud config set project "${PROJECT_ID}" >/dev/null
# Get all clusters (zonal and regional)
echo "Collecting cluster list for project: ${PROJECT_ID}..." >&2
clusters_json="$(gcloud container clusters list --format=json)"
if [[ "${clusters_json}" == "[]" ]]; then
echo "No GKE clusters found in project: ${PROJECT_ID}"
exit 0
fi
# Header
printf "%-60s %-40s %-15s %-12s %-10s %-80s\n" \
"CLUSTER" "NODE_POOL" "LOCATION" "DISK_TYPE" "USES_CMEK" "BOOT_DISK_KMS_KEY"
echo "${clusters_json}" | jq -r '.[] | @base64' | while read -r cluster_b64; do
_c() { echo "${cluster_b64}" | base64 --decode | jq -r "${1}"; }
cluster_name=$(_c '.name')
cluster_location=$(_c '.location')
# List node pools for this cluster
nodepools_json="$(gcloud container node-pools list \
--cluster "${cluster_name}" \
--location "${cluster_location}" \
--format=json || echo "[]")"
if [[ "${nodepools_json}" == "[]" ]]; then
printf "%-60s %-40s %-15s %-12s %-10s %-80s\n" \
"${cluster_name}" "<no-node-pools>" "${cluster_location}" "-" "-" "-"
continue
fi
echo "${nodepools_json}" | jq -r '.[] | @base64' | while read -r np_b64; do
_n() { echo "${np_b64}" | base64 --decode | jq -r "${1}"; }
np_name=$(_n '.name')
disk_type=$(_n '.config.diskType // "-"')
boot_disk_kms_key=$(_n '.config.bootDiskKmsKey // ""')
if [[ -n "${boot_disk_kms_key}" && "${boot_disk_kms_key}" != "null" ]]; then
uses_cmek="true"
else
uses_cmek="false"
boot_disk_kms_key="-"
fi
printf "%-60s %-40s %-15s %-12s %-10s %-80s\n" \
"${cluster_name}" "${np_name}" "${cluster_location}" \
"${disk_type}" "${uses_cmek}" "${boot_disk_kms_key}"
done
done
How to interpret the output
- Each row shows:
CLUSTER,NODE_POOL,LOCATION,DISK_TYPE,USES_CMEK,BOOT_DISK_KMS_KEY. - A problem for this control is any node pool where:
USES_CMEKisfalse, andDISK_TYPEispd-standardorpd-ssd(i.e., the node boot disks are not using a customer-managed key).
- For compliant node pools:
USES_CMEKistrue, andBOOT_DISK_KMS_KEYcontains a full KMS key resource:projects/<key_project_id>/locations/<location>/keyRings/<ring_name>/cryptoKeys/<key_name>.