# Gateway API on GKE

Serve the console, Dex, kcp, and published apps from one GKE Gateway with managed certificates and DNS-only records.

Source: https://railgrid.ai/docs/self-hosting/hub/gateway-api/


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 `HTTPRoute`s 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-managed` GatewayClass present).
- A global static IP, and Google Certificate Manager certificates for `*.example.com` and the apps zone (`*.apps.example.com`), with DNS authorizations, collected in a certificate map. Managed certificates go `ACTIVE` once 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.

```yaml
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: 301
```

With 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

```yaml
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: 443
```

The load balancer re-encrypts to the hub but does not verify the hub's certificate, so the [serving certificate](https://railgrid.ai/docs/self-hosting/hub/helm/#serving-certificate-and-provider-trust) only has to satisfy providers.

In the hub values, list Google's proxy ranges and the Gateway address in `hub.trustedProxyCIDRs`:

```yaml
hub:
  trustedProxyCIDRs:
    - 35.191.0.0/16
    - 130.211.0.0/22
    - 203.0.113.10/32     # the Gateway's static IP
```

## Route 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:

```yaml
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: dex
```

## Route kcp

For [external kcp](https://railgrid.ai/docs/self-hosting/hub/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 `clusterIP`s 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](https://railgrid.ai/docs/self-hosting/providers/infrastructure/).

A newly published app answers 404 for several minutes while the load balancer programs its route. That is normal.

## Verify

```bash
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   # 200
```

If 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](https://railgrid.ai/docs/self-hosting/hub/helm/#configure-a-production-hub), then [Dex](https://railgrid.ai/docs/self-hosting/hub/oidc/).
