Move GKE control plane access to the DNS endpoint and turn off IP access
Turn on the cluster's DNS-based endpoint with --enable-dns-access,
regenerate every kubeconfig with get-credentials and move CI
to the gke.goog name, then run --no-enable-ip-access. We did this on
a GKE 1.36 cluster. The DNS kubeconfig kept working through the switch. A kubeconfig with the
control plane's IP address hung until its timeout, --internal-ip was refused, and
the cluster's authorized networks disappeared from its configuration.
By Ostgate · Updated
The test
We ran this on 11 November 2026 on a Standard zonal cluster, GKE 1.36.4-gke.2377000 in europe-central2-a, with private nodes, from a Mac with gcloud 590.0.0 and kubectl 1.35.0. At the start the cluster had a DNS name with external traffic off, the internal IP endpoint only, and authorized networks of two internal ranges enforced on that endpoint. So that an IP-based kubeconfig could reach the control plane from the Mac at all, we briefly turned on the external endpoint and added the Mac's address to the authorized networks. Afterwards we put the original settings back and compared them with the saved copy: they matched.
Read your cluster's current settings first and keep them:
gcloud container clusters describe acme-gke \
--location=europe-central2-a --project=acme-prod \
--format="yaml(controlPlaneEndpointsConfig)"
1. Turn on the DNS endpoint with external traffic
In the Console, open the cluster, and on the Cluster details tab under Control Plane Networking click edit. In Edit control plane networking, select Access using DNS. With gcloud:
gcloud container clusters update acme-gke \
--location=europe-central2-a --project=acme-prod \
--enable-dns-access
The setting behind it is dnsEndpointConfig., which
“controls whether user traffic is allowed over this endpoint. Note that Google-managed services
may still use the endpoint even if this is false”
(GKE
API reference). Our cluster already had a gke.goog name with the field set to
false. The update finished in under a second and set it to true.
Who may use the name is decided by IAM. Google's page says access is managed “by configuring
the IAM permission container.” and lists
roles/ and roles/.
Machines with no internet access reach the name through “Private Google Access, Cloud NAT
gateway, or Private Service Connect for Google Cloud APIs”
(Customize
your network isolation). The DNS path from a laptop, and the other ways to reach a private
control plane, are in kubectl to a private
GKE cluster from a Mac.
2. Move every kubeconfig and pipeline to the DNS name
“The get-credentials command automatically uses the IP-based endpoint if the IP-based endpoint access is enabled” (Customize your network isolation). So while IP access is on, entries written without the flag keep pointing at an address. Regenerate them:
gcloud container clusters get-credentials acme-gke \
--location=europe-central2-a --project=acme-prod \
--dns-endpoint
In our test, without the flag gcloud wrote server: , the
internal endpoint, since the external one was off. With it, server: was the
gke.goog name and the entry had no certificate-.
To find entries that still use an address:
kubectl config view -o jsonpath='{range .clusters[*]}{.name}{"\t"}{.cluster.server}{"\n"}{end}'
Pipelines need the same change:
- GitHub Actions
google-github-actions/: setget-gke-credentials use_dns_based_endpoint: true(its default is false) and dropuse_internal_ip(action.yml). - Terraform
google_container_cluster:control_plane_endpoints_config.anddns_endpoint_config. allow_external_traffic ip_endpoints_config., which “Defaults toenabled true” (provider docs). Sinceenableddefaults to true, declare it false in the configuration that manages the cluster. - Anything that pins the cluster CA (Terraform's
cluster_, Argo CDca_ certificate caData, a hand-edited kubeconfig) fails on the DNS name withx509:. The fixes are in x509: certificate signed by unknown authority on the GKE DNS endpoint.certificate signed by unknown authority
We did not run the GitHub action or Terraform against the test cluster.
3. Turn off IP access
In the same Edit control plane networking dialog, clear Access using IPv4
addresses. With gcloud, the flag turns off both IP endpoints: “If you want to disable both
the internal and external endpoints of the control plane, use the
no-enable-ip-access flag”
(Customize
your network isolation).
gcloud container clusters update acme-gke \
--location=europe-central2-a --project=acme-prod \
--no-enable-ip-access
Our update took 22 seconds. Afterwards the IP part of the configuration had one field left:
controlPlaneEndpointsConfig:
dnsEndpointConfig:
allowExternalTraffic: true
endpoint: gke-…-123456789012.europe-central2-a.gke.goog
ipEndpointsConfig:
enabled: false
Right after it, from the Mac:
- The kubeconfig written with
--dns-endpointlisted the namespaces as before. - The kubeconfig with the external address, which had listed them a minute earlier, got no
answer. With
--request-timeout=20skubectl printedUnable, and curl to the address timed out on the TCP connect. Nothing refuses the connection, so a client waits for its own timeout.to connect to the server: context deadline exceeded get-credentialswithout--dns-endpointwrote thegke.googname. Google documents this: it “automatically uses the DNS-based endpoint if the IP-based endpoint access is disabled”.get-credentials --internal-ipwrote nothing:
ERROR: (gcloud.container.clusters.get-credentials) IP access is disabled for cluster acme-gke.
What breaks when IP access is off
- Kubeconfigs, scripts and dashboards with the control plane's public or private address. They hang until the client's timeout.
- Cloud Shell on the IP path: “If the external endpoint of your cluster's control plane is
disabled, you can't access your GKE control plane with Cloud Shell.” Google recommends the
DNS endpoint for Cloud Shell. A fresh
get-credentialspicks the DNS name by itself once IP access is off; an entry written earlier still holds the address. - CI that sets
use_internal_ipor reads the endpoint address, and Terraform that still declares IP access. - Users who reached the internal endpoint over a VPN, an interconnect or a bastion. The
internal address goes too, and
--internal-ipis refused. They need the DNS name and a route to Google APIs. - Editing authorized networks, see below.
- The switch itself on some clusters: Google lists
Directfor a cluster that “uses legacy network”, fixed by a migration to Private Service Connect (Troubleshoot network isolation). Our cluster did not hit it.IP access can't be disabled for public clusters
We did not try Cloud Shell. The Cloud Shell line is Google's.
Authorized networks after the switch
Authorized networks filter by address: access to the IP endpoints “depends on the source IP
address and is controlled by your authorized networks”
(Customize
your network isolation). With no IP endpoint they have nothing to filter. In our test they
went further: after --no-enable-ip-access the cluster description had no
authorizedNetworksConfig and masterAuthorizedNetworksConfig was
null. Setting the same two ranges again failed:
ERROR: (gcloud.container.clusters.update) INVALID_ARGUMENT: Invalid value for "cluster.master_authorized_networks_config.cidr_blocks" or "cluster.control_plane_endpoints_config.ip_endpoints_config.authorized_networks_config.cidr_blocks": authorized networks fields cannot be mutated if direct IP access is disabled.
Google's fix for that error is to turn the IP endpoint on again with
enable-ip-access. Save the list before the switch. On the DNS name, who gets in is
decided by IAM, and the hardening guide adds “You can use VPC Service Controls to control who
can access the DNS-based endpoint”
(Best
practices for hardening your cluster's security).
The organization policy
Google's hardening guide recommends the DNS endpoint alone and points to a managed constraint
to enforce it. The constraint is constraints/,
titled “Require using only the DNS-based endpoint to access GKE clusters.” and described as
“Enable the DNS-based endpoint for GKE control plane access and disable IP-based endpoints when
creating or updating clusters.”
(Organization
Policy constraints). Going by that description, it applies when a cluster is created or
updated, so a create or update that keeps IP access on is what it stops. Move the clusters
first, then enforce it. We did not enforce the constraint in our project.
Turning IP access back on
To return to our starting point (internal endpoint only, two authorized networks enforced on it), we passed the whole IP configuration in one update:
gcloud container clusters update acme-gke \
--location=europe-central2-a --project=acme-prod \
--enable-ip-access --enable-private-endpoint \
--enable-master-authorized-networks \
--master-authorized-networks=10.0.1.0/24,10.0.2.0/24 \
--enable-authorized-networks-on-private-endpoint
It took 25 seconds, and the internal endpoint came back on the same address as before. We
never ran --enable-ip-access alone, so we don't know whether GKE would have
restored the old authorized networks by itself. Then --no-enable-dns-access set
allowExternalTraffic back to false in under a second, and the saved and the final
controlPlaneEndpointsConfig were identical. The DNS kubeconfig then got the HTML
page titled Error 403 (Forbidden)!!1; the other messages of a closed or
unreachable control plane are in kubectl
times out on a GKE cluster.
How Ostgate does it
Ostgate reads each cluster's endpoint settings and picks a path, shown in the
Access column. On the DNS endpoint it uses “the cluster's gke.goog name,
reached over the internet and signed in with your Google account”, without gcloud
(GKE docs). Private clusters also work through the Connect
gateway, a VPN or a bastion VM over IAP. Diagnose Access… “names the setting that blocks
a cluster and the gcloud command that fixes it, for whoever administers the project”
(release notes, 0.0.8). Use with kubectl… adds a context
to ~/.kube/ostgate with one Path row per path the cluster allows. We did not
open the test cluster in Ostgate while its IP access was off.
Symptoms and causes
| You see | Cause | Fix |
|---|---|---|
Unable , or kubectl hangs, on an IP address |
IP access is off and the kubeconfig still has the address | get-credentials |
IP from get-credentials |
--internal-ip on a cluster without IP endpoints |
Drop --internal-ip, use --dns-endpoint |
authorized |
Editing authorized networks while IP access is off | Turn IP access on with the list in the same update |
Direct |
The cluster uses a legacy network (Google; not reproduced) | Migrate the cluster to Private Service Connect |
Error on the gke.goog name |
allowExternalTraffic is false |
--enable-dns-access |
x509: on the gke.goog name |
The client pins the cluster CA | Remove the CA from the client |
Questions
How do I turn off IP-based access to a GKE control plane?
Turn on the DNS-based endpoint with external traffic first (gcloud container clusters update CLUSTER --enable-dns-access), regenerate every kubeconfig with gcloud container clusters get-credentials --dns-endpoint and move CI to the DNS name, then run gcloud container clusters update CLUSTER --no-enable-ip-access. In our test on GKE 1.36 that last update took 22 seconds, and a kubeconfig written with --dns-endpoint kept working through it.
What stops working when IP access is off?
Everything that still points at the control plane's public or private IP address. In our test kubectl with an IP-based kubeconfig hung until its 20-second timeout and printed context deadline exceeded, and get-credentials --internal-ip failed with IP access is disabled for cluster. Google says Cloud Shell can't reach a cluster whose external endpoint is disabled and recommends the DNS endpoint there. Authorized networks can't be edited until IP access is on again.
What happens to authorized networks after --no-enable-ip-access?
They no longer apply, since they filter by source IP address and no IP endpoint is left. In our test the cluster description listed no authorized networks after the switch, and setting them failed with authorized networks fields cannot be mutated if direct IP access is disabled. Save the list first: we passed it again in the same update that turned IP access back on.
What does constraints/container.managed.enableControlPlaneDNSOnlyAccess do?
It is a managed Organization Policy constraint described as: Enable the DNS-based endpoint for GKE control plane access and disable IP-based endpoints when creating or updating clusters. Google's hardening guide names it as the way to enforce DNS-only control plane access in an organization. We did not enforce it in our test project.
Ostgate runs on Apple Silicon Macs with macOS 15 or later, with a 7-day trial and no sign-up. Setup is in the GKE docs, and GKE on a Mac lists what the GKE area does.