Skip to content

Gateway Patterns

This page provides curated Gateway API deployment patterns for MaaS on OpenShift. Each pattern includes copy-pasteable YAML manifests, prerequisites, verification steps, and troubleshooting guidance for common failure modes.

Related topics:

Pattern index

Pattern Environment One-line purpose
ClusterIP + Route re-encrypt Dev / Lab / Production ClusterIP Gateway Service fronted by an OpenShift Route with re-encrypt TLS; no external LoadBalancer required

Environment matrix

Use this table to decide which pattern fits your deployment:

Environment Recommended pattern Who terminates client TLS? Internal TLS Namespace expectations
Development / Lab ClusterIP + Route re-encrypt OpenShift Router (wildcard or self-signed cert) service-ca auto-provisioned cert (re-encrypt to Gateway) openshift-ingress for Gateway and Route; application namespace for HTTPRoute
Production ClusterIP + Route re-encrypt OpenShift Router (CA-signed certificate) service-ca auto-provisioned cert (re-encrypt to Gateway) openshift-ingress for Gateway and Route; application namespace for HTTPRoute

TLS responsibilities summary:

  • Client-facing TLS: Managed by the OpenShift Router. In production, replace the default wildcard certificate with a CA-signed certificate on the IngressController.
  • Gateway Service TLS: Auto-provisioned by the OpenShift service-ca-operator via the service.beta.openshift.io/serving-cert-secret-name annotation. The Secret name in the ConfigMap annotation must match the Gateway listener certificateRefs.
  • Authorino and maas-api TLS: Configured separately. See TLS Configuration.

ClusterIP Gateway with OpenShift Route (re-encrypt)

Unsupported Configuration

This pattern mixes incompatible routing paradigms by connecting OpenShift Routes (HAProxy-based) to Kubernetes Gateway API resources (Envoy-based).

Known limitations:

  • The /v1/tenants endpoint cannot discover the external hostname (Gateway has no hostname in spec.listeners, returns error instead of internal service name)
  • Not compliant with Gateway API specification
  • May have unexpected behavior or compatibility issues

Recommended alternative: Deploy Gateway with LoadBalancer service type and configure spec.listeners[].hostname to follow the Gateway API standard pattern.

This pattern deploys a Gateway API Gateway as a ClusterIP service (no external LoadBalancer) and uses an OpenShift Route with re-encrypt TLS termination to expose the Gateway externally.

Traffic flow

graph LR
    Client -->|HTTPS| Router["OpenShift Router"]
    Router -->|re-encrypt TLS| GWSvc["Gateway Service<br/>ClusterIP:443"]
    GWSvc --> Envoy["Istio / Envoy"]
    Envoy -->|HTTPRoute| MaaSAPI["maas-api:8443"]
  1. The client connects over HTTPS to the OpenShift Router.
  2. The Router terminates the client TLS session and opens a new TLS connection (re-encrypt) to the Gateway Service using the service-ca certificate.
  3. The Gateway (Istio/Envoy) terminates that connection and routes traffic via HTTPRoute to backend services such as maas-api.

Why ClusterIP + Route?

  • No LoadBalancer required. Works on bare-metal, restricted cloud accounts, and environments where LoadBalancer provisioning is slow or unavailable.
  • OpenShift-native ingress. Leverages the platform Router for TLS termination, hostname routing, and integration with existing wildcard certificates.
  • Re-encrypt for defense in depth. Traffic is encrypted between the Router and the Gateway Service, not just at the edge.

Prerequisites

Manifests

All manifests are in docs/samples/gateway-patterns/clusterip-route-reencrypt/.

1. GatewayClass

apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: openshift-default
spec:
  controllerName: "openshift.io/gateway-controller/v1"

If openshift-default already exists on your cluster, skip this step.

2. Gateway options ConfigMap

The ConfigMap configures the Gateway Service as ClusterIP and requests a service-ca TLS certificate:

apiVersion: v1
kind: ConfigMap
metadata:
  name: gw-options
  namespace: openshift-ingress
data:
  service: |
    metadata:
      annotations:
        service.beta.openshift.io/serving-cert-secret-name: "maas-gw-service-tls"
    spec:
      type: ClusterIP
Field Purpose
serving-cert-secret-name The service-ca-operator provisions a TLS certificate into this Secret. Must match the Gateway certificateRefs.
type: ClusterIP No external LoadBalancer; the OpenShift Route handles external ingress.

