Clusters Are Created With Private Endpoint Enabled And
More Info:
Create clusters with a private control plane endpoint and public access disabled so the control plane is not reachable from the public internet. A public endpoint significantly increases exposure.
Risk Level
Critical
Address
Security
Compliance Standards
- CIS GKE
Triage and Remediation
- Remediation
Remediation
Manual Steps
-
Identify and review current endpoint configuration (any machine with gcloud installed)
PROJECT_ID="<your-project-id>"LOCATION="<your-cluster-location>"CLUSTER_NAME="<your-cluster-name>"gcloud container clusters describe "$CLUSTER_NAME" \--location "$LOCATION" \--project "$PROJECT_ID" \--format="json(privateClusterConfig,privateCluster,endpoint,masterAuthorizedNetworksConfig)"Review:
privateClusterConfig.enablePrivateEndpointistrue(private endpoint enabled)- Public endpoint exposure (presence of
endpoint,privateCluster: true) - Any
masterAuthorizedNetworksConfiguse, which still indicates a public control-plane endpoint.
-
Decide on remediation strategy (console/CLI/IaC review, no command)
Based on your org’s policies, decide whether to:- Replace this cluster with a new private-cluster-only control plane, or
- Accept and document the risk for this cluster (e.g., dependency on public endpoint, legacy tooling, no Private Service Connect / VPC access).
-
Plan a replacement private cluster with private endpoint only (any machine with gcloud installed)
Draft the cluster creation command ensuring private endpoint and required flags are set (do not run in production until planned and approved):PROJECT_ID="<your-project-id>"LOCATION="<your-cluster-location>"CLUSTER_NAME_NEW="<new-private-cluster-name>"MASTER_CIDR="<10.0.0.0/28>" # choose an unused RFC1918 range in your VPCgcloud container clusters create "$CLUSTER_NAME_NEW" \--project "$PROJECT_ID" \--location "$LOCATION" \--enable-private-nodes \--enable-ip-alias \--enable-private-endpoint \--master-ipv4-cidr="$MASTER_CIDR"Review network prerequisites (VPC, subnets, routes, firewall rules, access paths for admins/CI) before actually creating the cluster.
-
If using IaC, update templates instead of ad‑hoc CLI (any IaC workstation)
- Modify Terraform/Deployment Manager/other templates so new clusters include:
enable_private_nodes = trueenable_private_endpoint = true- IP aliasing and
master_ipv4_cidr_blockset.
- Run your normal plan/review process (e.g.,
terraform plan) to confirm only the intended changes will apply, then apply in a maintenance window.
- Modify Terraform/Deployment Manager/other templates so new clusters include:
-
Migrate workloads and cut over (any machine with kubectl and gcloud)
- Export and re‑apply workloads to the new private cluster. Example to switch context:
gcloud container clusters get-credentials "$CLUSTER_NAME_NEW" \--location "$LOCATION" \--project "$PROJECT_ID"
- Reconfigure CI/CD, monitoring, and access paths to use the private endpoint (through VPC/PSC/Bastion as appropriate).
- Once validated, decommission the old cluster:
gcloud container clusters delete "$CLUSTER_NAME" \--location "$LOCATION" \--project "$PROJECT_ID"
- Export and re‑apply workloads to the new private cluster. Example to switch context:
-
Verify the new cluster complies (any machine with gcloud installed)
gcloud container clusters describe "$CLUSTER_NAME_NEW" \--location "$LOCATION" \--project "$PROJECT_ID" \--format="json(privateClusterConfig,privateCluster,endpoint)"Confirm:
privateClusterConfig.enablePrivateEndpointistrue.- The cluster is a private cluster (
privateCluster: true). - Access to the control plane is only via the private endpoint (no reliance on a public endpoint in your operational procedures).
Using kubectl
kubectl cannot modify GKE control plane endpoint settings; they must be changed at the cloud provider level (gcloud/console/Terraform) where the cluster is created or updated. Refer to the Manual Steps section for the exact remediation process.
Automation
#!/usr/bin/env bash
# Check GKE clusters for private endpoint enabled and public access disabled.
# Requirements:
# - Run on any machine with gcloud, jq, and access to the target projects.
# - Authenticated to gcloud with sufficient IAM permissions.
set -euo pipefail
# Comma-separated list of projects to check (edit this list as needed)
PROJECTS="<PROJECT_ID_1>,<PROJECT_ID_2>"
IFS=',' read -r -a PROJECT_ARRAY <<< "${PROJECTS}"
printf "project,location,cluster_name,enablePrivateNodes,enablePrivateEndpoint,masterIpv4Cidr,publicEndpointAccessible\n"
for PROJECT in "${PROJECT_ARRAY[@]}"; do
PROJECT="$(echo "${PROJECT}" | xargs)" # trim spaces
if [[ -z "${PROJECT}" ]]; then
continue
fi
# List all clusters in the project across all locations
CLUSTERS_JSON="$(gcloud container clusters list \
--project "${PROJECT}" \
--format=json 2>/dev/null || echo "[]")"
echo "${CLUSTERS_JSON}" | jq -c '.[]' | while read -r CLUSTER; do
NAME="$(echo "${CLUSTER}" | jq -r '.name')"
LOCATION="$(echo "${CLUSTER}" | jq -r '.location')"
DESC="$(gcloud container clusters describe "${NAME}" \
--location "${LOCATION}" \
--project "${PROJECT}" \
--format=json 2>/dev/null || echo "{}")"
ENABLE_PRIVATE_NODES="$(echo "${DESC}" | jq -r '.privateClusterConfig.enablePrivateNodes // false')"
ENABLE_PRIVATE_ENDPOINT="$(echo "${DESC}" | jq -r '.privateClusterConfig.enablePrivateEndpoint // false')"
MASTER_IPV4_CIDR="$(echo "${DESC}" | jq -r '.privateClusterConfig.masterIpv4CidrBlock // ""')"
# Public endpoint is considered accessible if:
# - private endpoint is NOT enabled, or
# - the public endpoint access is not explicitly disabled
# GKE does not have a direct "disable public" flag; access is controlled via masterAuthorizedNetworksConfig.
# For this benchmark, treat any cluster without enablePrivateEndpoint=true as exposing a public endpoint.
if [[ "${ENABLE_PRIVATE_ENDPOINT}" != "true" ]]; then
PUBLIC_ENDPOINT_ACCESSIBLE="true"
else
# When private endpoint is enabled, there is still an external control plane address,
# but access can be restricted. For a conservative interpretation, flag as false
# only when private endpoint is enabled and authorized networks is empty/disabled.
MAN_AUTH_ENABLED="$(echo "${DESC}" | jq -r '.masterAuthorizedNetworksConfig.enabled // false')"
if [[ "${MAN_AUTH_ENABLED}" == "true" ]]; then
# There are explicitly allowed external CIDRs; treat as public-accessible for this control.
PUBLIC_ENDPOINT_ACCESSIBLE="true"
else
PUBLIC_ENDPOINT_ACCESSIBLE="false"
fi
fi
printf "%s,%s,%s,%s,%s,%s,%s\n" \
"${PROJECT}" \
"${LOCATION}" \
"${NAME}" \
"${ENABLE_PRIVATE_NODES}" \
"${ENABLE_PRIVATE_ENDPOINT}" \
"${MASTER_IPV4_CIDR}" \
"${PUBLIC_ENDPOINT_ACCESSIBLE}"
done
done
Explanation of output indicating a problem:
- The script prints one CSV line per cluster.
- A cluster is non-compliant and needs manual review when:
enablePrivateEndpointisfalse, orenablePrivateNodesisfalse, ormasterIpv4Cidris empty, orpublicEndpointAccessibleistrue.
According to the benchmark remediation, a fully aligned cluster should show:
enablePrivateNodes=trueenablePrivateEndpoint=truemasterIpv4Cidr= non-empty CIDRpublicEndpointAccessible=false