Skip to main content

Kubernetes Secrets Are Encrypted Using Keys Managed In

More Info:​

Enable application-layer secrets encryption so Kubernetes Secrets are encrypted at rest using a Cloud KMS key you control. This protects secret data beyond Googles default encryption.

Risk Level​

High

Address​

Security

Compliance Standards​

  • CIS GKE

Triage and Remediation​

Remediation​

Manual Steps
  1. Identify the GKE cluster(s) and current encryption status
    Run on any machine with gcloud configured:

    gcloud container clusters list --project <cluster_project_id>
    gcloud container clusters describe <cluster_name> \
    --location <location> \
    --project <cluster_project_id> \
    --format json | jq '.databaseEncryption'

    Review whether state is "ENCRYPTED" and a keyName is present. If state is "DECRYPTED" or keyName is empty, application‑layer secrets encryption with Cloud KMS is not enabled.

  2. Decide the KMS project, location, and key structure
    With your security/ops team, determine:

    • Which project will own the KMS key (<key_project_id>).
    • Region (<location>) that matches or is appropriate for your cluster.
    • Key ring name (<ring_name>) and key name (<key_name>).
      Document whether you will use a dedicated key per cluster, per environment, or shared across clusters, considering key‑rotation and access‑control policies.
  3. Create or verify the Cloud KMS key
    If a suitable key does not already exist, create one (run on any machine with gcloud):

    gcloud kms keyrings create <ring_name> \
    --location <location> \
    --project <key_project_id>

    gcloud kms keys create <key_name> \
    --location <location> \
    --keyring <ring_name> \
    --purpose encryption \
    --project <key_project_id>

    If using an existing key, confirm it is of purpose ENCRYPT_DECRYPT and in the correct region:

    gcloud kms keys describe <key_name> \
    --location <location> \
    --keyring <ring_name> \
    --project <key_project_id>
  4. Grant the GKE service agent access to the KMS key
    First, identify the Kubernetes Engine Service Agent for the cluster project (run on any machine with gcloud):

    gcloud beta services identity create \
    --service container.googleapis.com \
    --project <cluster_project_id> \
    --quiet \
    --format 'value(email)'

    Then grant it roles/cloudkms.cryptoKeyEncrypterDecrypter on the key:

    gcloud kms keys add-iam-policy-binding <key_name> \
    --location <location> \
    --keyring <ring_name> \
    --member serviceAccount:<service_account_name> \
    --role roles/cloudkms.cryptoKeyEncrypterDecrypter \
    --project <key_project_id>

    Replace <service_account_name> with the email printed by the previous command.

  5. Enable application‑layer secrets encryption on the cluster
    For a new cluster (decide whether to recreate vs update an existing cluster):

    gcloud container clusters create <cluster_name> \
    --cluster-version=latest \
    --zone <zone> \
    --database-encryption-key projects/<key_project_id>/locations/<location>/keyRings/<ring_name>/cryptoKeys/<key_name> \
    --project <cluster_project_id>

    To enable on an existing cluster:

    gcloud container clusters update <cluster_name> \
    --zone <zone> \
    --database-encryption-key projects/<key_project_id>/locations/<location>/keyRings/<ring_name>/cryptoKeys/<key_name> \
    --project <cluster_project_id>

    Assess operational impact: cluster control‑plane reconfiguration may temporarily affect control‑plane availability; coordinate during a maintenance window if needed.

  6. Verify encryption is enabled and using the intended key
    After the update/recreate completes, run (any machine with gcloud):

    gcloud container clusters describe <cluster_name> \
    --location <location> \
    --project <cluster_project_id> \
    --format json | jq '.databaseEncryption'

    Confirm that state is "ENCRYPTED" and that keyName matches
    projects/<key_project_id>/locations/<location>/keyRings/<ring_name>/cryptoKeys/<key_name>.

Using kubectl

kubectl cannot configure application-layer secrets encryption or Cloud KMS keys, because this setting is managed entirely in the GKE control plane via the Google Cloud Console, gcloud CLI, or IaC. To remediate this finding, follow the guidance in the Manual Steps section for making the change at the cloud provider configuration level.

Automation
#!/usr/bin/env bash
#
# Check GKE clusters for Application-layer Secrets Encryption with Cloud KMS.
#
# REQUIREMENTS:
# - Runs on: any machine with gcloud, jq, and access to the relevant projects.
# - Auth: 'gcloud auth login' (or service account) and 'gcloud config set project' as needed.
#
# USAGE:
# ./check_gke_kms_encryption.sh # use current gcloud project
# GCP_PROJECTS="proj-a proj-b" ./check_gke_kms_encryption.sh
# GCP_PROJECTS="$(gcloud projects list --format='value(projectId)')" ./check_gke_kms_encryption.sh
#
# OUTPUT:
# Columns:
# PROJECT GKE project ID
# CLUSTER GKE cluster name
# LOCATION Cluster location (zone or region)
# ENABLED true/false if databaseEncryption is enabled
# KEY_RESOURCE Full KMS key resource, or "-" if not set
# STATE OK / NOT_ENCRYPTED / MISCONFIGURED
#
# INTERPRETATION (what indicates a problem):
# - STATE=NOT_ENCRYPTED
# => Application-layer Secrets Encryption is NOT enabled. This fails CIS GKE 5.3.1.
# - STATE=MISCONFIGURED
# => 'databaseEncryption' block exists but is missing key fields
# (e.g., keyName or state). Requires manual review.
# - STATE=OK
# => Application-layer Secrets Encryption is enabled and has a KMS key configured.
#
set -euo pipefail