3. Gateway

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: maas-default-gateway
  namespace: openshift-ingress
  labels:
    kuadrant.io/gateway: "true"
    app.kubernetes.io/name: maas-gateway
  annotations:
    opendatahub.io/managed: "false"
    security.opendatahub.io/authorino-tls-bootstrap: "true"
spec:
  gatewayClassName: openshift-default
  infrastructure:
    parametersRef:
      group: ""
      kind: ConfigMap
      name: gw-options
  listeners:
    - name: https
      port: 443
      protocol: HTTPS
      allowedRoutes:
        namespaces:
          from: Selector
          selector:
            matchLabels:
              maas.opendatahub.io/gateway-access: "true"
      tls:
        certificateRefs:
          - group: ""
            kind: Secret
            name: maas-gw-service-tls
        mode: Terminate
Label / Annotation Purpose
kuadrant.io/gateway: "true" Enables Kuadrant/RHCL policy attachment (AuthPolicy, RateLimitPolicy)
opendatahub.io/managed: "false" Lets maas-controller manage AuthPolicies; prevents ODH Model Controller from overwriting them
security.opendatahub.io/authorino-tls-bootstrap: "true" Creates the EnvoyFilter for Gateway → Authorino TLS communication
infrastructure.parametersRef References the gw-options ConfigMap for ClusterIP and service-ca configuration

4. OpenShift Route

The Route exposes the Gateway Service externally with re-encrypt TLS:

apiVersion: route.openshift.io/v1
kind: Route
metadata:
  name: maas-gateway-route
  namespace: openshift-ingress
spec:
  host: maas.<cluster-domain>   # Replace with your cluster domain
  to:
    kind: Service
    name: maas-default-gateway-openshift-default  # Verify with kubectl get svc -n openshift-ingress
    weight: 100
  port:
    targetPort: 443
  tls:
    termination: reencrypt
    insecureEdgeTerminationPolicy: Redirect
    # The router needs the service CA bundle to verify the backend certificate
    # during reencrypt handshake. Fetch it with:
    #   kubectl get configmap signing-cabundle -n openshift-service-ca -o jsonpath='{.data.ca-bundle\.crt}'
    destinationCACertificate: |
      -----BEGIN CERTIFICATE-----
      ... (paste service CA bundle here) ...
      -----END CERTIFICATE-----

Service name

The Gateway controller generates a Service name from the Gateway name and GatewayClass name. Verify the actual name:

kubectl get svc -n openshift-ingress \
  -l gateway.networking.k8s.io/gateway-name=maas-default-gateway

Hostname placeholder

Replace maas.<cluster-domain> with your actual hostname. Retrieve the cluster domain with:

kubectl get ingresses.config.openshift.io cluster -o jsonpath='{.spec.domain}'

5. HTTPRoute

Attach workloads to the Gateway. The Gateway allowedRoutes uses a label selector. The controller automatically labels the infrastructure namespace (see Automatic infrastructure namespace labeling). Label any additional namespaces (e.g. model namespaces) manually:

# Label model namespaces that need to attach HTTPRoutes
oc label namespace <model-namespace> maas.opendatahub.io/gateway-access=true --overwrite

This example routes all traffic to maas-api:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: maas-api
  namespace: <application-namespace>
spec:
  parentRefs:
    - name: maas-default-gateway
      namespace: openshift-ingress
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: maas-api
          port: 8443

Replace <application-namespace> with your ODH/RHOAI application namespace (opendatahub or redhat-ods-applications).

Note

The HTTPRoute for maas-api is created automatically by maas-controller by default. You only need to create it manually if you are not using maas-controller.

Apply

# Apply GatewayClass, ConfigMap, and Gateway
kustomize build docs/samples/gateway-patterns/clusterip-route-reencrypt | kubectl apply -f -

# Wait for the Gateway to be programmed
kubectl wait --for=condition=Programmed gateway/maas-default-gateway \
  -n openshift-ingress --timeout=60s

# Determine your cluster domain and the Gateway Service name
CLUSTER_DOMAIN=$(kubectl get ingresses.config.openshift.io cluster -o jsonpath='{.spec.domain}')
GW_SVC=$(kubectl get svc -n openshift-ingress \
  -l gateway.networking.k8s.io/gateway-name=maas-default-gateway \
  -o jsonpath='{.items[0].metadata.name}')

# Fetch the service CA bundle for reencrypt termination
SERVICE_CA=$(kubectl get configmap signing-cabundle -n openshift-service-ca -o jsonpath='{.data.ca-bundle\.crt}')

# Create the Route (update host and service name in openshift-route.yaml, or apply inline)
kubectl apply -f - <<EOF
apiVersion: route.openshift.io/v1
kind: Route
metadata:
  name: maas-gateway-route
  namespace: openshift-ingress
