Install MaaS Components
Complete Operator Setup before proceeding.
Installation flow:
- Database Setup — Create the PostgreSQL connection Secret
- Create Gateway — Deploy maas-default-gateway (required before modelsAsAService)
- Configure DataScienceCluster — Enable modelsAsAService in your DataScienceCluster
- Model Setup — Deploy sample models
- Validation — Verify the deployment
- Enable Observability — Set up metrics, dashboards, and monitoring (recommended)
Database Setup
maas-api uses PostgreSQL as its persistence layer for API key metadata: hashed tokens, subscription bindings, expiration dates, and revocation state. The database must be reachable before maas-api starts; the pod will crash-loop until the connection succeeds and the schema migration completes.
Namespace Separation
When infrastructure namespace separation is enabled (the default), create maas-db-config
in the infrastructure namespace (e.g., odh-ai-gateway-infra for ODH or
redhat-ai-gateway-infra for RHOAI), not the controller namespace. The infrastructure
namespace is the source of truth for database credentials. See
Infrastructure Namespace Separation
for details.
Create the maas-db-config Secret in your ODH/RHOAI namespace (typically opendatahub for ODH or redhat-ods-applications for RHOAI):
kubectl create secret generic maas-db-config \
-n opendatahub \
--from-literal=DB_CONNECTION_URL='postgresql://username:password@hostname:5432/database?sslmode=require'
Connection string format:
Development
For development, you can deploy a PostgreSQL instance and Secret using the setup script:
Setting the namespace: The script defaults to opendatahub. Set the NAMESPACE environment variable if your MaaS deployment uses a different namespace:
# RHOAI uses redhat-ods-applications
NAMESPACE=redhat-ods-applications ./scripts/setup-database.sh
# Custom namespace
NAMESPACE=my-maas-namespace ./scripts/setup-database.sh
The full scripts/deploy.sh script also creates PostgreSQL automatically when deploying MaaS.
Using deploy.sh with an external database
If you use scripts/deploy.sh, you can supply your own PostgreSQL connection string with the --postgres-connection flag. This skips the built-in POC PostgreSQL deployment and creates the maas-db-config Secret automatically:
Restarting maas-api
If you add or update the Secret after the DataScienceCluster already has modelsAsAService in managed state, restart the maas-api deployment to pick up the config:
This is not required when the Secret exists before enabling modelsAsAService in your DataScienceCluster.
Create Gateway
Create maas-default-gateway in openshift-ingress before enabling aigateway.modelsAsAService in your DataScienceCluster.
scripts/deploy.sh runs this step automatically in loadbalancer mode. Run the script yourself when installing via DataScienceCluster first, using ocproute mode, or on disconnected clusters.
| Deployment Model | Command |
|---|---|
| External LoadBalancer (default) | ./scripts/setup-gateway.sh |
| OpenShift Route (any cluster) | INGRESS_MODE=ocproute ./scripts/setup-gateway.sh |
| Air-gapped / No GitHub fetch | DISCONNECTED=true INGRESS_MODE=ocproute ./scripts/setup-gateway.sh |
Common overrides: CLUSTER_DOMAIN, CERT_NAME (loadbalancer mode only), DRY_RUN, MAAS_MANIFEST_REF (pinned git ref for remote kustomize fallback). For the full variable list, TLS auto-detection order, and examples, see scripts/README.md.
Verify:
kubectl wait --for=condition=Programmed gateway/maas-default-gateway -n openshift-ingress --timeout=120s
Expected output:
For ClusterIP mode, also verify the Route:
Expected output:
NAME HOST/PORT PATH SERVICES PORT TERMINATION WILDCARD
maas-gateway-route maas.apps.example.cluster.com maas-default-gateway-openshift-default 443 reencrypt None
For ClusterIP architecture and troubleshooting, see Gateway patterns — ClusterIP + Route.
Required annotations
The script applies opendatahub.io/managed: "false" and security.opendatahub.io/authorino-tls-bootstrap: "true" on the Gateway. See TLS Configuration for custom certificates and Authorino integration. The authorino-tls-bootstrap annotation is interim until CONNLINK-528.
Authorino TLS
After the Gateway exists, run ./scripts/setup-authorino-tls.sh. For RHOAI/RHCL, set AUTHORINO_NAMESPACE=rh-connectivity-link.
Configure DataScienceCluster
After creating the database Secret and Gateways, create or update your DataScienceCluster. Choose your deployment method:
The AI Gateway Operator deploys maas-controller, which self-bootstraps AITenant/models-as-a-service; that AITenant creates or adopts MaasTenantConfig/default-tenant and reconciles the MaaS platform workloads (maas-api, gateway policies, telemetry). Create or update your DataScienceCluster with modelsAsAService in Managed state:
KServe not required for MaaS
MaaS is now deployed as a sub-component of the AI Gateway Operator (aigateway.modelsAsAService), not KServe. You no longer need KServe enabled to use MaaS. Include KServe only if you need model serving capabilities independently.
kubectl apply -f - <<EOF
apiVersion: datasciencecluster.opendatahub.io/v2
kind: DataScienceCluster
metadata:
name: default-dsc
spec:
components:
aigateway:
managementState: Managed
modelsAsAService:
managementState: Managed
dashboard:
managementState: Managed
llamastackoperator:
managementState: Managed
EOF
LlamaStack Operator (GenAI Studio)
The llamastackoperator component is required for GenAI Studio functionality.
If you do not need GenAI Studio, you can omit it or set it to Removed.
You also need to enable the genAiStudio feature flag on OdhDashboardConfig —
see OdhDashboardConfig Feature Flags below.
Connectivity Link warning (ODH with Kuadrant)
When using ODH with Kuadrant (upstream), you may see Warning: Red Hat Connectivity Link is not installed, LLMInferenceService cannot be used initially. This typically resolves after a few minutes as the operator reconciles. If it persists, run kubectl describe datasciencecluster default-dsc and check that the Kuadrant/Connectivity Link operator is installed and healthy.
Validate DataScienceCluster:
# Check DataScienceCluster status
kubectl get datasciencecluster default-dsc
# Wait for MaaS to be ready (optional)
kubectl wait --for=jsonpath='{.status.conditions[?(@.type=="ModelControllerReady")].status}'=True \
datasciencecluster/default-dsc --timeout=300s
# Verify maas-api deployment is running (use opendatahub for ODH, redhat-ods-applications for RHOAI)
kubectl get deployment maas-api -n opendatahub
kubectl rollout status deployment/maas-api -n opendatahub --timeout=120s
The maas-controller (deployed by the operator) will automatically create AITenant/models-as-a-service, which creates or adopts MaasTenantConfig/default-tenant and reconciles:
- MaaS API (Deployment, Service, ServiceAccount, ClusterRole, ClusterRoleBinding, HTTPRoute)
- MaaS API AuthPolicy (maas-api-auth-policy) - Protects the MaaS API endpoint
- Gateway default AuthPolicy (gateway-default-auth) - Denies unauthenticated traffic
- Gateway default TokenRateLimitPolicy (gateway-default-deny) - Denies unsubscribed traffic
- TelemetryPolicy and Istio Telemetry (when telemetry is enabled)
- DestinationRule (when TLS is enabled)
- NetworkPolicy (maas-authorino-allow) - Allows Authorino to reach MaaS API
MaasTenantConfig CR
With modelsAsAService Managed, the ODH/RHOAI operator (via the aigateway component) deploys maas-controller, which self-bootstraps AITenant/models-as-a-service in ai-tenants. The AITenant reconciler creates or adopts a namespace-scoped MaasTenantConfig object. The resource name must be default-tenant (enforced via CEL validation). The MaasTenantConfig CR lives in the models-as-a-service namespace (same namespace as MaaSSubscription and MaaSAuthPolicy). The authoritative API definition is in the maas-controller repo: maastenantconfig_types.go.
Nothing in spec is required for a default install. If you omit spec, telemetry metric toggles use the defaults described below. During bootstrap, existing Tenant/default-tenant.spec.externalOIDC settings are automatically migrated to AITenant/models-as-a-service.spec.oidc, and existing Tenant/default-tenant.spec.apiKeys and spec.telemetry settings are copied into MaasTenantConfig/default-tenant when those fields are unset. Going forward, set OIDC and Gateway on AITenant; set API key and telemetry settings on MaasTenantConfig.
| Field | What to set |
|---|---|
spec.apiKeys.maxExpirationDays |
Maximum allowed API key lifetime in days. When set, users cannot mint keys with a longer lifetime than this value (via expiresIn). Optional; if unset, the controller does not apply a cap through this field (see also maas-api / API_KEY_MAX_EXPIRATION_DAYS in your deployment). |
spec.apiKeys.deletionRetentionDays |
Retention in days for lifecycle-invalidated API keys before physical deletion. Defaults to 90 and must be at least 1. |
spec.telemetry.enabled |
Enable TelemetryPolicy and Istio Telemetry (default true). |
spec.telemetry.metrics.captureOrganization |
Include organization_id on metrics (default true). |
spec.telemetry.metrics.captureUser |
Include user labels on metrics (default false; privacy-sensitive). |
spec.telemetry.metrics.captureGroup |
Include group labels on metrics (default false; higher cardinality). |
spec.telemetry.metrics.captureModelUsage |
Include model labels on usage metrics (default true). |
spec.telemetry.logs.captureUser |
Include user_id on usage logs (default false; privacy-sensitive). Independent of metrics.captureUser. |
spec.telemetry.logs.captureOrganization |
Include organization_id on usage logs (default true). Independent of metrics.captureOrganization. |
Example (patch common values):
apiVersion: maas.opendatahub.io/v1alpha1
kind: MaasTenantConfig
metadata:
name: default-tenant
namespace: models-as-a-service
spec:
apiKeys:
maxExpirationDays: 90
deletionRetentionDays: 90
telemetry:
enabled: true
metrics:
captureUser: false
captureGroup: false
logs:
captureUser: false
captureOrganization: true
Development and early testing
Kustomize deployment can be used for development and early testing purposes. For production, use the Managed tab above.
Set modelsAsAService to Removed so the AI Gateway Operator does not deploy MaaS, then deploy MaaS directly from the canonical build root:
kubectl apply -f - <<EOF
apiVersion: datasciencecluster.opendatahub.io/v2
kind: DataScienceCluster
metadata:
name: default-dsc
spec:
components:
aigateway:
managementState: Managed
modelsAsAService:
managementState: Removed
dashboard:
managementState: Managed
EOF
Deploy the MaaS controller (run from the project root; ensure the maas-db-config Secret exists per Database Setup):
On a fresh cluster, use ./scripts/deploy.sh (or run install_maas_controller_crds_and_wait from scripts/deployment-helpers.sh before applying) so CRDs are Established before Config and other CRs in the same bundle.
Troubleshooting
If components do not become ready, run kubectl describe datasciencecluster default-dsc to inspect conditions and events.
OdhDashboardConfig Feature Flags
The RHOAI Dashboard uses feature flags in the OdhDashboardConfig resource to control which tabs
and features are visible in the UI. The operator creates this resource automatically when the
Dashboard component is deployed. Enable genAiStudio manually if you need GenAI Studio.
Patch the OdhDashboardConfig in your applications namespace (typically redhat-ods-applications
for RHOAI or opendatahub for ODH):
kubectl patch odhdashboardconfig odh-dashboard-config \
-n redhat-ods-applications --type=merge \
-p '{"spec":{"dashboardConfig":{"genAiStudio":true}}}'
| Flag | Effect | Prerequisites |
|---|---|---|
genAiStudio: true |
Shows the GenAI Studio tab in the Dashboard | llamastackoperator set to Managed in DSC |
observabilityDashboard: true (default) |
Shows the Observability tab in the Dashboard | COO, OpenTelemetry Operator installed; DSCI monitoring.metrics configured |
When using the Managed deployment path (i.e. aigateway.modelsAsAService set to Managed in
the DSC), also enable the MaaS-specific dashboard flags:
kubectl patch odhdashboardconfig odh-dashboard-config \
-n redhat-ods-applications --type=merge \
-p '{"spec":{"dashboardConfig":{"modelAsService":true,"vLLMDeploymentOnMaaS":true}}}'
| Flag | Effect | Prerequisites |
|---|---|---|
modelAsService: true |
Enables the Models as a Service UI in the Dashboard | aigateway.modelsAsAService set to Managed in DSC |
vLLMDeploymentOnMaaS: true |
Enables vLLM model deployment via the MaaS Dashboard UI | modelAsService enabled |
Deprecated field: maasAuthPolicies
The field maasAuthPolicies is deprecated and frozen via a CEL transition rule in the
OdhDashboardConfig CRD. On new installs, setting it returns a validation error. Clusters
upgraded from RHOAI 3.4 that already had the field set will retain the existing value without
error, but the field is no longer used. The field will be removed in a future release.
Use modelAsService: true instead.
Namespace
For ODH installations, replace redhat-ods-applications with opendatahub (or your configured
applications namespace from DSCI spec.applicationsNamespace).
Flag names
These flags are defined in the odh-dashboard source. The OdhDashboardConfig CRD is of
API version opendatahub.io/v1alpha. The operator re-creates this resource with factory
defaults if it is deleted, but does not overwrite user changes to existing fields.
Creating Additional Tenants (Optional)
The default installation creates a single tenant in the models-as-a-service namespace. To create additional isolated tenants:
1. Create a Gateway
Each tenant requires its own Gateway. Follow the Gateway Patterns guide to create a gateway using the same pattern as maas-default-gateway.
Avoid hostname filters
Do not add hostname fields to Gateway listeners. Hostname-based routing can cause TLS/SNI issues. Use the default pattern with no hostname filter.
2. Create AITenant CR
Create an AITenant resource in the ai-tenants namespace:
apiVersion: maas.opendatahub.io/v1alpha1
kind: AITenant
metadata:
name: team-red
namespace: ai-tenants
spec:
gateway:
name: team-red-gateway # Must already exist in openshift-ingress
Apply the manifest:
The controller will automatically create:
- Tenant namespace: ai-tenant-team-red
- MaasTenantConfig CR: default-tenant in the tenant namespace
- maas-api deployment: maas-api-team-red
- HTTPRoute and AuthPolicy for the tenant
3. Verify
# Check AITenant status
kubectl get aitenant team-red -n ai-tenants -o yaml
# Check tenant namespace was created
kubectl get namespace ai-tenant-team-red
# Check MaasTenantConfig CR
kubectl get maastenantconfig default-tenant -n ai-tenant-team-red
For complete AITenant configuration options (OIDC, RBAC), see the AITenant CRD reference.
Delete a tenant
Delete the AITenant resource to start tenant cleanup:
Do not delete the default tenant
Do not delete AITenant/models-as-a-service in ai-tenants unless you are intentionally tearing down MaaS. The controller bootstraps the default AITenant only once per Config/default; after deletion it is not automatically recreated. MaaS UI and API workflows that depend on the default tenant may stop working until the default tenant state is restored.
Deletion invalidates all API keys and removes per-tenant maas-api resources, MaaS CRs (MaaSSubscription, MaaSAuthPolicy), and AITenant-owned RBAC. Lifecycle-invalidated keys are soft-deleted for the configured retention period before physical deletion. The tenant namespace is kept so non-MaaS user objects and workloads there survive; AITenant ownership metadata (labels and annotations) is cleared from the namespace. The AITenant can remain in Terminating phase while cleanup is in progress, or report Ready=False with reason DeletionBlocked if a cleanup step fails. The shared Gateway object and user model workloads outside the tenant namespace are also preserved.
Operand NetworkPolicies
MaaS operand NetworkPolicies follow ODH-ADR-Operator-0016. maas-controller reconciles operand policies (server-side apply) and recreates them if deleted. The operator bundle policy maas-controller-allow-monitoring is managed separately by the platform operator.
| Policy | Workload | Direction | Allowed peers |
|---|---|---|---|
maas-api-allow-gateway |
maas-api |
Ingress | Gateway pods in openshift-ingress → :8443 |
maas-authorino-allow |
maas-api |
Ingress | Authorino pods in Kuadrant/RHCL namespaces → :8443 |
maas-api-allow-monitoring |
maas-api |
Ingress | redhat-ods-monitoring → :9090 |
maas-api-egress-restrict |
maas-api |
Egress | OpenShift CoreDNS; Kubernetes API; allow-all by default (overridable or opt-out via maasconfig/default; see PostgreSQL egress) |
usage-logs-collector-egress-restrict |
usage-logs collector | Egress | CoreDNS; LokiStack gateway pods (app.kubernetes.io/name=lokistack, app.kubernetes.io/instance=usage, app.kubernetes.io/component=lokistack-gateway) → :8080 |
usage-tenancy-proxy-allow-perses |
tenancy proxy | Ingress | Perses pods and kubelet probes (host-network) → :8443 |
usage-tenancy-proxy-egress-restrict |
tenancy proxy | Egress | CoreDNS; Kubernetes API; LokiStack gateway pods (app.kubernetes.io/name=lokistack, app.kubernetes.io/component=lokistack-gateway, app.kubernetes.io/instance=data-science-lokistack; usage when usageLogging is true) → :8080 |
PostgreSQL egress
maas-api connects using maas-db-config only. PostgreSQL is customer-provided: bundled POC Postgres from
scripts/setup-database.sh, shared in-cluster instances (any namespace or labels), and external databases (for example
RDS) are all valid. MaaS does not require app=postgres on database pods and does not infer database topology
from maas-db-config.
Default egress (no maasconfig overrides)
maas-controller reconciles maas-api-egress-restrict with explicit egress per
ODH-ADR-Operator-0016:
- DNS to OpenShift CoreDNS (ports
53and5353) - Kubernetes API (TCP
443and6443, port-only rule — see below) - Allow-all remaining egress (
egress: - {})
This works for any database topology without extra configuration. A separate companion NetworkPolicy cannot
narrow egress while allow-all is present (NetworkPolicies are unioned).
Restricting egress (recommended for production)
For least-privilege egress, configure maasconfig/default (same pattern as the
MLflow operator). MaaS does not guess your database labels or
namespaces — you declare the peers explicitly.
Replace the default egress block with spec.networkPolicyEgressRules. You are responsible for including DNS,
Kubernetes API, and database destinations:
apiVersion: maas.opendatahub.io/v1alpha1
kind: Config
metadata:
name: default
spec:
networkPolicyEgressRules:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: openshift-dns
podSelector:
matchLabels:
dns.operator.openshift.io/daemonset-dns: default
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- protocol: UDP
port: 5353
- protocol: TCP
port: 5353
- ports:
- protocol: TCP
port: 443
- protocol: TCP
port: 6443
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: <postgres-namespace>
podSelector:
matchLabels:
app: <postgres-app-label>
ports:
- protocol: TCP
port: 5432
Replace <postgres-namespace> and <postgres-app-label> with your Postgres workload labels. For external databases
with stable CIDRs, use ipBlock instead of namespaceSelector/podSelector.
Append rules while keeping the default allow-all block with spec.networkPolicyAdditionalEgressRules (for example
a non-standard database port). This does not remove allow-all; use networkPolicyEgressRules when you need a
restrictive policy.
After changing maasconfig/default, maas-controller updates maas-api-egress-restrict on the next reconcile (or
delete the NetworkPolicy to force immediate recreation).
Customer-owned egress (opt-out)
When your platform team manages maas-api egress NetworkPolicies separately (for example namespace-level lock-down
or bespoke database peers), set spec.maasApiEgressNetworkPolicy: Disabled on maasconfig/default. MaaS stops
reconciling maas-api-egress-restrict and deletes any Config-owned instance on the next reconcile. You must not set
networkPolicyEgressRules or networkPolicyAdditionalEgressRules when opting out.
If no other egress NetworkPolicy selects the maas-api pods, Kubernetes allows all outbound
traffic from them.
apiVersion: maas.opendatahub.io/v1alpha1
kind: Config
metadata:
name: default
spec:
maasApiEgressNetworkPolicy: Disabled
Apply your own egress NetworkPolicy(ies) targeting the maas-api pods. Other MaaS operand policies (ingress,
usage logging, and so on) continue to be reconciled by maas-controller.
Kubernetes API egress (port-only rule)
maas-api-egress-restrict and usage-tenancy-proxy-egress-restrict allow egress to TCP ports 443 and 6443 without a to peer. This is a documented platform exception: in-cluster clients reach the API through the kubernetes.default Service ClusterIP, and on OVN-Kubernetes the policy may be evaluated against that VIP before apiserver endpoint DNAT. A namespaceSelector for openshift-kube-apiserver does not match that path and can block required API access (token projection, TokenReview). ODH-ADR-Operator-0016 does not define a shared API-server peer mapping; components document this limitation instead of adding a broad ingress or egress allow-all.
Next steps
- Deploy models. See Model Setup for sample model deployments.
- Perform validation. Follow the validation guide to verify that MaaS is working correctly.
- Enable observability. See Observability Setup.