TL;DR Link to heading
I had a Kubernetes service exposed via two different gateway flavours on different clusters. Two of them used an istio Gateway with TLS terminated in-pod, where cert-manager handed it a wildcard cert via a regular Secret. The third used a GKE-managed Gateway (gatewayClassName: gke-l7-global-external-managed), where TLS terminates at a Google Cloud Load Balancer that does not read Kubernetes Secrets. For that one I had to use Google Certificate Manager with a DNS-01 authorisation, then point the Gateway at the resulting cert map via a networking.gke.io/certmap annotation. cert-manager does not fit this path.
Motivation Link to heading
I was wiring three clusters to expose the same internal control plane externally. Two of them had istio gateway, a wildcard cert in a Secret, and an HTTPRoute per host. The third cluster has no istio installed and uses GKE’s managed Gateway implementation for its public ingress. I assumed the same cert dance would work everywhere and started copying the istio HTTPRoute. It did not. The provider docs sorted it out.
Where the assumption breaks Link to heading
cert-manager issues an x509 cert and stores the resulting key pair in a Kubernetes Secret. Any pod with access to that Secret can serve TLS for the names in the cert: an istio gateway pod, an envoy sidecar.
A GKE-managed Gateway is different. The control plane reads the Gateway resource and provisions an external GCLB. TLS terminates at the load balancer, outside the cluster. The GCLB does not have a way to mount a Kubernetes Secret. It has its own cert store, exposed through Google Certificate Manager, and the Gateway references it by annotation:
metadata:
annotations:
networking.gke.io/certmap: my-cert-map
You cannot point a GKE-managed Gateway at a Secret, and you cannot ask cert-manager to write into Certificate Manager: the supply chains run on opposite sides of the cluster boundary.
The pieces you need in Certificate Manager Link to heading
To get a managed cert that the Gateway can use, you create four resources, all in the GCP project that owns the cluster:
resource "google_certificate_manager_dns_authorization" "console" {
name = "console"
domain = "console.example.com"
}
resource "google_certificate_manager_certificate" "console" {
name = "console"
managed {
domains = ["console.example.com"]
dns_authorizations = [google_certificate_manager_dns_authorization.console.id]
}
}
resource "google_certificate_manager_certificate_map" "console" {
name = "console"
}
resource "google_certificate_manager_certificate_map_entry" "console" {
name = "console"
map = google_certificate_manager_certificate_map.console.name
certificates = [google_certificate_manager_certificate.console.id]
hostname = "console.example.com"
}
I picked DNS-01 over HTTP-01. The cert can issue before the Gateway exists, which matters when the Gateway needs a reserved IP that has to land in DNS first. DNS-01 also validates through a Cloudflare-proxied A record; HTTP-01 does not.
The DNS authorisation resource exposes the CNAME you need to publish at the DNS provider:
output "cert-dns-authorization-record" {
value = {
name = google_certificate_manager_dns_authorization.console.dns_resource_record[0].name
type = google_certificate_manager_dns_authorization.console.dns_resource_record[0].type
data = google_certificate_manager_dns_authorization.console.dns_resource_record[0].data
}
}
Publish that CNAME (with proxied = false if you are behind Cloudflare; the validator reads it on the authoritative record) and the cert moves from PROVISIONING to ACTIVE within 15-60 minutes.
Wiring it into the Gateway Link to heading
The Gateway then references the cert map by annotation, and uses a reserved global IP so the public DNS record can be pinned ahead of time:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: console
namespace: console
annotations:
networking.gke.io/certmap: console
spec:
gatewayClassName: gke-l7-global-external-managed
addresses:
- type: NamedAddress
value: ipv4-console-ui
listeners:
- name: https
protocol: HTTPS
port: 443
hostname: console.example.com
allowedRoutes:
namespaces:
from: Same
Three gotchas:
- GKE Ingress uses a
networking.gke.io/addressesannotation. The Gateway controller ignores it; usespec.addresseswithtype: NamedAddressand the IP’s resource name. - A
HealthCheckPolicyis needed if the default GCLB health check (GET/) does not match your backend. My backend pod responds to/healthz?full=true, so the policy points at that path on port 8080. - The cert can be
ACTIVEfor a few minutes before the GCLB serves it. The firstcurlafter activation may still hit a TLS handshake failure. Retry in a minute.
When cert-manager is right Link to heading
For the other two clusters, where the gateway is an istio-managed pod, cert-manager remains the right answer. The istio Gateway listener references the Secret directly:
listeners:
- name: https
protocol: HTTPS
port: 443
hostname: "*.example.com"
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: wildcard-example-com-tls
cert-manager issues the wildcard, refreshes it before expiry, and the istio Gateway picks up whatever is in the Secret. No annotations, no certmap wiring.
What I would do differently Link to heading
If I had to bring a third cluster in line with the first two, I would install istio there too and use cert-manager. Certificate Manager works but adds a parallel toolchain: Terraform resources, a DNS challenge dance, and the address-annotation and health-check gotchas above. For a single hostname the Certificate Manager path is acceptable. For ten hostnames on the same cluster I would rather pay the istio cost once.
cert-manager gets a cert to a TLS terminator inside the cluster. If the terminator sits outside, Google Certificate Manager is the only path to the GCLB.