Ensure Clusters Are Created With Private Nodes
More Info:
Create clusters with private nodes so nodes have only internal IP addresses and are not directly reachable from the internet. This reduces the node attack surface.
Risk Level
High
Address
Security
Compliance Standards
- CIS GKE
Triage and Remediation
- Remediation
Remediation
Manual Steps
-
Identify clusters without private nodes
- Run on: any machine with
gcloudaccess.
PROJECT_ID="<PROJECT_ID>"for LOCATION in $(gcloud container clusters list --project "$PROJECT_ID" --format="value(location)" | sort -u); doecho "== $LOCATION =="gcloud container clusters list --project "$PROJECT_ID" --region "$LOCATION" --format="value(name)" \| xargs -I{} sh -c 'echo "Cluster: {}"gcloud container clusters describe {} \--location '"$LOCATION"' --project '"$PROJECT_ID"' \--format="value(name,privateClusterConfig.enablePrivateNodes)"'done- Note clusters where
privateClusterConfig.enablePrivateNodesis empty orfalse.
- Run on: any machine with
-
Assess exposure and necessity of public nodes
- For each non-private cluster from step 1, review:
- Whether workloads require direct node access from the internet.
- Whether connectivity can be provided via load balancers, Ingress, VPN, or Cloud Interconnect instead of public node IPs.
- Document clusters where private nodes are feasible and desirable.
- For each non-private cluster from step 1, review:
-
Check use of IP aliases and master CIDR (for planning migration)
- Run on: any machine with
gcloudaccess.
CLUSTER_NAME="<CLUSTER_NAME>"LOCATION="<REGION_OR_ZONE>"PROJECT_ID="<PROJECT_ID>"gcloud container clusters describe "$CLUSTER_NAME" \--location "$LOCATION" --project "$PROJECT_ID" \--format='value(ipAllocationPolicy.useIpAliases, privateClusterConfig.masterIpv4CidrBlock)'- If
useIpAliasesis notTrue, note that the cluster cannot simply be flipped to private nodes; a new cluster will be required to meet the remediation.
- Run on: any machine with
-
Decide remediation approach per cluster
- If the cluster already has
useIpAliases=Trueand your networking design allows it, plan to recreate the cluster with--enable-private-nodes(GKE does not support toggling this on an existing non-private cluster). - If
useIpAliases=False, plan a migration to a new cluster that meets all three flags:--enable-private-nodes,--enable-ip-alias, and--master-ipv4-cidr.
- If the cluster already has
-
Create a compliant replacement cluster
- Run on: any machine with
gcloudaccess. Choose appropriate ranges for your environment.
PROJECT_ID="<PROJECT_ID>"LOCATION="<REGION_OR_ZONE>"NEW_CLUSTER_NAME="<NEW_PRIVATE_CLUSTER_NAME>"gcloud container clusters create "$NEW_CLUSTER_NAME" \--project "$PROJECT_ID" \--location "$LOCATION" \--enable-private-nodes \--enable-ip-alias \--master-ipv4-cidr="172.16.0.0/28"- Migrate workloads (namespaces, workloads, services, ingress, and associated IAM, firewall, and networking rules) from the old cluster to the new private one, then decommission the old cluster.
- Run on: any machine with
-
Verify remediation
- Run on: any machine with
gcloudaccess.
CLUSTER_NAME="<NEW_PRIVATE_CLUSTER_NAME>"LOCATION="<REGION_OR_ZONE>"PROJECT_ID="<PROJECT_ID>"gcloud container clusters describe "$CLUSTER_NAME" \--location "$LOCATION" --project "$PROJECT_ID" \--format=json | jq '.privateClusterConfig.enablePrivateNodes, .ipAllocationPolicy.useIpAliases, .privateClusterConfig.masterIpv4CidrBlock'- Confirm that
enablePrivateNodesistrue,useIpAliasesistrue, andmasterIpv4CidrBlockis set.
- Run on: any machine with
Using kubectl
kubectl cannot enable private nodes or change GKE cluster networking; this configuration is managed at the cloud provider control-plane level via gcloud, the GCP console, or IaC. Refer to the Manual Steps section for how to review the current cluster and recreate or reconfigure it with private nodes enabled.
Automation
#!/usr/bin/env bash
# Reports whether each GKE cluster in a project/locations has private nodes enabled.
# Run on: any machine with gcloud, jq, and appropriate IAM permissions.
set -euo pipefail
PROJECT_ID="YOUR_PROJECT_ID" # <-- set explicitly
LOCATIONS="-" # "-" = all locations; or space-separated list, e.g. "us-central1 us-east1"
if [[ "$PROJECT_ID" == "YOUR_PROJECT_ID" ]]; then
echo "ERROR: Set PROJECT_ID in this script before running." >&2
exit 1
fi
echo "Project: $PROJECT_ID"
echo "Locations: $LOCATIONS"
echo
# Get clusters (optionally filtered by locations)
if [[ "$LOCATIONS" == "-" ]]; then
CLUSTERS_JSON="$(gcloud container clusters list \
--project "$PROJECT_ID" \
--format=json)"
else
CLUSTERS_JSON="$(gcloud container clusters list \
--project "$PROJECT_ID" \
--filter="location:(${LOCATIONS// / OR })" \
--format=json)"
fi
if [[ "$(echo "$CLUSTERS_JSON" | jq 'length')" -eq 0 ]]; then
echo "No clusters found."
exit 0
fi
printf "%-40s %-20s %-8s %-16s %-22s\n" "CLUSTER_NAME" "LOCATION" "PRIVATE" "MASTER_IPV4_CIDR" "NOTE"
printf "%0.s-" {1..110}; echo
echo "$CLUSTERS_JSON" | jq -r '.[] | @base64' | while read -r row; do
_jq() { echo "$row" | base64 --decode | jq -r "$1"; }
NAME=$(_jq '.name')
LOCATION=$(_jq '.location')
DESC_JSON="$(gcloud container clusters describe "$NAME" \
--location "$LOCATION" \
--project "$PROJECT_ID" \
--format=json)"
ENABLE_PRIVATE_NODES="$(echo "$DESC_JSON" | jq -r '.privateClusterConfig.enablePrivateNodes // "false"')"
MASTER_IPV4_CIDR="$(echo "$DESC_JSON" | jq -r '.privateClusterConfig.masterIpv4CidrBlock // ""')"
ENABLE_IP_ALIAS="$(echo "$DESC_JSON" | jq -r '.ipAllocationPolicy.useIpAliases // "false"')"
NOTE=""
if [[ "$ENABLE_PRIVATE_NODES" != "true" ]]; then
NOTE="NOT PRIVATE (finding)"
elif [[ "$ENABLE_PRIVATE_NODES" == "true" && "$ENABLE_IP_ALIAS" != "true" ]]; then
NOTE="privateNodes=true but ip-alias DISABLED (misconfigured)"
elif [[ "$ENABLE_PRIVATE_NODES" == "true" && -z "$MASTER_IPV4_CIDR" ]]; then
NOTE="privateNodes=true but masterIpv4CidrBlock EMPTY (check config)"
fi
printf "%-40s %-20s %-8s %-16s %-22s\n" \
"$NAME" "$LOCATION" "$ENABLE_PRIVATE_NODES" "$MASTER_IPV4_CIDR" "$NOTE"
done
Explanation of output indicating a problem:
- Any cluster row with:
PRIVATEcolumn showingfalse, orNOTEcolumn containingNOT PRIVATE (finding)
indicates a non-compliant cluster for this control (nodes are not configured as private). Use this list as input to manual redesign/recreation/migration decisions; enabling private nodes on an existing non-private cluster is not a deterministic one-shot fix.