# ANSI colors for readability (can be disabled by setting NO_COLOR=1)
if [[ "${NO_COLOR:-}" == "1" ]]; then
RED=""; GREEN=""; YELLOW=""; RESET=""
else
RED="$(printf '\033[31m')"
GREEN="$(printf '\033[32m')"
YELLOW="$(printf '\033[33m')"
RESET="$(printf '\033[0m')"
fi

PROJECTS=()
if [[ -n "${GCP_PROJECTS:-}" ]]; then
# Space-separated list from env var
read -r -a PROJECTS <<< "${GCP_PROJECTS}"
else
# Use current gcloud project
CURRENT_PROJECT="$(gcloud config get-value project 2>/dev/null || true)"
if [[ -z "${CURRENT_PROJECT}" ]]; then
echo "ERROR: No projects specified and no current gcloud project set." >&2
echo " Set GCP_PROJECTS or run: gcloud config set project <project_id>" >&2
exit 1
fi
PROJECTS=("${CURRENT_PROJECT}")
fi

echo "PROJECT,CLUSTER,LOCATION,ENABLED,KEY_RESOURCE,STATE" # CSV header

for PROJECT in "${PROJECTS[@]}"; do
CLUSTERS_JSON="$(gcloud container clusters list \
--project "${PROJECT}" \
--format=json 2>/dev/null || true)"

if [[ -z "${CLUSTERS_JSON}" || "${CLUSTERS_JSON}" == "[]" ]]; then
# No clusters in this project; continue silently.
continue
fi

echo "${CLUSTERS_JSON}" | jq -c '.[]' | while read -r CLUSTER; do
NAME="$(echo "${CLUSTER}" | jq -r '.name')"
LOCATION="$(echo "${CLUSTER}" | jq -r '.location')"

# Describe cluster to inspect databaseEncryption
DESCRIBE_JSON="$(gcloud container clusters describe "${NAME}" \
--location "${LOCATION}" \
--project "${PROJECT}" \
--format=json 2>/dev/null || true)"

if [[ -z "${DESCRIBE_JSON}" ]]; then
STATE="MISCONFIGURED"
ENABLED="unknown"
KEY_RESOURCE="-"
printf '%s,"%s",%s,%s,%s,%s%s%s\n' \
"${PROJECT}" "${NAME}" "${LOCATION}" "${ENABLED}" "${KEY_RESOURCE}" \
"${YELLOW}" "${STATE}" "${RESET}"
continue
fi

DB_ENCRYPTION="$(echo "${DESCRIBE_JSON}" | jq -c '.databaseEncryption // {}')"
RAW_STATE="$(echo "${DB_ENCRYPTION}" | jq -r '.state // empty')"
KEY_NAME="$(echo "${DB_ENCRYPTION}" | jq -r '.keyName // empty')"

if [[ -z "${RAW_STATE}" ]]; then
# No state field at all => not configured
ENABLED="false"
KEY_RESOURCE="-"
STATE="NOT_ENCRYPTED"
COLOR="${RED}"
elif [[ "${RAW_STATE}" != "ENCRYPTED" ]]; then
# State present but not ENCRYPTED (e.g., DECRYPTED or UNKNOWN)
ENABLED="false"
KEY_RESOURCE="${KEY_NAME:-"-"}"
STATE="NOT_ENCRYPTED"
COLOR="${RED}"
else
# State == ENCRYPTED, verify keyName present
if [[ -z "${KEY_NAME}" ]]; then
ENABLED="true"
KEY_RESOURCE="-"
STATE="MISCONFIGURED"
COLOR="${YELLOW}"
else
ENABLED="true"
KEY_RESOURCE="${KEY_NAME}"
STATE="OK"
COLOR="${GREEN}"
fi
fi

printf '%s,"%s",%s,%s,"%s",%s%s%s\n' \
"${PROJECT}" "${NAME}" "${LOCATION}" "${ENABLED}" "${KEY_RESOURCE}" \
"${COLOR}" "${STATE}" "${RESET}"
done
done

What indicates a problem in the script output

  • Any row with STATE=NOT_ENCRYPTED:
    • The databaseEncryption block is missing, or
    • .databaseEncryption.state is not ENCRYPTED.
    • This means Kubernetes Secrets are not protected with Cloud KMS as required.
  • Any row with STATE=MISCONFIGURED:
    • state is ENCRYPTED but keyName is missing, or the cluster could not be described.
    • Requires manual review to ensure a valid Cloud KMS key is configured.
  • Only rows with STATE=OK meet the requirement for CIS GKE 5.3.1.