Skip to main content

Clusters Are Created With Private Endpoint Enabled And Public Access Disabled

More Info:​

Disable Access To The Kubernetes Api From Outside The Node Network If It Is Not Required.

Risk Level​

Low

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)
  • Essential 8
  • ISO/IEC 27017
  • ISO/IEC 27018
  • ISO/IEC 27701
  • KSA PDPL
  • MAS Technology Risk Management (Singapore)
  • MITRE ATT&CK (Cloud)
  • NIST SP 800-171
  • NYDFS 23 NYCRR 500
  • SWIFT Customer Security Controls Framework
  • Sarbanes-Oxley IT General Controls
  • Securities and Exchange Board of India (SEBI) - Cloud Security Adoption Framework
  • UK NCSC Cyber Assessment Framework

Triage and Remediation​

Remediation​

Manual Steps
  1. Identify and review current endpoint exposure

    • On any machine with gcloud access:
      gcloud container clusters list --project <PROJECT_ID> \
      --format="table(name,location,privateClusterConfig.enablePrivateEndpoint,privateClusterConfig.enablePrivateNodes,endpoint)"
    • For each cluster of interest, inspect details:
      gcloud container clusters describe <CLUSTER_NAME> \
      --location <LOCATION> --project <PROJECT_ID> --format json | \
      jq '.endpoint, .privateClusterConfig'
    • Confirm whether enablePrivateEndpoint is true and whether a public endpoint is exposed.
  2. Confirm application and operator access requirements

    • Identify all entities that currently access the Kubernetes API (CI/CD systems, operators, developers, monitoring tools).
    • Determine if they can use private connectivity (VPC peering, Cloud VPN, Cloud Interconnect, Cloud Run/Cloud Functions with VPC connector, bastion hosts, etc.) instead of public API access.
    • If any critical workflow cannot move off the public endpoint, record the justification and required source IP ranges.
  3. Decide the target exposure model per cluster
    For each cluster, choose:

    • Fully private endpoint (recommended): All API access from within the VPC/network; no public endpoint.
    • Hybrid / temporary public access: If strictly required, limit API access to minimal authorized IP ranges and plan for a migration to private-only.
    • Document the chosen model, risk acceptance, and migration plan if you cannot immediately disable public access.
  4. Plan and apply configuration changes (new or replacement clusters)

    • GKE does not support simply toggling all aspects of private cluster networking without downtime/risk; safest is to create a new private cluster and migrate workloads.
    • On any machine with gcloud access, create a new cluster with a private endpoint and no public access (per remediation):
      gcloud container clusters create <NEW_CLUSTER_NAME> \
      --project <PROJECT_ID> \
      --location <LOCATION> \
      --enable-ip-alias \
      --enable-private-nodes \
      --enable-private-endpoint \
      --master-ipv4-cidr=<MASTER_CIDR_RANGE>
    • Where possible, avoid specifying --enable-master-authorized-networks; if you must expose the control plane temporarily, strictly limit CIDR ranges and ensure a decommission plan.
  5. Migrate workloads and cut over access

    • Reconfigure clients, CI/CD, and operators to reach the new cluster API via private connectivity only.
    • Migrate workloads (e.g., with kubectl contexts, GitOps tools, or deployment scripts) to the new private cluster.
    • After validation, decommission or lock down any legacy clusters still exposing a public endpoint.
  6. Verify remediation state

    • On any machine with gcloud access, confirm the cluster’s endpoint configuration:
      gcloud container clusters describe <NEW_CLUSTER_NAME> \
      --location <LOCATION> --project <PROJECT_ID> --format json | \
      jq '.privateClusterConfig.enablePrivateEndpoint, .privateClusterConfig.enablePrivateNodes, .privateClusterConfig.masterIpv4CidrBlock, .endpoint'
    • Ensure:
      • .privateClusterConfig.enablePrivateEndpoint == true
      • .privateClusterConfig.enablePrivateNodes == true
      • .privateClusterConfig.masterIpv4CidrBlock is set to the intended range
      • No unintended public endpoint is in use for normal operations.
Using kubectl

kubectl cannot modify the cluster’s private/public endpoint configuration because it is managed at the GKE control plane / cloud provider level. To address this finding, make the change via gcloud/console/IaC as described and refer to the Manual Steps section for exact remediation guidance.

Automation
#!/usr/bin/env bash
#
# Audit GKE clusters for:
# - Private endpoint enabled
# - Public endpoint access disabled (masterAuthorizedNetworksConfig disabled)
#
# REQUIREMENTS:
# - Run on any machine with:
# * gcloud installed and authenticated
# * jq installed
# - Caller must have permission to list and describe clusters.
#
# USAGE:
# ./gke_private_endpoint_audit.sh
#
# Optional environment variables:
# GCP_PROJECTS="proj-1 proj-2" # space-separated list; if unset, uses current gcloud project
# GCP_LOCATIONS="us-central1 us-east1" # if unset, queries all locations via gcloud

