Skip to main content

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

Manual Steps
  1. Identify clusters and current private node setting

    • Run on: any machine with gcloud configured.
    • 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 false or empty; these do not have private nodes.
  2. 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=true requirement 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.
  3. 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.
  4. 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.
  5. 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
  6. 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 true for all clusters where you intended private nodes; ensure any remaining false entries have an approved, documented exception.
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

Additional Reading: