Phase 2: Platform Configuration

This phase configures the platform prerequisites for MaaS: Kuadrant/Authorino (auth and rate limiting), User Workload Monitoring, the GatewayClass, and the MaaS Gateway.

Apply each step in order and wait for the status gates before proceeding to the next step. The ordering matters because later resources depend on earlier ones (e.g. the Authorino CR references the TLS cert created by the Service annotation, and the Gateway requires the GatewayClass to exist).

All file paths and oc apply commands in this guide are relative to the rhoai-maas-guide repository root. Make sure you have cloned it and are working from its root directory (see Getting Started).
This guide is not a replacement for the official Red Hat OpenShift AI Models as a Service documentation. It is a companion resource with opinionated Kustomize manifests and automation scripts to accelerate deployment.

Prerequisites

  • OpenShift 4.17+ cluster with oc CLI authenticated as cluster-admin

  • Red Hat Connectivity Link (RHCL) operator already installed (see RHCL docs)

  • Phase 1 (operators) completed

Step 1: Kuadrant and Authorino

Kuadrant provides authentication (Authorino) and rate limiting (Limitador) for MaaS API endpoints. These three configurations:

  • Create the kuadrant-system namespace

  • Pre-configure the Authorino Service with a service-ca annotation to auto-generate a TLS certificate (authorino-server-cert) for secure Gateway-to-Authorino communication

  • Deploy the Kuadrant CR with observability enabled, which triggers the Kuadrant operator to install Authorino and Limitador components. The observability.enable: true setting ensures these components expose metrics that User Workload Monitoring can scrape, which is essential for the optional observability dashboards in Phase 7.

The Kuadrant operator auto-creates an Authorino instance when reconciling the Kuadrant resource. You’ll enable TLS on that Authorino instance in the next step.

Apply the namespace, service annotation, and Kuadrant CR. Do not apply the full kustomization in one shot. The Kuadrant operator auto-creates an Authorino CR when it reconciles the Kuadrant resource, so the Authorino CR must be configured separately via oc patch after Kuadrant is ready.

oc apply -f manifests/02-platform-config/kuadrant/namespace.yaml
oc apply -f manifests/02-platform-config/kuadrant/service-annotation.yaml
oc apply -f manifests/02-platform-config/kuadrant/kuadrant.yaml

Wait for Kuadrant to become ready:

oc wait --for=condition=Ready kuadrant/kuadrant -n kuadrant-system --timeout=120s

Troubleshooting: If Kuadrant reports MissingDependency (Istio race condition), restart the Kuadrant operator pod and wait again:

oc delete pod -n openshift-operators \
  $(oc get pods -n openshift-operators --no-headers | grep kuadrant-operator | awk '{print $1}')
oc wait --for=condition=Ready kuadrant/kuadrant -n kuadrant-system --timeout=180s

Step 2: Configure TLS for Models-as-a-Service

Now that Kuadrant is deployed, we need to update the Authorino CR to enable TLS. (Detailed documentation for this step is available in the RHOAI 3.4 docs section 1.4. ) Follow the four steps below to enable TLS between the Gateway and Authorino.

Step 2a: The service annotation (already applied above) triggered the service-ca operator to generate the authorino-server-cert TLS Secret:

oc get secret authorino-server-cert -n kuadrant-system

Step 2b: Patch the Authorino CR to enable the TLS listener:

oc patch authorino authorino -n kuadrant-system --type=merge --patch '{
  "spec": {
    "listener": {
      "tls": {
        "enabled": true,
        "certSecretRef": {
          "name": "authorino-server-cert"
        }
      }
    }
  }
}'

Step 2c: Configure Authorino deployment with TLS certificate env vars:

oc -n kuadrant-system set env deployment/authorino \
  SSL_CERT_FILE=/etc/ssl/certs/openshift-service-ca/service-ca-bundle.crt \
  REQUESTS_CA_BUNDLE=/etc/ssl/certs/openshift-service-ca/service-ca-bundle.crt

