Migration Guide: Tier-Based to Subscription Model
This guide helps operators migrate from the legacy tier-based architecture (ConfigMap + gateway-auth-policy) to the new subscription-driven architecture (MaaSModelRef + MaaSAuthPolicy + MaaSSubscription).
Overview
The MaaS platform has evolved from a tier-based system to a subscription model that provides:
- Per-model access control and rate limits (instead of gateway-level)
- CRD-based configuration (schema-validated, GitOps friendly)
- Declarative management via maas-controller
- Separation of concerns (auth vs. billing)
For architectural details, see old-vs-new-flow.md.
Prerequisites
Before starting the migration:
- MaaS platform with maas-controller installed
- Cluster permissions: namespace admin for
opendatahub,models-as-a-service, and model namespaces - Current configuration backup:
- ConfigMap
tier-to-group-mapping - Existing TokenRateLimitPolicy (gateway-level)
- List of LLMInferenceServices and their tier annotations
- Existing gateway-auth-policy (if present)
Pre-Migration Checklist
- Document current tier definitions and group mappings
- Document current rate limits per tier
- List all deployed models and their tier annotations
- Identify which groups have access to which models
- Test migration procedure in non-production environment
- Plan maintenance window (optional, see zero-downtime approach below)
- Back up current configuration
# Backup script
mkdir -p migration-backup
# Backup tier-to-group-mapping ConfigMap if it exists
if kubectl get configmap tier-to-group-mapping -n maas-api >/dev/null 2>&1; then
kubectl get configmap tier-to-group-mapping -n maas-api -o yaml > migration-backup/tier-to-group-mapping.yaml
echo "Backed up tier-to-group-mapping"
else
echo "No tier-to-group-mapping ConfigMap found (skipping backup)"
fi
# Only backup gateway-auth-policy if it exists
if kubectl get authpolicy gateway-auth-policy -n openshift-ingress >/dev/null 2>&1; then
kubectl get authpolicy gateway-auth-policy -n openshift-ingress -o yaml > migration-backup/gateway-auth-policy.yaml
echo "Backed up gateway-auth-policy"
else
echo "No gateway-auth-policy found (skipping backup)"
fi
# Backup tokenratelimitpolicy resources if they exist
if kubectl get tokenratelimitpolicy -n openshift-ingress >/dev/null 2>&1; then
kubectl get tokenratelimitpolicy -n openshift-ingress -o yaml > migration-backup/gateway-rate-limits.yaml
echo "Backed up tokenratelimitpolicy resources"
else
echo "No tokenratelimitpolicy resources found (skipping backup)"
fi
# Backup llminferenceservice resources if they exist
if kubectl get llminferenceservice -n llm >/dev/null 2>&1; then
kubectl get llminferenceservice -n llm -o yaml > migration-backup/llm-models.yaml
echo "Backed up llminferenceservice resources"
else
echo "No llminferenceservice resources found (skipping backup)"
fi
Migration Strategies
Option A: Zero-Downtime (Recommended)
Run both old and new systems in parallel, validate the new system, then switch over.
Advantages: - No service interruption - Safe rollback if issues arise - Time to validate new configuration
Approach: 1. Install maas-controller (creates gateway defaults) 2. Create new MaaS CRs alongside existing tier configuration 3. Validate new system works correctly 4. Remove old tier-based configuration
Option B: Full Cutover (Requires Downtime)
Replace old system with new system in one maintenance window.
Advantages: - Simpler process - Faster migration
Disadvantages: - Service downtime during migration - Less time for validation
Step-by-Step Migration (Zero-Downtime)
Phase 1: Install maas-controller
If maas-controller is not already installed:
# Deploy maas-controller
kubectl apply -k deployment/base/maas-controller/default
# Verify controller is running
kubectl get pods -n opendatahub -l app=maas-controller
# Check controller logs
kubectl logs -n opendatahub -l app=maas-controller --tail=20
# Verify gateway default policies were created
kubectl get authpolicy gateway-default-auth -n openshift-ingress
kubectl get tokenratelimitpolicy gateway-default-deny -n openshift-ingress
Important: The maas-controller creates gateway-level default policies (gateway-default-auth and gateway-default-deny) that deny unconfigured models. These work alongside your existing tier-based policies during migration.
Note: The maas-controller automatically creates the subscription namespace when it starts:
- Default behavior: Creates the models-as-a-service namespace
- Custom namespace: If you specify --maas-subscription-namespace custom-ns, only custom-ns is created (NOT both)
The controller creates only one subscription namespace - either the default models-as-a-service or your custom namespace.
Phase 2: Map Tiers to Subscriptions
For each tier in your ConfigMap, create equivalent MaaS CRs for each model.
Example: Migrating "premium" tier
OLD tier configuration (from tier-to-group-mapping.yaml):
OLD rate limit (from gateway TokenRateLimitPolicy):
spec:
limits:
premium-user-tokens:
rates:
- limit: 50000
window: 1m
when:
- predicate: auth.identity.tier == "premium"
counters:
- expression: auth.identity.userid
OLD model annotation (on LLMInferenceService):
NEW subscription configuration
For each model that premium tier can access, create:
1. MaaSModelRef (registers model with MaaS):
apiVersion: maas.opendatahub.io/v1alpha1
kind: MaaSModelRef
metadata:
name: my-model-name
namespace: llm # Must be in same namespace as the LLMInferenceService
spec:
modelRef:
kind: LLMInferenceService
name: my-model-name
Apply it:
2. MaaSAuthPolicy (access control - who can access):
apiVersion: maas.opendatahub.io/v1alpha1
kind: MaaSAuthPolicy
metadata:
name: my-model-premium-access
namespace: models-as-a-service
spec:
modelRefs:
- name: my-model-name
namespace: llm
subjects:
groups:
- name: premium-users
- name: premium-group
users: []
Apply it:
3. MaaSSubscription (rate limits - billing):
apiVersion: maas.opendatahub.io/v1alpha1
kind: MaaSSubscription
metadata:
name: my-model-premium-subscription
namespace: models-as-a-service
spec:
owner:
groups:
- name: premium-users
- name: premium-group
users: []
modelRefs:
- name: my-model-name
namespace: llm
tokenRateLimits:
- limit: 50000 # From old TokenRateLimitPolicy
window: 1m
Apply it:
Verify controller generated policies
The maas-controller should automatically create Kuadrant policies:
# Check MaaSModelRef status
kubectl get maasmodelref my-model-name -n llm -o jsonpath='{.status.phase}'
# Expected: Ready
# Check generated AuthPolicy (one per model)
kubectl get authpolicy -n llm -l maas.opendatahub.io/model=my-model-name
# Check generated TokenRateLimitPolicy (one per model)
kubectl get tokenratelimitpolicy -n llm -l maas.opendatahub.io/model=my-model-name
# View full status
kubectl describe maasmodelref my-model-name -n llm
Automation Script
To simplify migration, use the provided script:
# Generate MaaS CRs from existing tier configuration
./scripts/migrate-tier-to-subscription.sh \
--tier premium \
--models my-model-1,my-model-2,my-model-3 \
--groups premium-users \
--rate-limit 50000 \
--output migration-crs/
# Review generated CRs
ls migration-crs/
# Apply generated CRs
kubectl apply -f migration-crs/
Note: Resources generated by the migration script are automatically labeled with: -
migration.maas.opendatahub.io/generated=true- Identifies script-generated resources -migration.maas.opendatahub.io/from-tier=<tier>- Tracks which tier they came fromYou can use these labels to manage or rollback migration resources:
# List all script-generated resources kubectl get maasmodelref -n llm -l migration.maas.opendatahub.io/generated=true kubectl get maasauthpolicy,maassubscription -n models-as-a-service -l migration.maas.opendatahub.io/generated=true # Delete resources from a specific tier migration kubectl delete maasmodelref -n llm -l migration.maas.opendatahub.io/from-tier=premium kubectl delete maasauthpolicy,maassubscription -n models-as-a-service -l migration.maas.opendatahub.io/from-tier=premium
See Migration Script section below for details.
Phase 3: Validate New Configuration
Test each migrated model to ensure the new subscription model works correctly:
# 1. Check all MaaS CRs are Ready
kubectl get maasmodelref -n llm
kubectl get maasauthpolicy -n models-as-a-service
kubectl get maassubscription -n models-as-a-service
# 2. Check generated Kuadrant policies
kubectl get authpolicy -n llm
kubectl get tokenratelimitpolicy -n llm
# 3. Test inference as a user in the premium group
# ⚠️ SECURITY WARNING: Token Handling
# The examples below store bearer tokens in shell variables, which can leak via:
# - Shell history files (~/.bash_history, ~/.zsh_history)
# - Process listings (ps, /proc)
# - Environment variable dumps
#
# For production or sensitive environments, use one of these safer alternatives:
#
# Option A: Secure token file with restricted permissions
# mkdir -p ~/.kube/tokens
# chmod 700 ~/.kube/tokens
# oc whoami -t > ~/.kube/tokens/current
# chmod 600 ~/.kube/tokens/current
# # Use in curl: -H "Authorization: Bearer $(cat ~/.kube/tokens/current)"
# # Clean up after use: rm -f ~/.kube/tokens/current
#
# Option B: Disable shell history for this session
# set +o history # Disable history (bash/zsh)
# TOKEN=$(oc whoami -t)
# # ... run commands ...
# unset TOKEN # Clear token from environment
# set -o history # Re-enable history
#
# For demonstration purposes, the examples use TOKEN variables.
# Always clear sensitive tokens after use with: unset TOKEN
oc login --username=premium-user # Or use existing token
# Discover gateway host
HOST="maas.$(kubectl get ingresses.config.openshift.io cluster -o jsonpath='{.spec.domain}')"
# Safer approach: Use token file with restricted permissions
mkdir -p ~/.kube/tokens && chmod 700 ~/.kube/tokens
oc whoami -t > ~/.kube/tokens/current && chmod 600 ~/.kube/tokens/current
# Test model access
curl -H "Authorization: Bearer $(cat ~/.kube/tokens/current)" \
"https://${HOST}/llm/my-model-name/v1/chat/completions" \
-H "Content-Type: application/json" \
-d '{"model":"my-model-name","messages":[{"role":"user","content":"test"}],"max_tokens":10}'
# Expected: 200 OK with model response
# 4. Test rate limiting
for i in {1..60}; do
curl -s -o /dev/null -w "%{http_code}\n" \
-H "Authorization: Bearer $(cat ~/.kube/tokens/current)" \
"https://${HOST}/llm/my-model-name/v1/chat/completions" \
-H "Content-Type: application/json" \
-d '{"model":"my-model-name","messages":[{"role":"user","content":"test"}],"max_tokens":10}'
done | sort | uniq -c
# Expected: Mix of 200 and 429 responses based on rate limit
# 5. Test unauthorized user (should get 403)
oc login --username=unauthorized-user
oc whoami -t > ~/.kube/tokens/current && chmod 600 ~/.kube/tokens/current
curl -v -H "Authorization: Bearer $(cat ~/.kube/tokens/current)" \
"https://${HOST}/llm/my-model-name/v1/chat/completions" \
-H "Content-Type: application/json" \
-d '{"model":"my-model-name","messages":[{"role":"user","content":"test"}],"max_tokens":10}'
# Expected: 403 Forbidden
# Clean up token file after use
rm -f ~/.kube/tokens/current
# 6. Use validation script
./scripts/validate-deployment.sh
Phase 4: Remove Old Configuration
Once new system is validated and working correctly:
4.1 Remove tier annotations from models
# Remove tier annotations from all models
# Track failures to ensure all annotations are removed
failed_models=()
# Use process substitution to avoid subshell issue with pipe
while read model; do
if kubectl annotate $model -n llm alpha.maas.opendatahub.io/tiers- --ignore-not-found; then
echo "✓ Removed tier annotation from $model"
else
echo "✗ Failed to remove tier annotation from $model" >&2
failed_models+=("$model")
fi
done < <(kubectl get llminferenceservice -n llm -o name)
# Report any failures
if [ ${#failed_models[@]} -gt 0 ]; then
echo ""
echo "⚠️ WARNING: Failed to remove tier annotations from the following models:" >&2
printf ' - %s\n' "${failed_models[@]}" >&2
echo ""
echo "Please manually remove annotations from these models:" >&2
for model in "${failed_models[@]}"; do
echo " kubectl annotate $model -n llm alpha.maas.opendatahub.io/tiers-" >&2
done
exit 1
else
echo ""
echo "✓ Successfully removed tier annotations from all models"
fi
4.2 Delete old gateway-auth-policy (if exists)
# Check if gateway-auth-policy exists
kubectl get authpolicy gateway-auth-policy -n openshift-ingress
# Delete it (gateway-default-auth replaces it)
kubectl delete authpolicy gateway-auth-policy -n openshift-ingress --ignore-not-found
4.3 Update or remove gateway-level TokenRateLimitPolicy
The old TokenRateLimitPolicy has tier-based predicates that are no longer needed.
Option A: Remove tier-based limits
# Edit and remove tier-based limit rules
kubectl edit tokenratelimitpolicy <policy-name> -n openshift-ingress
# Remove sections like:
# premium-user-tokens:
# when:
# - predicate: auth.identity.tier == "premium"
Option B: Delete if fully replaced
# If gateway-default-deny provides sufficient default, delete old policy
kubectl delete tokenratelimitpolicy <old-policy-name> -n openshift-ingress
Note: gateway-default-deny (created by maas-controller) provides default rate limiting (0 tokens for unconfigured models).
4.4 Handle tier-to-group-mapping ConfigMap
Option A: Keep ConfigMap (if MaaS API uses it for other features)
# Keep ConfigMap but document that tiers are deprecated
kubectl annotate configmap tier-to-group-mapping -n maas-api \
deprecated="true" \
deprecated-reason="Migrated to subscription model" \
--overwrite
Option B: Delete ConfigMap (if no longer needed)
# Verify MaaS API doesn't use /v1/tiers/lookup endpoint
# Check maas-api logs for tier lookup calls
# Delete ConfigMap
kubectl delete configmap tier-to-group-mapping -n maas-api
Phase 5: ODH Model Controller Considerations
Context: If you have ODH Model Controller deployed (from github.com/opendatahub-io/odh-model-controller), it may manage AuthPolicies for LLMInferenceServices.
Check if ODH Model Controller is managing AuthPolicies
# Check for ODH Model Controller deployment
kubectl get deployment odh-model-controller -n opendatahub
# Check for ODH-managed AuthPolicies
kubectl get authpolicy -A -l app.kubernetes.io/managed-by=odh-model-controller
If ODH Model Controller manages AuthPolicies
Scenario 1: ODH creates AuthPolicies, maas-controller also creates AuthPolicies
- Potential conflict: Both controllers may try to manage policies
- Resolution: Use annotation to opt out ODH management for MaaS-managed models
# Opt out ODH management for specific AuthPolicy
kubectl annotate authpolicy <policy-name> -n <namespace> \
opendatahub.io/managed=false
Scenario 2: Coordinate with ODH team
- Contact ODH team to understand AuthPolicy management strategy
- Determine if ODH Model Controller's AuthPolicy creation should be disabled for MaaS models
- Consider updating ODH Model Controller configuration
Scenario 3: No ODH Model Controller or no AuthPolicy management
- No action needed
- maas-controller is sole owner of AuthPolicies
Verify no conflicts
# Check for duplicate AuthPolicies targeting same HTTPRoute
kubectl get authpolicy -A -o json | \
jq -r '.items[] | select(.spec.targetRef != null and .spec.targetRef.kind == "HTTPRoute") | "\(.metadata.namespace)/\(.metadata.name) -> \(.spec.targetRef.name // "<missing-target>")"' | \
sort
# Look for multiple policies targeting the same HTTPRoute
# Expected: One AuthPolicy per HTTPRoute (created by maas-controller)
# If you see "<missing-target>", investigate that AuthPolicy for missing targetRef.name
Migration Automation Script
A migration script is provided to automate CR generation from existing tier configuration.
Usage
Options
| Flag | Description | Example |
|---|---|---|
--tier <name> |
Tier name from ConfigMap | --tier premium |
--models <list> |
Comma-separated model names | --models model1,model2 |
--groups <list> |
Comma-separated group names (auto-detected if omitted) | --groups premium-users |
--rate-limit <limit> |
Token rate limit | --rate-limit 50000 |
--window <duration> |
Rate limit window (default: 1m) | --window 1m |
--output <dir> |
Output directory for CRs (default: migration-crs) | --output migration-crs/ |
--subscription-ns <ns> |
Subscription namespace (default: models-as-a-service) | --subscription-ns models-as-a-service |
--model-ns <ns> |
Model namespace (default: llm) | --model-ns llm |
--maas-ns <ns> |
MaaS namespace (default: opendatahub) | --maas-ns opendatahub |
--dry-run |
Generate files without applying | --dry-run |
--apply |
Apply generated CRs to cluster | --apply |
--verbose |
Enable verbose logging | --verbose |
--help |
Show help message | --help |
Examples
Example 1: Generate CRs for premium tier
./scripts/migrate-tier-to-subscription.sh \
--tier premium \
--models model-a,model-b,model-c \
--groups premium-users \
--rate-limit 50000 \
--window 1m \
--output migration-crs/premium/ \
--dry-run
Example 2: Generate and apply for all tiers
# Free tier
./scripts/migrate-tier-to-subscription.sh \
--tier free \
--models simulator,qwen3 \
--groups system:authenticated \
--rate-limit 100 \
--window 1m \
--output migration-crs/free/ \
--apply
# Premium tier
./scripts/migrate-tier-to-subscription.sh \
--tier premium \
--models simulator,qwen3,llama \
--groups premium-users \
--rate-limit 50000 \
--window 1m \
--output migration-crs/premium/ \
--apply
# Enterprise tier
./scripts/migrate-tier-to-subscription.sh \
--tier enterprise \
--models simulator,qwen3,llama,gpt \
--groups enterprise-users \
--rate-limit 100000 \
--window 1m \
--output migration-crs/enterprise/ \
--apply
Example 3: Extract tier info from ConfigMap and generate CRs
# Get tier configuration from ConfigMap
kubectl get configmap tier-to-group-mapping -n maas-api -o yaml
# Run script for each tier with extracted group and limit info
./scripts/migrate-tier-to-subscription.sh \
--tier premium \
--groups premium-users,premium-group \
--models $(kubectl get llminferenceservice -n llm -o json | \
jq -r '[.items[]
| . as $item
| try (
.metadata.annotations["alpha.maas.opendatahub.io/tiers"] | fromjson
) catch (
(env.DEBUG // "" | if . != "" then "WARN: malformed JSON in \($item.metadata.name)" | debug else empty end) | []
)
| if type == "array" and any(. == "premium") then $item.metadata.name else empty end
] | join(",")') \
--rate-limit 50000 \
--output migration-crs/premium/
Conversion Worksheet
Use this table to plan your migration:
| Old Tier | Groups | Models | Rate Limit (tokens/min) | New MaaSAuthPolicy Name | New MaaSSubscription Name |
|---|---|---|---|---|---|
| free | system:authenticated | simulator, qwen3 | 100 | free-models-access | free-models-subscription |
| premium | premium-users, premium-group | simulator, qwen3, llama | 50000 | premium-models-access | premium-models-subscription |
| enterprise | enterprise-users, admin-group | all models | 100000 | enterprise-models-access | enterprise-models-subscription |
Worksheet Template
Download and fill out this worksheet before migration:
# migration-plan.yaml
tiers:
- name: free
groups:
- system:authenticated
models:
- simulator
- qwen3
rateLimit:
limit: 100
window: 1m
- name: premium
groups:
- premium-users
- premium-group
models:
- simulator
- qwen3
- llama
rateLimit:
limit: 50000
window: 1m
- name: enterprise
groups:
- enterprise-users
- admin-group
models:
- simulator
- qwen3
- llama
- gpt
rateLimit:
limit: 100000
window: 1m
Rollback Plan
If migration fails or issues arise:
Immediate Rollback
# 1. List MaaS CRs created during migration (verify before deletion)
echo "=== MaaSModelRef resources ==="
kubectl get maasmodelref -n llm
echo "=== MaaSAuthPolicy resources ==="
kubectl get maasauthpolicy -n models-as-a-service
echo "=== MaaSSubscription resources ==="
kubectl get maassubscription -n models-as-a-service
# 2. Delete specific MaaS CRs created during migration
# Option A: Delete by resource name (if you know the specific names)
kubectl delete maasmodelref my-model-name -n llm
kubectl delete maasauthpolicy my-model-premium-access -n models-as-a-service
kubectl delete maassubscription my-model-premium-subscription -n models-as-a-service
# Option B: Delete all script-generated resources
kubectl delete maasmodelref -n llm -l migration.maas.opendatahub.io/generated=true
kubectl delete maasauthpolicy,maassubscription -n models-as-a-service -l migration.maas.opendatahub.io/generated=true
# Option C: Delete resources from a specific tier migration
kubectl delete maasmodelref -n llm -l migration.maas.opendatahub.io/from-tier=premium
kubectl delete maasauthpolicy,maassubscription -n models-as-a-service -l migration.maas.opendatahub.io/from-tier=premium
# 3. Re-apply old gateway-auth-policy (if it was backed up)
if [ -f migration-backup/gateway-auth-policy.yaml ]; then
kubectl apply -f migration-backup/gateway-auth-policy.yaml
echo "Restored gateway-auth-policy"
else
echo "No gateway-auth-policy backup found (skipping restore)"
fi
# 4. Re-apply old TokenRateLimitPolicy (if backed up)
if [ -f migration-backup/gateway-rate-limits.yaml ]; then
kubectl apply -f migration-backup/gateway-rate-limits.yaml
echo "Restored gateway-rate-limits"
else
echo "No gateway-rate-limits backup found (skipping restore)"
fi
# 5. Re-add tier annotations to models
kubectl annotate llminferenceservice my-model-name -n llm \
alpha.maas.opendatahub.io/tiers='["premium","enterprise"]' \
--overwrite
# 6. Re-apply tier-to-group-mapping ConfigMap (if backed up)
if [ -f migration-backup/tier-to-group-mapping.yaml ]; then
kubectl apply -f migration-backup/tier-to-group-mapping.yaml
echo "Restored tier-to-group-mapping"
else
echo "No tier-to-group-mapping backup found (skipping restore)"
fi
# 7. Restart MaaS API to reload tier configuration
kubectl rollout restart deployment/maas-api -n opendatahub
Rollback Validation
# Test tier-based system is working
# Using secure token file (see Phase 3 security warning for details)
mkdir -p ~/.kube/tokens && chmod 700 ~/.kube/tokens
oc whoami -t > ~/.kube/tokens/current && chmod 600 ~/.kube/tokens/current
HOST="maas.$(kubectl get ingresses.config.openshift.io cluster -o jsonpath='{.spec.domain}')"
curl -H "Authorization: Bearer $(cat ~/.kube/tokens/current)" \
"https://${HOST}/llm/my-model-name/v1/chat/completions" \
-H "Content-Type: application/json" \
-d '{"model":"my-model-name","messages":[{"role":"user","content":"test"}],"max_tokens":10}'
# Expected: 200 OK (tier-based system restored)
# Clean up token file
rm -f ~/.kube/tokens/current
Partial Rollback
If only some models have issues, you can rollback specific models:
# Delete MaaS CRs for specific model only
kubectl delete maasmodelref my-model-name -n llm
kubectl delete maasauthpolicy my-model-premium-access -n models-as-a-service
kubectl delete maassubscription my-model-premium-subscription -n models-as-a-service
# Re-add tier annotation to that model
kubectl annotate llminferenceservice my-model-name -n llm \
alpha.maas.opendatahub.io/tiers='["premium","enterprise"]' \
--overwrite
Troubleshooting
Models return 401 Unauthorized
Symptom: Models return 401 after migration
Possible Causes: - No MaaSAuthPolicy exists for the model - User not authenticated - gateway-default-auth denying request
Resolution:
# Check if MaaSAuthPolicy exists for the model
kubectl get maasauthpolicy -n models-as-a-service -o json | \
jq -r '.items[] | select(.spec.modelRefs[]? | .name? == "my-model-name")'
# Check if AuthPolicy was generated
kubectl get authpolicy -n llm -l maas.opendatahub.io/model=my-model-name
# Check AuthPolicy status
kubectl describe authpolicy -n llm <policy-name>
# Verify user is authenticated
oc whoami
Models return 403 Forbidden
Symptom: Models return 403 after migration
Possible Causes: - User's groups not in MaaSAuthPolicy subjects - AuthPolicy not enforced yet
Resolution:
# Check user's groups
oc whoami --show-groups
# Check MaaSAuthPolicy groups
kubectl get maasauthpolicy my-model-premium-access -n models-as-a-service -o yaml
# Verify groups match
kubectl get maasauthpolicy my-model-premium-access -n models-as-a-service -o jsonpath='{.spec.subjects.groups[*].name}'
# Check AuthPolicy enforcement
kubectl get authpolicy -n llm -o jsonpath='{.items[*].status.conditions[?(@.type=="Enforced")].status}'
# Check Authorino logs
kubectl logs -n openshift-ingress -l app.kubernetes.io/name=authorino --tail=50
Models return 429 Too Many Requests
Symptom: Models immediately return 429 even on first request
Possible Causes: - No MaaSSubscription exists for the model - User's groups not in MaaSSubscription owner groups - TokenRateLimitPolicy not configured correctly
Resolution:
# Check if MaaSSubscription exists for the model
kubectl get maassubscription -n models-as-a-service -o json | \
jq -r '.items[] | select(.spec.modelRefs[]? | .name? == "my-model-name")'
# Check if TokenRateLimitPolicy was generated
kubectl get tokenratelimitpolicy -n llm -l maas.opendatahub.io/model=my-model-name
# Check TokenRateLimitPolicy status
kubectl describe tokenratelimitpolicy -n llm <policy-name>
# Verify user's groups match subscription owner groups
oc whoami --show-groups
kubectl get maassubscription my-model-premium-subscription -n models-as-a-service -o jsonpath='{.spec.owner.groups[*].name}'
# Check Limitador logs
kubectl logs -n kuadrant-system -l app.kubernetes.io/name=limitador --tail=50
maas-controller not creating policies
Symptom: MaaSModelRef shows Ready but no AuthPolicy/TokenRateLimitPolicy created
Possible Causes: - maas-controller not watching correct namespace - Controller reconciliation failed - HTTPRoute not found
Resolution:
# Check maas-controller logs
kubectl logs -n opendatahub -l app=maas-controller --tail=100
# Check MaaSModelRef status
kubectl get maasmodelref my-model-name -n llm -o yaml
# Verify HTTPRoute exists
kubectl get httproute -n llm my-model-name
# Check subscription namespace matches controller config
kubectl get deployment maas-controller -n opendatahub -o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="MAAS_SUBSCRIPTION_NAMESPACE")].value}'
# Manually trigger reconciliation by updating MaaSAuthPolicy
kubectl annotate maasauthpolicy my-model-premium-access -n models-as-a-service \
reconcile-trigger="$(date +%s)" --overwrite
MaaSModelRef shows Pending or Failed
Symptom: MaaSModelRef status.phase is Pending or Failed
Possible Causes: - LLMInferenceService not ready - HTTPRoute not created by KServe yet - Model namespace mismatch
Resolution:
# Check MaaSModelRef status
kubectl describe maasmodelref my-model-name -n llm
# Check LLMInferenceService status
kubectl get llminferenceservice my-model-name -n llm -o yaml
# Check if HTTPRoute exists
kubectl get httproute -n llm my-model-name
# Wait for KServe to create HTTPRoute
kubectl wait --for=condition=Ready llminferenceservice/my-model-name -n llm --timeout=5m
# Check maas-controller logs for errors
kubectl logs -n opendatahub -l app=maas-controller | grep my-model-name
Duplicate AuthPolicies (ODH Model Controller conflict)
Symptom: Multiple AuthPolicies targeting the same HTTPRoute
Possible Causes: - Both ODH Model Controller and maas-controller creating AuthPolicies - Policy ownership conflict
Resolution:
# Check for multiple AuthPolicies on same route
kubectl get authpolicy -n llm -o json | \
jq -r '.items[] | select(.spec.targetRef.name=="my-model-name") | .metadata.name'
# Check managed-by labels
kubectl get authpolicy -n llm -o json | \
jq -r '.items[] | "\(.metadata.name): \(.metadata.labels."app.kubernetes.io/managed-by")"'
# Opt out ODH management
kubectl annotate authpolicy <odh-policy-name> -n llm \
opendatahub.io/managed=false
# Or delete ODH-managed policy (maas-controller will recreate)
kubectl delete authpolicy <odh-policy-name> -n llm
ConfigMap changes not reflected
Symptom: Updated tier-to-group-mapping not taking effect
Note: After migration, tier-to-group-mapping ConfigMap is no longer used by the subscription model.
Resolution: - Update MaaSAuthPolicy and MaaSSubscription CRs instead of ConfigMap - ConfigMap is only used if you haven't migrated yet
# Update MaaSAuthPolicy groups
kubectl edit maasauthpolicy my-model-premium-access -n models-as-a-service
# Update MaaSSubscription owner groups and limits
kubectl edit maassubscription my-model-premium-subscription -n models-as-a-service
Frequently Asked Questions
Do I need to use API keys with the new subscription model?
No. The new subscription model works with OpenShift tokens by default. The gateway-default-auth and per-route AuthPolicies use kubernetesTokenReview for authentication.
API key support is optional and requires additional MaaS API configuration. The migration guide assumes you continue using OpenShift token authentication.
Can I have different rate limits for the same model?
Yes. Create multiple MaaSSubscriptions for the same model with different owner groups and token limits.
Example:
# Basic tier: 100 tokens/min
---
apiVersion: maas.opendatahub.io/v1alpha1
kind: MaaSSubscription
metadata:
name: my-model-basic-subscription
namespace: models-as-a-service
spec:
owner:
groups:
- name: basic-users
modelRefs:
- name: my-model
namespace: llm
tokenRateLimits:
- limit: 100
window: 1m
# Premium tier: 10000 tokens/min
---
apiVersion: maas.opendatahub.io/v1alpha1
kind: MaaSSubscription
metadata:
name: my-model-premium-subscription
namespace: models-as-a-service
spec:
owner:
groups:
- name: premium-users
modelRefs:
- name: my-model
namespace: llm
tokenRateLimits:
- limit: 10000
window: 1m
When a user belongs to multiple owner groups, the controller selects the subscription with the highest token rate limit. In this example, users in both groups get the premium subscription with 10000 tokens/min (higher than the basic subscription's 100 tokens/min).
What happens to users during migration?
With zero-downtime approach: Users experience no interruption. The old tier-based system remains active until you validate and switch to the new system.
With full cutover: Users may experience brief interruption during the maintenance window.
Do I need to restart MaaS API?
No. MaaS API is unchanged. Only the tier lookup endpoint (/v1/tiers/lookup) becomes unused after migration.
If you delete the tier-to-group-mapping ConfigMap, MaaS API will no longer serve tier information, but this doesn't require a restart.
Can I migrate one model at a time?
Yes. You can migrate models incrementally:
- Create MaaS CRs for one model
- Test and validate
- Remove tier annotation from that model
- Repeat for next model
This allows gradual migration with minimal risk.
What if a user is in multiple groups with different subscriptions?
When a user belongs to multiple owner groups with different subscriptions for the same model, the controller selects the subscription with the highest token rate limit (the subscription with the highest limit value wins).
Example: A user in both basic-users and premium-users groups:
- If basic-subscription has 100 tokens/min and premium-subscription has 10000 tokens/min, the user gets the premium subscription with 10000 tokens/min (highest limit wins).
- If both subscriptions have the same token rate limit, the controller uses an implementation-defined tie-breaker (not guaranteed to be stable).
Note: The
spec.priorityfield exists in the MaaSSubscription CRD but is currently not used by the controller. Selection is based solely on token rate limit.
Can I still use the tier-to-group-mapping ConfigMap?
During migration: Yes, both systems can coexist.
After migration: The ConfigMap is no longer used by the subscription model. You can: - Delete it if not needed - Keep it if MaaS API uses it for other features (check API documentation) - Annotate it as deprecated
How do I know which models a tier has access to?
In the old system, check the alpha.maas.opendatahub.io/tiers annotation on each LLMInferenceService:
kubectl get llminferenceservice -n llm -o json | \
jq -r '.items[] | "\(.metadata.name): \(.metadata.annotations."alpha.maas.opendatahub.io/tiers")"'
In the new system, check MaaSAuthPolicy:
kubectl get maasauthpolicy -n models-as-a-service -o json | \
jq -r '.items[] | "\(.metadata.name): \(.spec.modelRefs[])"'
What happens if I don't create a MaaSSubscription for a model?
Users with access (via MaaSAuthPolicy) will get 429 Too Many Requests immediately because:
- The per-route AuthPolicy allows them (auth passes)
- No per-route TokenRateLimitPolicy exists for them
- gateway-default-deny kicks in with 0 token limit
This is the "dual-gate" model: both auth AND subscription must pass.
Can I use the subscription model without MaaSAuthPolicy?
No. Without MaaSAuthPolicy, no per-route AuthPolicy is created, so gateway-default-auth denies all requests (401/403).
You must create both MaaSAuthPolicy (for access) and MaaSSubscription (for rate limits).
How do I grant access to all authenticated users?
Use the system:authenticated group:
apiVersion: maas.opendatahub.io/v1alpha1
kind: MaaSAuthPolicy
metadata:
name: public-model-access
namespace: models-as-a-service
spec:
modelRefs:
- name: public-model
namespace: llm
subjects:
groups:
- name: system:authenticated
users: []
This is equivalent to the old tier system's free tier with system:authenticated group.
Additional Resources
Support
For issues or questions: 1. Check the troubleshooting section above 2. Review MaaS Controller logs 3. Consult the old-vs-new-flow.md for architectural details 4. Open an issue on GitHub with migration logs and error messages