spec:
  host: maas.${CLUSTER_DOMAIN}
  to:
    kind: Service
    name: ${GW_SVC}
    weight: 100
  port:
    targetPort: 443
  tls:
    termination: reencrypt
    insecureEdgeTerminationPolicy: Redirect
    destinationCACertificate: |
$(echo "$SERVICE_CA" | sed 's/^/      /')
EOF

Verify

# Gateway should show Programmed=True
kubectl get gateway maas-default-gateway -n openshift-ingress

# Service should be ClusterIP
kubectl get svc -n openshift-ingress \
  -l gateway.networking.k8s.io/gateway-name=maas-default-gateway

# Route should be Admitted
kubectl get route maas-gateway-route -n openshift-ingress

# TLS certificate Secret should exist
kubectl get secret maas-gw-service-tls -n openshift-ingress

Troubleshooting

Symptom Likely cause Fix
Gateway stays NotProgrammed GatewayClass not accepted, or gw-options ConfigMap missing Check kubectl get gatewayclass; verify ConfigMap in openshift-ingress
Route shows HostAlreadyClaimed Another Route uses the same hostname Change spec.host to a unique FQDN
503 from the Route Gateway Service not ready or certificate not yet provisioned Wait for service-ca-operator to provision maas-gw-service-tls; check kubectl get secret -n openshift-ingress
TLS handshake failure (re-encrypt) Service CA cert not trusted by the Router Ensure tls.destinationCACertificate contains the service CA bundle from signing-cabundle ConfigMap in openshift-service-ca namespace
certificateRefs name mismatch Secret name in Gateway does not match ConfigMap annotation Verify both reference the same Secret name (maas-gw-service-tls)

Automatic infrastructure namespace labeling

When a Gateway uses allowedRoutes.namespaces.from: Selector with matchLabels, the MaaS controller automatically applies those labels to the infrastructure namespace (where maas-api-route HTTPRoutes live). This ensures the Gateway accepts MaaS API routes without manual labeling of the infra namespace.

Model namespaces still require manual labeling — the controller only manages the infrastructure namespace.

How it works

Each time an AITenant is reconciled, the controller reads the Gateway's listener matchLabels and applies them to the infra namespace. An annotation (maas.opendatahub.io/gateway-selector-labels) tracks which gateway owns each label so that labels can be cleaned up when a tenant is deleted or a gateway's selector changes.

When multiple gateways share the same label (e.g. both use maas-gateway-access=true), all are recorded as co-owners. Deleting one tenant does not remove a label that another tenant's gateway still requires.

The controller also watches for Gateway spec changes and re-reconciles the affected AITenants automatically.

Conflict errors

Two situations produce reconciliation errors visible in the AITenant status (condition type: Degraded, reason: InfraNamespaceLabelFailed):

Intra-gateway conflict: A single gateway has two listeners that disagree on a label value (e.g. listener https wants env=prod and listener https-alt wants env=staging). Fix the gateway so all listeners use the same value for each label key.

Cross-gateway conflict: Two gateways want different values for the same label key. For example, gateway-a set env=prod and gateway-b wants env=staging. Use distinct label keys per gateway (e.g. gateway-a-access=true and gateway-b-access=true) or ensure both use the same value.

A co-owner cannot change a shared label's value while other gateways still depend on the current value. The conflicting tenant's AITenant status reports which gateway owns the contested label.

Limitations

  • Only matchLabels is supported. Gateways using matchExpressions require manual namespace configuration.
  • The controller does not label model namespaces — only the infrastructure namespace.
  • Labels not selected by any managed Gateway are not tracked in the ownership annotation. The controller does not manage or clean up those labels. Remove them manually if they are no longer needed:
    oc label namespace <infra-namespace> <label-key>-
    
  • If an existing label on the infrastructure namespace conflicts with a gateway's desired value, the controller rejects the reconciliation rather than overwriting it. Resolve the conflict manually before retrying.

MaaS integration

After deploying a gateway pattern, complete the MaaS-specific steps:

  1. Attach models to the Gateway. Set spec.router.gateway.refs on your LLMInferenceService to point at the deployed Gateway. See Model Setup Guide.

  2. Configure end-to-end TLS. Set up Authorino listener TLS and maas-api backend TLS. See TLS Configuration.

  3. Complete MaaS installation. If you have not already, follow the full installation flow: database, DataScienceCluster, and validation. See Install MaaS Components.

  4. Deploy sample models. Use the bundled model samples to verify the gateway is working end-to-end. See Model Setup.