Wait for the Authorino deployment to become available:

oc wait --for=condition=Available deployment/authorino -n kuadrant-system --timeout=300s

Step 3: User Workload Monitoring (UWM)

Enable User Workload Monitoring so that Prometheus scrapes MaaS and Kuadrant metrics from user namespaces.

oc apply -k manifests/02-platform-config/uwm/

Wait for the user-workload monitoring stack to start:

The prometheus-operator deployment may take 10-20 seconds to appear after applying the ConfigMap. If the wait command returns "not found", retry after a few seconds.
oc wait --for=condition=Available deployment/prometheus-operator \
  -n openshift-user-workload-monitoring --timeout=300s

Verify the Prometheus pods are running:

oc get pods -n openshift-user-workload-monitoring

You should see prometheus-user-workload-0 and thanos-ruler-user-workload-0 pods in Running state.

Expected output:

NAME                                   READY   STATUS    RESTARTS   AGE
prometheus-operator-76b9c6d5dc-rrvn8   2/2     Running   0          23m
prometheus-user-workload-0             6/6     Running   0          23m
thanos-ruler-user-workload-0           4/4     Running   0          23m

Step 4: GatewayClass

Apply the GatewayClass that initializes OpenShift’s built-in Gateway API controller:

oc apply -f manifests/02-platform-config/gatewayclass.yaml

Wait for the GatewayClass to be accepted:

oc wait --for=condition=Accepted gatewayclass/openshift-default --timeout=120s

Verify:

oc get gatewayclass openshift-default

Expected output:

NAME                CONTROLLER                           ACCEPTED   AGE
openshift-default   openshift.io/gateway-controller/v1   True       ...

Step 5: MaaS Gateway

The Gateway uses cluster-specific values (domain and TLS cert name), so it is provided as an envsubst template. Here we will extract the values, render the template, and apply them.

Step 5a: Check Your Platform Type

Before creating the Gateway, determine if your cluster requires additional configuration for non-cloud platforms:

oc get infrastructure cluster -o jsonpath='{.status.platform}'
Platform output What to do

AWS, Azure, GCP (cloud)

Skip to Step 5b — create the Gateway directly.

None, BareMetal, OpenStack

Continue to Step 5a.1 — verify MetalLB is installed first.

Step 5a.1: Verify MetalLB is Installed (Non-Cloud Platforms Only)

Check if MetalLB is installed:

oc get deployment metallb-operator-controller-manager -n metallb-system 2>/dev/null

If you see "NotFound" or "Error":

MetalLB is not installed. Without it, the Gateway will never reach Programmed=True because there is no cloud load balancer to assign an external IP.

You must go back and complete: Phase 1: MetalLB for Non-Cloud Clusters

After installing MetalLB and configuring an IPAddressPool, return to this step.

If MetalLB is installed: Continue to the next step.

Step 5b: Create the MaaS Gateway

First, apply the Gateway resource overrides ConfigMap. This sets the gateway proxy memory limit to 2Gi (the Istio default of 1Gi is insufficient when Kuadrant Wasm extensions are loaded):

oc apply -f manifests/02-platform-config/gateway-resources.yaml

Now create the Gateway. The Gateway template includes spec.infrastructure.parametersRef pointing to the ConfigMap above, so Istio applies the 2Gi limit to the generated Deployment and preserves it across reconciliations.

export CLUSTER_DOMAIN=$(oc get ingresses.config.openshift.io cluster \
  -o jsonpath='{.spec.domain}')
echo $CLUSTER_DOMAIN
export CERT_NAME=$(oc get ingresscontroller default \
  -n openshift-ingress-operator \
  -o jsonpath='{.spec.defaultCertificate.name}' 2>/dev/null)
export CERT_NAME="${CERT_NAME:-router-certs-default}"
echo $CERT_NAME
envsubst '${CLUSTER_DOMAIN} ${CERT_NAME}' < manifests/02-platform-config/gateway.yaml.tmpl | oc apply -f -

Step 5c: Annotate the Gateway for Authorino TLS bootstrap:

