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 --dns-endpoint 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.allowExternalTraffic, 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.clusters.connect” and lists roles/container.developer and roles/container.viewer. 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: https://10.0.0.2, the internal endpoint, since the external one was off. With it, server: was the gke.goog name and the entry had no certificate-authority-data. 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/get-gke-credentials: set use_dns_based_endpoint: true (its default is false) and drop use_internal_ip (action.yml).
  • Terraform google_container_cluster: control_plane_endpoints_config.dns_endpoint_config.allow_external_traffic and ip_endpoints_config.enabled, which “Defaults to true” (provider docs). Since enabled defaults to true, declare it false in the configuration that manages the cluster.
  • Anything that pins the cluster CA (Terraform's cluster_ca_certificate, Argo CD caData, a hand-edited kubeconfig) fails on the DNS name with x509: certificate signed by unknown authority. The fixes are in x509: certificate signed by unknown authority on the GKE DNS endpoint.

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-endpoint listed the namespaces as before.
  • The kubeconfig with the external address, which had listed them a minute earlier, got no answer. With --request-timeout=20s kubectl printed Unable to connect to the server: context deadline exceeded, and curl to the address timed out on the TCP connect. Nothing refuses the connection, so a client waits for its own timeout.
  • get-credentials without --dns-endpoint wrote the gke.goog name. Google documents this: it “automatically uses the DNS-based endpoint if the IP-based endpoint access is disabled”.
  • get-credentials --internal-ip wrote 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-credentials picks the DNS name by itself once IP access is off; an entry written earlier still holds the address.
  • CI that sets use_internal_ip or 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-ip is 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 Direct IP access can't be disabled for public clusters for a cluster that “uses legacy network”, fixed by a migration to Private Service Connect (Troubleshoot network isolation). Our cluster did not hit it.

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/container.managed.enableControlPlaneDNSOnlyAccess, 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 seeCauseFix
Unable to connect to the server: context deadline exceeded, or kubectl hangs, on an IP address IP access is off and the kubeconfig still has the address get-credentials --dns-endpoint
IP access is disabled for cluster … from get-credentials --internal-ip on a cluster without IP endpoints Drop --internal-ip, use --dns-endpoint
authorized networks fields cannot be mutated if direct IP access is disabled Editing authorized networks while IP access is off Turn IP access on with the list in the same update
Direct IP access can't be disabled for public clusters The cluster uses a legacy network (Google; not reproduced) Migrate the cluster to Private Service Connect
Error 403 (Forbidden)!!1 on the gke.goog name allowExternalTraffic is false --enable-dns-access
x509: certificate signed by unknown authority 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.