Gateway API on GKE
Serve the console, Dex, kcp, and published apps from one GKE Gateway with managed certificates and DNS-only records.
Expose every Railgrid hostname through one Gateway API Gateway backed by a global external Application Load Balancer. This is how Railgrid’s production hub runs on GKE. The same HTTPRoutes work on other Gateway API implementations; the policy objects (HealthCheckPolicy, GCPBackendPolicy) and the certificate map annotation are GKE-specific.
Prerequisites
- A GKE cluster with the Gateway API enabled (
gke-l7-global-external-managedGatewayClass present). - A global static IP, and Google Certificate Manager certificates for
*.example.comand the apps zone (*.apps.example.com), with DNS authorizations, collected in a certificate map. Managed certificates goACTIVEonce Google sees the authorization CNAMEs, which can take up to an hour; the Gateway serves nothing before that. - DNS records for every hostname pointing at the static IP. With Cloudflare as the DNS host, keep them DNS-only (grey cloud): proxying would put a second TLS terminator in front of the load balancer.
- The hub, Dex, and kcp installed per the other pages, or at least their namespaces planned.
The Gateway
One Gateway in its own namespace. Routes from any namespace may attach to the HTTPS listener, which is what lets the infrastructure provider publish apps from tenant namespaces. The HTTP listener only redirects.
apiVersion: v1
kind: Namespace
metadata:
name: gateway
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: railgrid
namespace: gateway
annotations:
networking.gke.io/certmap: railgrid-gateway # your Certificate Manager map
spec:
gatewayClassName: gke-l7-global-external-managed
addresses:
- type: NamedAddress
value: railgrid-gateway # your global static IP's name
listeners:
- name: https
protocol: HTTPS
port: 443
allowedRoutes:
namespaces:
from: All
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: Same
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: http-to-https
namespace: gateway
spec:
parentRefs:
- name: railgrid
sectionName: http
rules:
- filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301With a certificate map, the HTTPS listener carries no tls block. The load balancer takes a few minutes to program; until a route attaches, every hostname answers 404 with a valid certificate.
Three GKE behaviours to design for
Backends are plain HTTP unless the Service says otherwise. The load balancer speaks to a backend with the protocol named in the Service port’s appProtocol. The hub serves HTTPS on :9443, so its Service port needs appProtocol: HTTPS; the hub chart’s Service does not set it. Create your own Service for the Gateway and leave ingress.enabled and httpRoute.enabled off in the hub values. The same applies to the kcp shards and front proxy.
Health checks ignore readiness probes. Without a HealthCheckPolicy, the load balancer sends GET / to the serving port and expects 200. Dex answers 404 on /, kcp wants /readyz, the hub wants /healthz, and a published app’s access gate answers 421 or redirects to sign-in. Attach a policy to every backend Service.
Idle connections close after 30 seconds. Set a GCPBackendPolicy with a long timeoutSec on every backend that carries watches, event streams, MCP sessions, or edge tunnels.
Route the hub
apiVersion: v1
kind: Service
metadata:
name: railgrid-hub-gateway
namespace: railgrid-system
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: railgrid-hub
app.kubernetes.io/instance: railgrid-hub
ports:
- name: https
protocol: TCP
appProtocol: HTTPS
port: 443
targetPort: https
---
apiVersion: networking.gke.io/v1
kind: GCPBackendPolicy
metadata:
name: railgrid-hub-gateway
namespace: railgrid-system
spec:
default:
timeoutSec: 3600
connectionDraining:
drainingTimeoutSec: 60
targetRef:
group: ""
kind: Service
name: railgrid-hub-gateway
---
apiVersion: networking.gke.io/v1
kind: HealthCheckPolicy
metadata:
name: railgrid-hub-gateway
namespace: railgrid-system
spec:
default:
config:
type: HTTPS
httpsHealthCheck:
portSpecification: USE_SERVING_PORT
requestPath: /healthz
targetRef:
group: ""
kind: Service
name: railgrid-hub-gateway
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: railgrid-hub
namespace: railgrid-system
spec:
parentRefs:
- name: railgrid
namespace: gateway
sectionName: https
hostnames:
- console.example.com
rules:
- backendRefs:
- name: railgrid-hub-gateway
port: 443The load balancer re-encrypts to the hub but does not verify the hub’s certificate, so the serving certificate only has to satisfy providers.
In the hub values, list Google’s proxy ranges and the Gateway address in hub.trustedProxyCIDRs:
hub:
trustedProxyCIDRs:
- 35.191.0.0/16
- 130.211.0.0/22
- 203.0.113.10/32 # the Gateway's static IPRoute Dex
Dex serves plain HTTP in the cluster; TLS ends at the Gateway. Point the route at the chart’s Service and give it a /healthz check:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: dex
namespace: dex
spec:
parentRefs:
- name: railgrid
namespace: gateway
sectionName: https
hostnames:
- auth.example.com
rules:
- backendRefs:
- name: dex
port: 5556
---
apiVersion: networking.gke.io/v1
kind: HealthCheckPolicy
metadata:
name: dex
namespace: dex
spec:
default:
config:
type: HTTP
httpHealthCheck:
portSpecification: USE_SERVING_PORT
requestPath: /healthz
targetRef:
group: ""
kind: Service
name: dexRoute kcp
For external kcp
, create one Service per backend (front proxy and each shard) with appProtocol: HTTPS, an HTTPS /readyz health check, a 3600-second backend timeout, and an HTTPRoute per hostname. Give the shard Services fixed clusterIPs and add a Cloud DNS response policy for your VPC that answers each shard hostname with its ClusterIP: in-cluster clients must reach the shards directly with client certificates, which TLS termination at the Gateway would strip. GKE uses kube-dns, so the split horizon has to live in Cloud DNS rather than in a CoreDNS rewrite.
Published apps
The infrastructure provider creates an HTTPRoute per published app in the tenant’s namespace, attached to this Gateway. Configure it with operator.application.gateway.name: railgrid, operator.application.gateway.namespace: gateway, and operator.application.gateway.backendHealthCheck: gke-tcp. The last setting makes the provider add a TCP HealthCheckPolicy for every route backend (provider chart 0.2.34 and later). Without it the default GET / check never passes the app’s access gate and every app answers no healthy upstream. See Self-host Infrastructure
.
A newly published app answers 404 for several minutes while the load balancer programs its route. That is normal.
Verify
kubectl -n gateway get gateway railgrid # PROGRAMMED True, ADDRESS = your IP
kubectl get httproute -A
kubectl get healthcheckpolicy -A # one per backend Service
curl -s https://console.example.com/healthz # {"status":"ok",...}
curl -s -o /dev/null -w '%{http_code}\n' https://auth.example.com/.well-known/openid-configuration # 200If a hostname answers 503 no healthy upstream, the backend’s health check is failing: compare the HealthCheckPolicy with what the backend actually serves on its port. If it answers 404 with a valid certificate, no route has attached for that hostname yet, or the route’s parentRefs do not name this Gateway and listener.
Next
Install the hub , then Dex .