oc annotate gateway maas-default-gateway -n openshift-ingress \
  security.opendatahub.io/authorino-tls-bootstrap="true" --overwrite

Wait for the Gateway to be programmed:

oc wait --for=condition=Programmed gateway/maas-default-gateway \
  -n openshift-ingress --timeout=120s

Gateway Pod OOMKill Prevention

The ConfigMap applied in Step 5b overrides the Istio default 1Gi memory limit to 2Gi via spec.infrastructure.parametersRef. This prevents the gateway pod from being OOMKilled when Kuadrant Wasm extensions (Authorino auth, Limitador rate limiting) are compiled at startup. Unlike oc patch deployment, this approach survives Istio reconciliation.

If you deployed the Gateway without the ConfigMap (e.g. from an older version of this guide), apply the fix retroactively:

oc apply -f manifests/02-platform-config/gateway-resources.yaml

oc patch gateway maas-default-gateway -n openshift-ingress --type=merge -p '{
  "spec": {
    "infrastructure": {
      "parametersRef": {
        "group": "",
        "kind": "ConfigMap",
        "name": "maas-gateway-options"
      }
    }
  }
}'

Step 5d: Label namespaces for Gateway route binding

The Gateway restricts HTTPRoute attachment to namespaces explicitly labeled with maas.opendatahub.io/gateway-access=true. This follows the principle of least privilege - only namespaces that are designated to expose services through the Gateway can create routes.

For additional Gateway hardening beyond namespace selectors - including RBAC restrictions on HTTPRoute creation, ValidatingAdmissionPolicy enforcement, and audit procedures - see the KCS How to verify and secure configuration of OpenShift AI Gateway for Model Serving.

Any namespace that needs to serve models through the MaaS Gateway must be labeled with maas.opendatahub.io/gateway-access=true. Without this label, HTTPRoutes created in that namespace will not be accepted by the Gateway and the model will not be reachable.

If you create additional namespaces for model serving, remember to apply this label:

oc label namespace <your-namespace> \
  maas.opendatahub.io/gateway-access=true --overwrite

Label the redhat-ods-applications namespace where the MaaS API route is created:

oc label namespace redhat-ods-applications \
  maas.opendatahub.io/gateway-access=true --overwrite
Model namespaces (llm, external-models) are labeled in their respective deployment phases.

Step 5e: Create Passthrough Route (Non-Cloud Platforms Only)

If your platform type from Step 5a was None, BareMetal, or OpenStack, you must create a passthrough Route. This routes external traffic through the OpenShift ingress controller to the Gateway’s LoadBalancer service.

If your platform type from Step 5a was AWS, Azure, GCP, or another cloud provider, you can skip this step.
export CLUSTER_DOMAIN=$(oc get ingresses.config.openshift.io cluster \
  -o jsonpath='{.spec.domain}')

envsubst '${CLUSTER_DOMAIN}' < manifests/03-maas-platform/openshift-gateway-setup/route.yaml.tmpl | oc apply -f -

Why is this needed?

On non-cloud platforms, the Gateway’s LoadBalancer service receives an IP from MetalLB (e.g., 10.10.10.11), but that IP is only routable within the cluster network. External traffic cannot reach it directly.

The passthrough Route bridges external requests (via the OpenShift router at your cluster’s public DNS) to the internal LoadBalancer IP, allowing the Gateway to be accessible from outside the cluster.

Verify the Route was created:

oc get route maas-default-gateway-https -n openshift-ingress

Expected output:

NAME                         HOST/PORT                              PATH   SERVICES                                 PORT    TERMINATION            WILDCARD
maas-default-gateway-https   maas.apps.<cluster-domain>                    maas-default-gateway-openshift-default   https   passthrough/Redirect   None

Verification

After completing all steps, confirm the full platform state:

