Skip to content
Railgrid Docs
Sections
Open console

Enter a topic to search documentation.

    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-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 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 , 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

    Terminal
    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 , then Dex .