Skip to content

Commit d86977c

Browse files
committed
Merge remote-tracking branch 'origin/main' into unpin-ea1
2 parents 7d90202 + d0dc95b commit d86977c

File tree

11 files changed

+294
-70
lines changed

11 files changed

+294
-70
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

deployment/base/observability/gateway-telemetry-policy.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ spec:
1212
user: auth.identity.userid
1313
# Subscription metadata for usage attribution and billing
1414
subscription: auth.identity.selected_subscription
15-
organization_id: auth.identity.organizationId
16-
cost_center: auth.identity.costCenter
15+
organization_id: auth.identity.subscription_info.organizationId
16+
cost_center: auth.identity.subscription_info.costCenter
1717
targetRef:
1818
group: gateway.networking.k8s.io
1919
kind: Gateway

docs/content/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ The MaaSAuthPolicy delegates to the MaaS API for key validation and subscription
214214
2. MaaS API validates the key (format, not revoked, not expired) and returns username, groups, and subscription.
215215
3. Authorino calls MaaS API to check subscription (groups, username, requested subscription from the key).
216216
4. If the user lacks access to the requested subscription → error (403).
217-
5. On success, returns selected subscription; Authorino caches the result (e.g., 60s TTL). AuthPolicy may inject `X-MaaS-Subscription` **server-side** for downstream rate limiting and metrics. Clients do not send this header on inference; subscription comes from the API key record created at mint time.
217+
5. On success, returns selected subscription; Authorino caches the result (e.g., 60s TTL). Identity information (username, groups, subscription, key ID) is made available to TokenRateLimitPolicy and observability through AuthPolicy's `filters.identity` mechanism, but is **not forwarded** as HTTP headers to upstream model workloads (defense-in-depth security). Clients do not send subscription headers on inference; subscription comes from the API key record created at mint time.
218218

