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
Remediation
Manual Steps
-
Identify and review current endpoint exposure
- On any machine with
gcloudaccess: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
enablePrivateEndpointistrueand whether a publicendpointis exposed.
- On any machine with
-
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.
-
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.
-
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
gcloudaccess, 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.
-
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
kubectlcontexts, GitOps tools, or deployment scripts) to the new private cluster. - After validation, decommission or lock down any legacy clusters still exposing a public endpoint.
-
Verify remediation state
- On any machine with
gcloudaccess, 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.masterIpv4CidrBlockis set to the intended range- No unintended public endpoint is in use for normal operations.
- On any machine with
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_effectiveistrue
→ The API server is reachable from outside the node network (does not meet “public access disabled” intent).
- Contributing fields:
private_endpoint_enabledisfalse
→ Cluster not using a private endpoint.master_authorized_networks_enabledistrue
→ Public endpoint is gated by IP ranges, but public access is still present.master_ipv4_cidr_setisfalse
→ 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.