On macOS, the local DNS resolver may cache negative lookups for maas.apps.<cluster-domain>. If curl reports "Could not resolve host" but dig or nslookup resolves the address correctly, either flush your DNS cache (sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder) or use the --resolve flag: curl -vsk --resolve "maas.${CLUSTER_DOMAIN}:443:$(dig +short maas.${CLUSTER_DOMAIN} | head -1)" https://maas.${CLUSTER_DOMAIN}.
# Kuadrant ready
oc get kuadrant -n kuadrant-system

# Authorino running with TLS
oc get deployment authorino -n kuadrant-system
oc get secret authorino-server-cert -n kuadrant-system

# UWM running
oc get pods -n openshift-user-workload-monitoring

# GatewayClass accepted
oc get gatewayclass openshift-default

# Gateway programmed
oc get gateway maas-default-gateway -n openshift-ingress

# Check passthrough route exists (bare metal, Open Stack or None platform type)
oc get route maas-default-gateway-https -n openshift-ingress -o jsonpath='{.status.ingress[0].conditions[?(@.type=="Admitted")].status}'

# Verify TLS connection to the Gateway
CLUSTER_DOMAIN=$(oc get ingresses.config.openshift.io cluster -o jsonpath='{.spec.domain}')
echo $CLUSTER_DOMAIN
curl -vsk https://maas.${CLUSTER_DOMAIN} 2>&1 | grep -E "SSL connection|Connected"

Expected output:

$ oc get kuadrant -n kuadrant-system
NAME       MTLS AUTHORINO   MTLS LIMITADOR   AGE
kuadrant   false            false            49m

$ oc get deployment authorino -n kuadrant-system
oc get secret authorino-server-cert -n kuadrant-system
NAME        READY   UP-TO-DATE   AVAILABLE   AGE
authorino   1/1     1            1           49m
NAME                    TYPE                DATA   AGE
authorino-server-cert   kubernetes.io/tls   2      49m

$ oc get pods -n openshift-user-workload-monitoring
NAME                                   READY   STATUS    RESTARTS   AGE
prometheus-operator-76b9c6d5dc-rrvn8   2/2     Running   0          23m
prometheus-user-workload-0             6/6     Running   0          23m
thanos-ruler-user-workload-0           4/4     Running   0          23m

$ oc get gatewayclass openshift-default
NAME                CONTROLLER                           ACCEPTED   AGE
openshift-default   openshift.io/gateway-controller/v1   True       23m

$ oc get gateway maas-default-gateway -n openshift-ingress
NAME                   CLASS               ADDRESS       PROGRAMMED   AGE
maas-default-gateway   openshift-default   10.10.10.11   True         13m

# (If you created a passthrough route in Step 5e)
$ oc get route maas-default-gateway-https -n openshift-ingress -o jsonpath='{.status.ingress[0].conditions[?(@.type=="Admitted")].status}'
True

$ CLUSTER_DOMAIN=$(oc get ingresses.config.openshift.io cluster -o jsonpath='{.spec.domain}')
$ echo $CLUSTER_DOMAIN
apps.`<your cluster name>`.`<your domain>`
curl -vsk https://maas.${CLUSTER_DOMAIN} 2>&1 | grep -E "SSL connection|Connected"
* Connected to maas.apps.<cluster-domain> (...) port 443
* SSL connection using TLSv1.3 / ...    <-- exact cipher varies by client

Appendix

Directory Structure

manifests/02-platform-config/
  gateway-resources.yaml         # ConfigMap: gateway proxy resource overrides (2Gi memory)
  gateway.yaml.tmpl              # Gateway template (envsubst, references ConfigMap via parametersRef)
  gatewayclass.yaml              # GatewayClass resource
  kustomization.yaml             # Aggregates all subdirectories
  kuadrant/
    namespace.yaml               # kuadrant-system namespace
    service-annotation.yaml      # TLS cert annotation for Authorino
    kuadrant.yaml                # Kuadrant CR
    authorino.yaml               # Authorino TLS patch
    kustomization.yaml
  uwm/
    cluster-monitoring-config.yaml   # User Workload Monitoring ConfigMap
    kustomization.yaml

Next step