219219
```mermaid
220220
graph TB

docs/content/configuration-and-management/maas-controller-overview.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,34 @@ The Kuadrant AuthPolicy validates API keys via the MaaS API and validates user t
231231

232232
---
233233

234+
## 9.1. Identity Headers and Defense-in-Depth
235+
236+
**For model inference routes** (HTTPRoutes targeting model workloads):
237+
238+
The controller-generated AuthPolicies do **not** inject most identity-related HTTP headers (`X-MaaS-Username`, `X-MaaS-Group`, `X-MaaS-Key-Id`) into requests forwarded to upstream model pods. This is a defense-in-depth security measure to prevent accidental disclosure of user identity, group membership, and key identifiers in:
239+
240+
- Model runtime logs
241+
- Upstream debug dumps
242+
- Misconfigured proxies or sidecars
243+
244+
**Exception:** `X-MaaS-Subscription` **is** injected for Istio Telemetry to enable per-subscription latency tracking. Istio runs in the Envoy gateway and cannot access Authorino's `auth.identity` context—it can only read request headers. The injected subscription value is server-controlled (resolved by Authorino from validated subscriptions), not client-provided.
245+
246+
All identity information remains available to **gateway-level features** through Authorino's `auth.identity` and `auth.metadata` contexts, which are consumed by:
247+
248+
- **TokenRateLimitPolicy (TRLP)**: Uses `selected_subscription_key`, `userid`, `groups`, and `subscription_info` from `filters.identity` (access `subscription_info.labels` for tier-based rate limiting)
249+
- **Gateway telemetry/metrics**: Accesses identity fields with `metrics: true` enabled on `filters.identity`
250+
- **Authorization policies**: OPA/Rego rules evaluate `auth.identity` and `auth.metadata` directly
251+
252+
**For maas-api routes**:
253+
254+
The static AuthPolicy for maas-api (`deployment/base/maas-api/policies/auth-policy.yaml`) still injects `X-MaaS-Username` and `X-MaaS-Group` headers, as maas-api's `ExtractUserInfo` middleware requires them. This is separate from model inference routes and follows a different security model (maas-api is a trusted internal service).
255+
256+
**Security motivation:**
257+
258+
Model workloads (vLLM, Llama.cpp, etc.) do not require strong identity claims in cleartext headers. By keeping identity at the gateway layer, we reduce the attack surface and limit the blast radius of potential log leaks or upstream vulnerabilities.
259+
260+
---
261+
234262
## 10. Summary
235263

236264
| Topic | Summary |

docs/content/configuration-and-management/tls-configuration.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,14 @@ This section covers how `maas-api` is configured to use TLS certificates. These
152152

153153
The `maas-api` component accepts TLS configuration via environment variables:
154154

155-
| Variable | Description | Example |
156-
|----------|-------------|---------|
157-
| `TLS_CERT` | Path to TLS certificate file | `/etc/maas-api/tls/tls.crt` |
158-
| `TLS_KEY` | Path to TLS private key file | `/etc/maas-api/tls/tls.key` |
159-
160-
When both variables are set, the API server listens on HTTPS (port 8443) instead of HTTP (port 8080).
155+
| Variable | Description | Default | Example |
156+
|----------|-------------|---------|---------|
157+
| `TLS_CERT` | Path to TLS certificate file | (none) | `/etc/maas-api/tls/tls.crt` |
158+
| `TLS_KEY` | Path to TLS private key file | (none) | `/etc/maas-api/tls/tls.key` |
159+
| `TLS_SELF_SIGNED` | Generate a self-signed certificate at startup | `false` | `true` |
160+
| `TLS_MIN_VERSION` | Minimum accepted TLS version (`1.2` or `1.3`) | `1.2` | `1.3` |
161+
162+
When `TLS_CERT` and `TLS_KEY` are both set, the API server listens on HTTPS (port 8443) instead of HTTP (port 8080). If `TLS_SELF_SIGNED` is set to `true`, a self-signed certificate is generated automatically and explicit cert/key paths are not required. When both cert/key files and `TLS_SELF_SIGNED` are provided, the cert/key files take precedence.
161163

162164
### Volume Mounts
163165

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: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -448,32 +448,22 @@ allow {
448448
// match against subscription groups (which may differ from auth policy groups).
449449
// Also inject subscription metadata from subscription-info for Limitador metrics.
450450
// For API keys: username/groups come from apiKeyValidation metadata
451-
// For K8s tokens: username/groups come from auth.identity
451+
// Identity headers intentionally removed for defense-in-depth:
452+
// User identity, groups, and key IDs are not forwarded to upstream model workloads
453+
// to prevent accidental disclosure in logs or dumps. All identity information remains
454+
// available to TRLP and telemetry via auth.identity and filters.identity below.
455+
// Exception: X-MaaS-Subscription is injected for Istio Telemetry (per-subscription latency tracking).
452456
rule["response"] = map[string]interface{}{
453457
"success": map[string]interface{}{
454458
"headers": map[string]interface{}{
455-
// Username from API key validation or K8s token identity
456-
"X-MaaS-Username": map[string]interface{}{
459+
// Strip Authorization header to prevent token exfiltration to model backends
460+
// Both API keys and OpenShift tokens are validated by Authorino, but should
461+
// not be forwarded to model services to prevent credential theft
462+
"Authorization": map[string]interface{}{
457463
"plain": map[string]interface{}{
458-
"expression": `(has(auth.metadata) && has(auth.metadata.apiKeyValidation)) ? auth.metadata.apiKeyValidation.username : auth.identity.user.username`,
459-
},
460-
"metrics": false,
461-
"priority": int64(0),
462-
},
463-
// Groups - serialize to JSON array string from API key validation or K8s identity
464-
// Using string() conversion of JSON-serialized groups for proper escaping
465-
"X-MaaS-Group": map[string]interface{}{
466-
"plain": map[string]interface{}{
467-
"expression": `string(((has(auth.metadata) && has(auth.metadata.apiKeyValidation)) ? auth.metadata.apiKeyValidation.groups : auth.identity.user.groups))`,
468-
},
469-
"metrics": false,
470-
"priority": int64(0),
471-
},
472-
// Key ID for tracking (only for API keys)
473-
"X-MaaS-Key-Id": map[string]interface{}{
474-
"plain": map[string]interface{}{
475-
"expression": `(has(auth.metadata) && has(auth.metadata.apiKeyValidation)) ? auth.metadata.apiKeyValidation.keyId : ""`,
464+
"value": "",
476465
},
466+
"key": "authorization",
477467
"metrics": false,
478468
"priority": int64(0),
479469
},
@@ -510,14 +500,11 @@ allow {
510500
ref.Namespace, ref.Name,
511501
),
512502
},
513-
"organizationId": map[string]interface{}{
514-
"expression": `has(auth.metadata["subscription-info"].organizationId) ? auth.metadata["subscription-info"].organizationId : ""`,
515-
},
516-
"costCenter": map[string]interface{}{
517-
"expression": `has(auth.metadata["subscription-info"].costCenter) ? auth.metadata["subscription-info"].costCenter : ""`,
518-
},
519-
"subscription_labels": map[string]interface{}{
520-
"expression": `has(auth.metadata["subscription-info"].labels) ? auth.metadata["subscription-info"].labels : {}`,
503+
// Full subscription-info object from subscription-select endpoint
504+
// Contains: name, namespace, labels, organizationId, costCenter, error, message
505+
// Consumers should access nested fields (e.g., subscription_info.organizationId)
506+
"subscription_info": map[string]interface{}{
507+
"expression": `has(auth.metadata["subscription-info"].name) ? auth.metadata["subscription-info"] : {}`,
521508
},
522509
// Error information (for debugging - only populated when selection fails)
523510
"subscription_error": map[string]interface{}{

0 commit comments

Comments
 (0)