Ensure Clusters Are Created With Private Nodes
More Info:
Disable Public Ip Addresses For Cluster Nodes, So That They Only Have Private Ip Addresses. Private Nodes Are Nodes With No Public Ip Addresses
Risk Level
Medium
Address
Security
Compliance Standards
- APRA CPS 234 (Australia)
- BSI C5 (Germany)
- Brazil LGPD
- CCPA / CPRA (California)
- CIS Critical Security Controls v8
- CIS GKE
- CMMC 2.0
- CSA Cloud Controls Matrix v4
- DPDPA
- Digital Operational Resilience Act (EU)
- ISO/IEC 27017
- ISO/IEC 27018
- ISO/IEC 27701
- KSA PDPL
- MAS Technology Risk Management (Singapore)
- MITRE ATT&CK (Cloud)
- NIS2 Directive
- NIST SP 800-171
- NYDFS 23 NYCRR 500
- SWIFT Customer Security Controls Framework
- Sarbanes-Oxley IT General Controls
- UK NCSC Cyber Assessment Framework
Triage and Remediation
- Remediation
Remediation
Manual Steps
-
Identify clusters and current private node setting
- Run on: any machine with
gcloudconfigured. - List clusters and locations:
gcloud container clusters list --project PROJECT_ID
- For each cluster, check whether private nodes are enabled:
gcloud container clusters describe CLUSTER_NAME \--location LOCATION \--project PROJECT_ID \--format='value(privateClusterConfig.enablePrivateNodes)'
- Record which clusters return
falseor empty; these do not have private nodes.
- Run on: any machine with
-
Confirm networking prerequisites and potential impact
- For each non‑private cluster, review its networking:
gcloud container clusters describe CLUSTER_NAME \--location LOCATION \--project PROJECT_ID \--format='yaml(networkConfig, privateClusterConfig, ipAllocationPolicy)'
- Validate that:
- You can create/use a VPC‑native (IP alias) cluster (
ipAllocationPolicy.useIpAliases=truerequirement for private nodes). - Your workloads and admins do not rely on node public IPs (e.g., SSH, direct internet egress). Plan alternatives such as Cloud NAT, private bastion, or IAP.
- You can create/use a VPC‑native (IP alias) cluster (
- For each non‑private cluster, review its networking:
-
Decide remediation strategy: migrate vs. accept risk
- For each non‑private cluster, decide one of:
- Migrate to a new private cluster (recommended).
- Accept and document an exception (include justification, expiration, and periodic review).
- Capture the decision in your internal risk register or change tracking system.
- For each non‑private cluster, decide one of:
-
Plan creation of a new private cluster (if migrating)
- Choose or create a VPC and subnets for node and pod ranges.
- Decide on
--master-ipv4-cidr(control plane CIDR that does not overlap with existing ranges). - Define the full create command (example; adjust values):
gcloud container clusters create NEW_CLUSTER_NAME \--project PROJECT_ID \--location LOCATION \--enable-private-nodes \--enable-ip-alias \--master-ipv4-cidr=10.0.0.0/28 \--network=VPC_NAME \--subnetwork=SUBNET_NAME
- Have this reviewed and approved via your normal change management process.
-
Migrate workloads and decommission old cluster (if migrating)
- Create the new private cluster using the planned command from step 4.
- Update kubeconfig:
gcloud container clusters get-credentials NEW_CLUSTER_NAME \--location LOCATION \--project PROJECT_ID
- Migrate workloads (e.g., export manifests, helm releases, or use GitOps) and cut over traffic (DNS, load balancers, external dependencies) to the new cluster.
- When fully migrated and validated, delete the old non‑private cluster:
gcloud container clusters delete OLD_CLUSTER_NAME \--location LOCATION \--project PROJECT_ID
-
Verify compliance for all in-scope clusters
- Re-run for each cluster:
gcloud container clusters describe CLUSTER_NAME \--location LOCATION \--project PROJECT_ID \--format='value(privateClusterConfig.enablePrivateNodes)'
- Confirm the value is
truefor all clusters where you intended private nodes; ensure any remainingfalseentries have an approved, documented exception.
- Re-run for each cluster:
Using kubectl
kubectl cannot modify whether GKE nodes use public or private IPs; this setting is defined on the managed control plane via GCP (gcloud/console/Terraform). To address this finding, make the change at the cloud provider level as described in the Manual Steps section.
Automation
#!/usr/bin/env bash
#
# Report GKE clusters that do NOT have private nodes enabled.
# Requirements:
# - gcloud and jq installed
# - gcloud authenticated, with access to the target projects
#
# Usage examples:
# ./check_gke_private_nodes.sh my-project
# ./check_gke_private_nodes.sh proj-1 proj-2
# GCP_PROJECTS="proj-1 proj-2" ./check_gke_private_nodes.sh
# ./check_gke_private_nodes.sh # uses gcloud config get-value core/project
set -euo pipefail
# Get project list from args or env or gcloud default
if [[ "$#" -gt 0 ]]; then
PROJECTS=("$@")
elif [[ -n "${GCP_PROJECTS:-}" ]]; then
# shellcheck disable=SC2206
PROJECTS=(${GCP_PROJECTS})
else
DEFAULT_PROJECT="$(gcloud config get-value core/project 2>/dev/null || true)"
if [[ -z "${DEFAULT_PROJECT}" || "${DEFAULT_PROJECT}" == "(unset)" ]]; then
echo "ERROR: No projects specified and no default project set." >&2
echo "Usage: $0 <project-1> [project-2 ...]" >&2
exit 1
fi
PROJECTS=("${DEFAULT_PROJECT}")
fi
echo "Checking GKE clusters for private nodes..."
echo
for PROJECT in "${PROJECTS[@]}"; do
echo "Project: ${PROJECT}"
# List all clusters in all locations for this project
# Output: name, location, private-nodes-enabled
if ! CLUSTERS_JSON="$(gcloud container clusters list \
--project "${PROJECT}" \
--format=json 2>/dev/null)"; then
echo " ERROR: Unable to list clusters (insufficient permissions or invalid project)." >&2
echo
continue
fi
if [[ "${CLUSTERS_JSON}" == "[]" ]]; then
echo " No clusters found."
echo
continue
fi
echo " name,location,private_nodes_enabled"
echo "${CLUSTERS_JSON}" | jq -r '
.[] |
# For each cluster, look up full description to get privateClusterConfig
.name as $name |
.location as $loc |
@sh "gcloud container clusters describe \($name) --location \($loc) --project '"${PROJECT}"' --format=json"
' | while read -r DESCRIBE_CMD; do
# Run each generated gcloud describe command
# shellcheck disable=SC2086
if ! CLUSTER_DESC_JSON=$(eval ${DESCRIBE_CMD} 2>/dev/null); then
echo " ERROR: Failed to describe cluster with: ${DESCRIBE_CMD}" >&2
continue
fi
NAME=$(echo "${CLUSTER_DESC_JSON}" | jq -r '.name')
LOC=$(echo "${CLUSTER_DESC_JSON}" | jq -r '.location // .zone // ""')
PRIVATE_NODES=$(echo "${CLUSTER_DESC_JSON}" | jq -r '.privateClusterConfig.enablePrivateNodes // false')
echo " ${NAME},${LOC},${PRIVATE_NODES}"
done
echo
done
cat <<'EOF'
Interpretation:
- A cluster is COMPLIANT with CIS GKE 5.6.5 when:
private_nodes_enabled == true
- A cluster is NON-COMPLIANT (problem) when:
private_nodes_enabled == false
Any line in the output where the last column is "false" indicates a cluster
that was created without private nodes enabled and should be reviewed. There is
no deterministic automated fix; you typically need to recreate the cluster with
--enable-private-nodes (and required flags) after planning migration.
EOF