set -euo pipefail

# Helper to log to stderr
log() { printf '%s\n' "$*" >&2; }

# Discover projects
if [[ -n "${GCP_PROJECTS-}" ]]; then
read -r -a PROJECTS <<<"${GCP_PROJECTS}"
else
CURRENT_PROJECT="$(gcloud config get-value project 2>/dev/null || true)"
if [[ -z "${CURRENT_PROJECT}" || "${CURRENT_PROJECT}" == "(unset)" ]]; then
log "ERROR: No project configured and GCP_PROJECTS not set."
log "Run: gcloud config set project YOUR_PROJECT_ID"
exit 1
fi
PROJECTS=("${CURRENT_PROJECT}")
fi

# Discover locations
discover_locations() {
local project="$1"
if [[ -n "${GCP_LOCATIONS-}" ]]; then
read -r -a LOCS <<<"${GCP_LOCATIONS}"
printf '%s\n' "${LOCS[@]}"
return
fi

# Get locations from existing clusters
gcloud container clusters list \
--project "${project}" \
--format='value(location)' 2>/dev/null | sort -u
}

# CSV header
echo "project,location,cluster,private_endpoint_enabled,private_nodes_enabled,master_ipv4_cidr_set,master_authorized_networks_enabled,public_access_effective,notes"

for project in "${PROJECTS[@]}"; do
locations=$(discover_locations "${project}")
if [[ -z "${locations}" ]]; then
log "No clusters found in project ${project}"
continue
fi

while IFS= read -r location; do
[[ -z "${location}" ]] && continue

clusters_json="$(gcloud container clusters list \
--project "${project}" \
--location "${location}" \
--format=json 2>/dev/null || true)"

if [[ -z "${clusters_json}" || "${clusters_json}" == "[]" ]]; then
continue
fi

echo "${clusters_json}" | jq -r --arg project "${project}" --arg location "${location}" '
.[] | .name | @text
' | while IFS= read -r cluster; do
desc="$(gcloud container clusters describe "${cluster}" \
--project "${project}" \
--location "${location}" \
--format=json 2>/dev/null || true)"

if [[ -z "${desc}" ]]; then
echo "${project},${location},${cluster},ERROR,ERROR,ERROR,ERROR,ERROR,failed_to_describe_cluster"
continue
fi

private_ep="$(echo "${desc}" | jq -r '.privateClusterConfig.enablePrivateEndpoint // false')"
private_nodes="$(echo "${desc}" | jq -r '.privateClusterConfig.enablePrivateNodes // false')"
master_cidr="$(echo "${desc}" | jq -r '.privateClusterConfig.masterIpv4CidrBlock // empty')"
man_enabled="$(echo "${desc}" | jq -r '(.masterAuthorizedNetworksConfig.enabled // false)')"

# Effective public access:
# - If private endpoint is false => public endpoint used => public access effective = true
# - If private endpoint true AND masterAuthorizedNetworks disabled => public access disabled
# - If private endpoint true AND masterAuthorizedNetworks enabled with any CIDRs => limited public access (still not fully disabled)
public_access_effective="false"
notes=()

if [[ "${private_ep}" != "true" ]]; then
public_access_effective="true"
notes+=("private_endpoint_disabled")
fi

if [[ "${man_enabled}" == "true" ]]; then
public_access_effective="true"
notes+=("master_authorized_networks_enabled_(public_entrypoints_exist)")
fi

if [[ -z "${master_cidr}" ]]; then
notes+=("master_ipv4_cidr_not_set")
fi

notes_str=""
if ((${#notes[@]} > 0)); then
notes_str="$(IFS=';'; echo "${notes[*]}")"
fi

master_cidr_set="false"
[[ -n "${master_cidr}" ]] && master_cidr_set="true"

echo "${project},${location},${cluster},${private_ep},${private_nodes},${master_cidr_set},${man_enabled},${public_access_effective},${notes_str}"
done
done <<<"${locations}"
done

Explanation of output indicating a problem:

  • Focus on rows where:
    • public_access_effective is true
      → The API server is reachable from outside the node network (does not meet “public access disabled” intent).
  • Contributing fields:
    • private_endpoint_enabled is false
      → Cluster not using a private endpoint.
    • master_authorized_networks_enabled is true
      → Public endpoint is gated by IP ranges, but public access is still present.
    • master_ipv4_cidr_set is false
      → Required config for fully private clusters is missing.

These rows require manual review and a design decision; there is no one-click or fully automated fix, and remediation typically involves recreating or significantly reconfiguring the cluster according to the benchmark guidance.

Additional Reading:​