Skip to content

Commit 152e531

Browse files
jrhynessclaude
andauthored
chore: upgrade/validate Kuadrant 1.4.2 (opendatahub-io#643)
# Upgrade to Kuadrant 1.4.2 https://redhat.atlassian.net/browse/RHOAIENG-55749 https://redhat.atlassian.net/browse/RHOAIENG-54157 ## Summary Upgrades deployment to use Kuadrant 1.4.2 (from 1.3.1) to enable authorization header stripping for security and migrates AuthPolicy configurations to the new schema format required by Authorino v0.23.1. ## Changes ### 1. Kuadrant Upgrade - Update `scripts/deploy.sh` to install Kuadrant v1.4.2 catalog (was v1.3.1) - Brings Authorino v0.23.1 (was v0.22.0) - **Required for authorization header stripping capability** ### 2. Authorization Header Stripping for Internal Models **Security enhancement to prevent credential exfiltration** Added authorization header stripping in `maas-controller/pkg/controller/maas/maasauthpolicy_controller.go` for internal KServe models. **Problem:** User credentials (OpenShift tokens or API keys) were being forwarded to model backends, creating a credential exfiltration risk where malicious or compromised models could capture and misuse tokens. **Solution:** Authorino now strips the Authorization header before forwarding requests to internal model backends: ```go rule["response"] = map[string]any{ "success": map[string]any{ "headers": map[string]any{ // Strip Authorization header to prevent token exfiltration "Authorization": map[string]any{ "plain": map[string]any{ "value": "", }, "key": "authorization", "metrics": false, "priority": int64(0), }, // ... other headers (username, groups, subscription) Impact: - User tokens and API keys are validated by Authorino but NOT forwarded to model services - Applies to both inference requests and model discovery probing - Only affects internal KServe models (ExternalModel resources with credentialRef are unaffected) Version requirement verified: Testing confirmed that Kuadrant 1.4.1 / Authorino v0.24.0 does not properly strip headers (appends empty value but leaves original token present). Kuadrant 1.4.2 / Authorino v0.23.1 correctly replaces the authorization header. 3. AuthPolicy Schema Migration Migrate deployment/base/maas-api/policies/auth-policy.yaml from CEL predicate format to selector/operator format: Before (v1.3.1 format): when: - predicate: request.headers.authorization.startsWith("Bearer sk-oai-") After (v1.4.2 format): when: - selector: request.headers.authorization operator: matches value: "^Bearer sk-oai-.*" 4. Fallback Header Handling - Removed negative lookahead regex from fallback headers (X-MaaS-Username-OC, X-MaaS-Group-OC) - Authorino v0.23.1's matches operator doesn't support ^Bearer (?!sk-oai-).* pattern - Now rely on priority system instead: - API key headers: priority: 0 (higher priority, evaluated first) - Fallback headers: priority: 1 (lower priority, only used when API key when clause doesn't match) 5. Subscription Header Fix Fixed handling of multiple X-Maas-Subscription header values in maas-api/internal/handlers/models.go: Problem: When clients send x-maas-subscription header (even empty string), Authorino appends its injected value, resulting in: X-Maas-Subscription: ["", "simulator-subscription"] Gin's GetHeader() returns only the first value (empty string), causing 403 errors. Solution: Iterate header values in reverse order and take the last non-empty value: headerValues := c.Request.Header.Values("X-Maas-Subscription") for i := len(headerValues) - 1; i >= 0; i-- { trimmed := strings.TrimSpace(headerValues[i]) if trimmed != "" { requestedSubscription = trimmed break } } This ensures we use Authorino's validated subscription when available, while still supporting clients that don't send the header. 6. Documentation Updates Updated version requirements in README.md and docs/content/install/prerequisites.md: - MaaS v0.2.0+ requires Kuadrant 1.4.2+ (ODH) or RHCL 1.3+ (RHOAI) - Added detailed explanation of authorization header stripping security requirement Testing All E2E tests passing (74 passed): Fixed by subscription header change: - ✅ test_empty_subscription_header_value - correctly auto-selects subscription when empty header sent - ✅ test_api_key_ignores_subscription_header - correctly uses API key's bound subscription, ignoring client header Verified working: - ✅ API key authentication with subscription binding - ✅ OpenShift token authentication - ✅ Subscription-scoped model filtering - ✅ Rate limiting with TokenRateLimitPolicy - ✅ Namespace-scoped resource access - ✅ Cross-namespace model references Authorization header stripping verified: - ✅ Kuadrant 1.4.2 / Authorino v0.23.1: Header correctly stripped (backend receives empty string) - ❌ Kuadrant 1.4.1 / Authorino v0.24.0: Header NOT properly stripped (original token still present) Compatibility The new AuthPolicy schema format is backward compatible with Kuadrant 1.3.1. This was verified by: 1. Deploying main branch code (predicate format) with Kuadrant 1.4.2 ✅ 2. Deploying new schema format with Kuadrant 1.3.1 ✅ This allows safe deployment of auth-policy changes before operator upgrade. Migration Notes For clusters upgrading from Kuadrant 1.3.x to 1.4.x: 1. Apply updated AuthPolicy first (backward compatible) 2. Upgrade Kuadrant operator to 1.4.2 3. Monitor Authorino reconciliation - should be seamless 4. Validate auth flows - API key and OpenShift token authentication 5. Verify header stripping - User tokens should not reach model backends No service disruption expected during upgrade. Related Issues - [RHOAIENG-55749](https://redhat.atlassian.net/browse/RHOAIENG-55749): Validate Kuadrant/RHCL Compatibility and Upgrade - [RHOAIENG-54157](https://redhat.atlassian.net/browse/RHOAIENG-54157): Strip Authorization Header to Prevent Credential Exfiltration Merge criteria: - The commits are squashed in a cohesive manner and have meaningful messages. - Testing instructions have been added in the PR body (for PRs involving changes that are not immediately obvious). - The developer has manually tested the changes and verified that the changes work Summary by CodeRabbit - Security - Added authorization header stripping for internal models to prevent credential exfiltration to model backends - Bug Fixes - Improved API key validation logic for more robust authentication handling - Enhanced subscription header processing to correctly handle multiple header instances - Chores - Upgraded Kuadrant policy-engine dependency from v1.3.1 to v1.4.2 - Documentation - Updated version requirements to specify Kuadrant 1.4.2+ / RHCL 1.3+ for MaaS v0.2.0+ - Added security context explanation for authorization header stripping requirement <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Strengthened API-key checks using regex-based matching and adjusted header precedence so API-key-derived values override fallback headers. * Fixed subscription header handling to pick the last non-empty value when multiple headers are present and tightened non-empty checks. * Ensure upstream Authorization is stripped from responses to prevent credential forwarding. * **Documentation** * Updated prerequisites and migration notes; clarified minimum versions. * **Chores** * Bumped Kuadrant policy-engine reference to v1.4.2. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent d4a15d8 commit 152e531

File tree

6 files changed

+93
-32
lines changed

6 files changed

+93
-32
lines changed

README.md

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,44 @@ Our goal is to create a comprehensive platform for **Models as a Service** with
1616

1717
## 📋 Prerequisites
1818

19-
- **Openshift cluster** (4.19.9+) with kubectl/oc access
19+
- **OpenShift cluster** (4.19.9+) with kubectl/oc access
20+
- **Kuadrant v1.4.2+** (ODH) or **RHCL v1.3+** (RHOAI) - **Required for MaaS v0.2.0+**
2021
- **PostgreSQL database** (for production ODH/RHOAI deployments)
2122

22-
!!! warning "Database Required for Production"
23-
MaaS requires a PostgreSQL database for API key management. For production ODH/RHOAI deployments, you must create a Secret with the database connection URL **before** enabling modelsAsService.
23+
### ⚠️ Important Version Requirements
2424

25-
See [Database Prerequisites](docs/content/install/prerequisites.md#database-prerequisite) for details.
25+
#### Kuadrant 1.4.2+ Required (MaaS v0.2.0+)
2626

27-
Note: The `scripts/deploy.sh` script creates a development PostgreSQL instance automatically.
27+
**MaaS v0.2.0 and later requires Kuadrant 1.4.2+ (ODH) or RHCL 1.3+ (RHOAI).**
28+
29+
**Why Kuadrant 1.4.2+ is required:**
30+
31+
MaaS v0.2.0 requires the authorization header stripping capability added in Authorino v0.23.1 (shipped with Kuadrant 1.4.2) to protect user credentials from potential exfiltration to model backends.
32+
33+
**Security Context:**
34+
35+
When a user makes an inference request with their OpenShift token or API key, that credential must be validated by Authorino but should NOT be forwarded to model backends (whether internal KServe models or external providers). Kuadrant 1.4.2+ allows Authorino to:
36+
37+
1. Validate the incoming user credential (OpenShift token or MaaS API key)
38+
2. Strip/replace the Authorization header before forwarding to model backends
39+
3. Optionally inject model-specific credentials from Kubernetes Secrets (credentialRef) for ExternalModel resources
40+
41+
This prevents credential exfiltration where a malicious or compromised model service could capture and misuse user tokens.
42+
43+
**Migration Notes:**
44+
45+
- The deployment script (`scripts/deploy.sh`) automatically installs Kuadrant 1.4.2 for new deployments
46+
- For existing deployments, upgrade Kuadrant/RHCL before upgrading to MaaS v0.2.0+
47+
48+
For detailed version compatibility, see [Version Compatibility](docs/content/install/prerequisites.md#version-compatibility).
49+
50+
#### Database Required for Production
51+
52+
MaaS requires a PostgreSQL database for API key management. For production ODH/RHOAI deployments, you must create a Secret with the database connection URL **before** enabling modelsAsService.
53+
54+
See [Database Prerequisites](docs/content/install/prerequisites.md#database-prerequisite) for details.
55+
56+
Note: The `scripts/deploy.sh` script creates a development PostgreSQL instance automatically.
2857

2958
## 🚀 Quick Start
3059

deployment/base/maas-api/policies/auth-policy.yaml

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ spec:
1212
# API key authentication (for sk-oai-* tokens)
1313
api-keys:
1414
when:
15-
- predicate: request.headers.authorization.startsWith("Bearer sk-oai-")
15+
- selector: request.headers.authorization
16+
operator: matches
17+
value: "^Bearer sk-oai-.*"
1618
plain:
1719
selector: request.headers.authorization
1820
priority: 0
@@ -27,7 +29,9 @@ spec:
2729
# Validate API key via HTTP callback (only runs for API key auth)
2830
apiKeyValidation:
2931
when:
30-
- predicate: request.headers.authorization.startsWith("Bearer sk-oai-")
32+
- selector: request.headers.authorization
33+
operator: matches
34+
value: "^Bearer sk-oai-.*"
3135
http:
3236
# Placeholder URL - gets patched based on deployment mode:
3337
# - Operator mode (ODH/RHOAI): ODH overlay replacement (app-namespace param)
@@ -42,7 +46,9 @@ spec:
4246
# Check API key is valid (only for API key auth)
4347
api-key-valid:
4448
when:
45-
- predicate: request.headers.authorization.startsWith("Bearer sk-oai-")
49+
- selector: request.headers.authorization
50+
operator: matches
51+
value: "^Bearer sk-oai-.*"
4652
patternMatching:
4753
patterns:
4854
- selector: auth.metadata.apiKeyValidation.valid
@@ -55,29 +61,31 @@ spec:
5561
# Username: from API key validation (when API key used)
5662
X-MaaS-Username:
5763
when:
58-
- predicate: request.headers.authorization.startsWith("Bearer sk-oai-")
64+
- selector: request.headers.authorization
65+
operator: matches
66+
value: "^Bearer sk-oai-.*"
5967
plain:
6068
selector: auth.metadata.apiKeyValidation.username
6169
priority: 0
62-
# Username: from OpenShift identity (when OC token used)
70+
# Username: from OpenShift identity (fallback for non-API-key tokens)
71+
# No 'when' clause - always tries to inject, but priority 1 means API key wins
6372
X-MaaS-Username-OC:
64-
when:
65-
- predicate: '!request.headers.authorization.startsWith("Bearer sk-oai-")'
6673
plain:
6774
selector: auth.identity.user.username
6875
key: X-MaaS-Username
6976
priority: 1
7077
# Groups: from API key validation as JSON array (when API key used)
7178
X-MaaS-Group:
7279
when:
73-
- predicate: request.headers.authorization.startsWith("Bearer sk-oai-")
80+
- selector: request.headers.authorization
81+
operator: matches
82+
value: "^Bearer sk-oai-.*"
7483
plain:
7584
selector: auth.metadata.apiKeyValidation.groups.@tostr
7685
priority: 0
77-
# Groups: from OpenShift identity as JSON array (when OC token used)
86+
# Groups: from OpenShift identity as JSON array (fallback for non-API-key tokens)
87+
# No 'when' clause - always tries to inject, but priority 1 means API key wins
7888
X-MaaS-Group-OC:
79-
when:
80-
- predicate: '!request.headers.authorization.startsWith("Bearer sk-oai-")'
8189
plain:
8290
selector: auth.identity.user.groups.@tostr
8391
key: X-MaaS-Group
@@ -86,8 +94,12 @@ spec:
8694
# This header is used by /v1/models to determine which subscription's models to return
8795
X-MaaS-Subscription:
8896
when:
89-
- predicate: request.headers.authorization.startsWith("Bearer sk-oai-")
90-
- predicate: auth.metadata.apiKeyValidation.subscription != ""
97+
- selector: request.headers.authorization
98+
operator: matches
99+
value: "^Bearer sk-oai-.*"
100+
- selector: auth.metadata.apiKeyValidation.subscription
101+
operator: neq
102+
value: ""
91103
plain:
92104
selector: auth.metadata.apiKeyValidation.subscription
93105
priority: 0

docs/content/install/prerequisites.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Red Hat OpenShift AI (RHOAI). MaaS is installed by enabling it in the DataScienc
1212
|--------------|-----|-------------------------------|-------------|
1313
| v0.0.2 | 4.19.9+ | v1.3+ / v1.2+ | v1.2+ |
1414
| v0.1.0 | 4.19.9+ | v1.3+ / v1.2+ | v1.2+ |
15+
| v0.2.0+ | 4.19.9+ | v1.4.2+ / v1.3+ | v1.2+ |
1516

1617
!!! note "Other Kubernetes flavors"
1718
Other Kubernetes flavors (e.g., upstream Kubernetes, other distributions) are currently being validated.
@@ -34,14 +35,12 @@ MaaS requires Open Data Hub version 3.0 or later, with the Model Serving compone
3435
enabled (KServe) and properly configured for deploying models with `LLMInferenceService`
3536
resources.
3637

37-
A specific requirement for MaaS is to set up ODH's Model Serving with Kuadrant v1.3+, even
38-
though ODH can work with earlier Kuadrant versions.
38+
A specific requirement for MaaS v0.2.0+ is to set up ODH's Model Serving with Kuadrant v1.4.2 or later.
3939

4040
## Requirements for Red Hat OpenShift AI
4141

4242
MaaS requires Red Hat OpenShift AI (RHOAI) version 3.0 or later, with the Model Serving
4343
component enabled (KServe) and properly configured for deploying models with
4444
`LLMInferenceService` resources.
4545

46-
A specific requirement for MaaS is to set up RHOAI Model Serving with Red Hat Connectivity
47-
Link (RHCL) v1.2+, even though RHOAI can work with earlier RHCL versions.
46+
A specific requirement for MaaS v0.2.0+ is to set up RHOAI Model Serving with Red Hat Connectivity Link (RHCL) v1.3 or later.

maas-api/internal/handlers/models.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,17 @@ func (h *ModelsHandler) ListLLMs(c *gin.Context) {
187187
// Extract x-maas-subscription header.
188188
// For API keys: Authorino injects this from auth.metadata.apiKeyValidation.subscription
189189
// For user tokens: This header is not present (Authorino doesn't inject it)
190-
requestedSubscription := strings.TrimSpace(c.GetHeader("x-maas-subscription"))
190+
// Note: If client sends x-maas-subscription header, there may be multiple values.
191+
// Authorino appends its value, so we take the last non-empty value.
192+
requestedSubscription := ""
193+
headerValues := c.Request.Header.Values("X-Maas-Subscription")
194+
for i := len(headerValues) - 1; i >= 0; i-- {
195+
trimmed := strings.TrimSpace(headerValues[i])
196+
if trimmed != "" {
197+
requestedSubscription = trimmed
198+
break
199+
}
200+
}
191201
isAPIKeyRequest := strings.HasPrefix(authHeader, "Bearer sk-oai-")
192202

193203
// Fail closed: API keys without a bound subscription must be rejected

maas-controller/pkg/controller/maas/maasauthpolicy_controller.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,17 @@ allow {
452452
rule["response"] = map[string]interface{}{
453453
"success": map[string]interface{}{
454454
"headers": map[string]interface{}{
455+
// Strip Authorization header to prevent token exfiltration to model backends
456+
// Both API keys and OpenShift tokens are validated by Authorino, but should
457+
// not be forwarded to model services to prevent credential theft
458+
"Authorization": map[string]interface{}{
459+
"plain": map[string]interface{}{
460+
"value": "",
461+
},
462+
"key": "authorization",
463+
"metrics": false,
464+
"priority": int64(0),
465+
},
455466
// Username from API key validation or K8s token identity
456467
"X-MaaS-Username": map[string]interface{}{
457468
"plain": map[string]interface{}{

scripts/deploy.sh

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# OPTIONS:
1212
# --operator-type <odh|rhoai> Operator to install (default: odh)
1313
# Policy engine is auto-selected:
14-
# odh → kuadrant (community v1.3.1)
14+
# odh → kuadrant (community v1.4.2)
1515
# rhoai → rhcl (Red Hat Connectivity Link)
1616
# --enable-tls-backend Enable TLS for Authorino/MaaS API (default: on)
1717
# --enable-keycloak Deploy Keycloak for external OIDC (optional)
@@ -114,7 +114,7 @@ OPTIONS:
114114
Which operator to install (default: odh)
115115
Policy engine is auto-selected based on operator type:
116116
- rhoai → rhcl (Red Hat Connectivity Link)
117-
- odh → kuadrant (community v1.3.1 with AuthPolicy v1)
117+
- odh → kuadrant (community v1.4.2 with AuthPolicy v1)
118118
Only applies when --deployment-mode=operator
119119
120120
--enable-tls-backend
@@ -372,13 +372,13 @@ validate_configuration() {
372372
fi
373373

374374
# Auto-determine policy engine based on operator type
375-
# - ODH uses community Kuadrant (v1.3.1 from upstream catalog has AuthPolicy v1)
375+
# - ODH uses community Kuadrant (v1.4.2 from upstream catalog has AuthPolicy v1)
376376
# - RHOAI uses RHCL (Red Hat Connectivity Link - downstream)
377377
if [[ "$DEPLOYMENT_MODE" == "operator" ]]; then
378378
case "$OPERATOR_TYPE" in
379379
odh)
380380
POLICY_ENGINE="kuadrant"
381-
log_debug "Auto-selected policy engine for ODH: kuadrant (community v1.3.1)"
381+
log_debug "Auto-selected policy engine for ODH: kuadrant (community v1.4.2)"
382382
;;
383383
rhoai)
384384
POLICY_ENGINE="rhcl"
@@ -866,14 +866,14 @@ install_policy_engine() {
866866
;;
867867

868868
kuadrant)
869-
log_info "Installing Kuadrant v1.3.1 (upstream community)"
869+
log_info "Installing Kuadrant v1.4.2 (upstream community)"
870870

871-
# Create custom catalog for upstream Kuadrant v1.3.1
872-
# This version provides AuthPolicy v1 API required by ODH
871+
# Create custom catalog for upstream Kuadrant v1.4.2
872+
# This version provides AuthPolicy v1 API and Authorino v0.23.1
873873
local kuadrant_catalog="kuadrant-operator-catalog"
874874
local kuadrant_ns="kuadrant-system"
875875

876-
log_info "Creating Kuadrant v1.3.1 catalog source..."
876+
log_info "Creating Kuadrant v1.4.2 catalog source..."
877877
kubectl create namespace "$kuadrant_ns" 2>/dev/null || true
878878

879879
cat <<EOF | kubectl apply -f -
@@ -884,7 +884,7 @@ metadata:
884884
namespace: $kuadrant_ns
885885
spec:
886886
sourceType: grpc
887-
image: quay.io/kuadrant/kuadrant-operator-catalog:v1.3.1
887+
image: quay.io/kuadrant/kuadrant-operator-catalog:v1.4.2
888888
displayName: Kuadrant Operator Catalog
889889
publisher: Kuadrant
890890
updateStrategy:

0 commit comments

Comments
 (0)