Skip to content

Epic: Kuadrant AuthPolicy #100

Description

@araujof

Be compatible with the Kuadrant AuthPolicy API: any valid AuthPolicy should translate to Praxis configuration and be enforced with the same semantics.

Where we are

The PoC from praxis-proxy/praxis#716 delivered:

Covered today: JWT authentication, CEL authorization (patternMatching, when, patterns, and the legacy selector/operator/value form), and the RBAC membership idiom mapped onto CPEX role.*, perm.*, team.*.

Everything else is reported rather than silently dropped. This epic tracks closing those reports one by one.

Scope

  • Subset or every AuthPolicy authentication methods, credential location, and rule priority/fallback semantics (set decision and scoping).
  • Subset or every authorization backend (set decision and scoping).
  • metadata and callbacks.
  • response.success injection and denyWith on both authentication and authorization denials.
  • defaults/overrides and Gateway to route merge.
  • Gateway API targetRef binding (?).
  • A compatibility matrix and conformance suite that gates coverage claim.

Non-goals

  • Reverse translation (Praxis config to AuthPolicy).
  • Authorino as the enforcement engine. Praxis implements its own native Policy Engine.
  • gJSON expressions, deprecated upstream.

Sub-Tasks

Need a discussion on the following items to decide final scope of work.

  • apiKey authentication Epic: API-key identity support #108
  • x509 / mTLS authentication
  • oauth2Introspection authentication
  • kubernetesTokenReview authentication
  • anonymous authentication
  • Credential locations (credentials: header, query string, cookie)
  • Authentication rule priority and fallback
  • JWT signing algorithms and audiences
  • Scalar and nested identity claims in CEL
  • opa authorization
  • spicedb authorization
  • kubernetesSubjectAccessReview authorization
  • metadata (http, userInfo, uma) and auth.metadata.* in CEL
  • callbacks
  • denyWith custom denial responses
  • response.success header and dynamic metadata injection feat: assertions: to control http headers propagation #56
  • defaults / overrides and Gateway to route merge
  • Gateway API targetRef binding
  • Evaluator result caching (cache) feat(pdp): add an opt-in bounded decision cache #101
  • Global HTTP baseline on classified non-entity MCP methods
  • Compatibility matrix and conformance suite

Area

Security

Activity

  1. added theissue type on Aug 7, 2026
  2. moved this from Next to Epics in AI Gateway - Model Servingon Aug 7, 2026
  3. 25 remaining items

  4. shaneutt commented on Sep 14, 2026

    @shaneutt
    Member

    As a litmus test, here is a specific example of a relatively complete AuthPolicy which will need to be supported:

    apiVersion: kuadrant.io/v1
    kind: AuthPolicy
    metadata:
      name: maas-gateway-auth
      namespace: openshift-ingress
    spec:
      defaults:
        rules:
          authentication:
            api-keys:
              metrics: false
              plain:
                selector: request.headers.authorization
              priority: 0
              when:
                - operator: matches
                  selector: request.headers.authorization
                  value: ^Bearer sk-oai-.*
            api-keys-x-api-key:
              metrics: false
              plain:
                selector: request.headers.x-api-key
              priority: 1
              when:
                - predicate: '"x-api-key" in request.headers && request.headers["x-api-key"].matches("^sk-oai-.*") && !request.headers.authorization.matches("^Bearer sk-oai-.*")'
            openshift-identities:
              kubernetesTokenReview:
                audiences:
                  - 'https://kubernetes.default.svc'
              metrics: false
              priority: 2
              when:
                - predicate: '!(request.headers.authorization.matches("^Bearer sk-oai-.*") || ("x-api-key" in request.headers && request.headers["x-api-key"].matches("^sk-oai-.*")))'
          authorization:
            auth-valid:
              cache:
                key:
                  selector: '"api-key|" + (request.headers.authorization.matches("^Bearer sk-oai-.*") ? request.headers.authorization.replace("Bearer ", "") : request.headers["x-api-key"]) + "|" + (size(request.path.split("/").filter(x, x != "")) >= 2 && request.path.split("/").filter(x, x != "")[0] != "v1" && request.path.split("/").filter(x, x != "")[0] != "maas-api" ? request.path.split("/").filter(x, x != "")[0] + "/" + request.path.split("/").filter(x, x != "")[1] : ("x-gateway-model-name" in request.headers   ? request.headers["x-gateway-model-name"]   : ""))'
                ttl: 60
              metrics: false
              opa:
                allValues: false
                rego: |-
                  allow {
                    object.get(input.auth.metadata, "apiKeyValidation", {})
                    input.auth.metadata.apiKeyValidation.valid == true
                  }
                  allow {
                    not input.auth.metadata.apiKeyValidation
                  }
              priority: 0
            deny-api-key-mint:
              metrics: false
              patternMatching:
                patterns:
                  - predicate: '!(has(auth.metadata) && has(auth.metadata.apiKeyValidation))'
              priority: 0
              when:
                - predicate: request.method == "POST" && request.path.endsWith("/v1/api-keys")
            deny-client-identity-headers:
              metrics: false
              patternMatching:
                patterns:
                  - predicate: '!("x-maas-username" in request.headers)'
                  - predicate: '!("x-maas-group" in request.headers)'
              priority: 0
            require-group-membership:
              cache:
                key:
                  selector: '((has(auth.metadata) && has(auth.metadata.apiKeyValidation)) ? auth.metadata.apiKeyValidation.userId : (has(auth.identity.preferred_username) ? auth.identity.preferred_username : (has(auth.identity.sub) ? auth.identity.sub : auth.identity.user.username))) + "|" + ((has(auth.metadata) && has(auth.metadata.apiKeyValidation)) ? auth.metadata.apiKeyValidation.groups : (has(auth.identity.groups) ? auth.identity.groups : auth.identity.user.groups)).join(",") + "|" + ((has(auth.metadata) && has(auth.metadata.apiKeyValidation)) ? auth.metadata.apiKeyValidation.subscription : ("x-maas-subscription" in request.headers ? request.headers["x-maas-subscription"] : "")) + "|" + (size(request.path.split("/").filter(x, x != "")) >= 2 && request.path.split("/").filter(x, x != "")[0] != "v1" && request.path.split("/").filter(x, x != "")[0] != "maas-api" ? request.path.split("/").filter(x, x != "")[0] + "/" + request.path.split("/").filter(x, x != "")[1] : ("x-gateway-model-name" in request.headers   ? request.headers["x-gateway-model-name"]   : ""))'
                ttl: 60
              metrics: false
              opa:
                allValues: false
                rego: |-
                  allow {
                    object.get(input.auth.metadata["subscription-info"], "accessAllowed", false) == true
                  }
              priority: 0
              when:
                - predicate: '(size(request.path.split("/").filter(x, x != "")) >= 2 && request.path.split("/").filter(x, x != "")[0] != "v1" && request.path.split("/").filter(x, x != "")[0] != "maas-api" || "x-gateway-model-name" in request.headers)'
            subscription-valid:
              cache:
                key:
                  selector: '((has(auth.metadata) && has(auth.metadata.apiKeyValidation)) ? auth.metadata.apiKeyValidation.userId : (has(auth.identity.preferred_username) ? auth.identity.preferred_username : (has(auth.identity.sub) ? auth.identity.sub : auth.identity.user.username))) + "|" + ((has(auth.metadata) && has(auth.metadata.apiKeyValidation)) ? auth.metadata.apiKeyValidation.groups : (has(auth.identity.groups) ? auth.identity.groups : auth.identity.user.groups)).join(",") + "|" + ((has(auth.metadata) && has(auth.metadata.apiKeyValidation)) ? auth.metadata.apiKeyValidation.subscription : ("x-maas-subscription" in request.headers ? request.headers["x-maas-subscription"] : "")) + "|" + (size(request.path.split("/").filter(x, x != "")) >= 2 && request.path.split("/").filter(x, x != "")[0] != "v1" && request.path.split("/").filter(x, x != "")[0] != "maas-api" ? request.path.split("/").filter(x, x != "")[0] + "/" + request.path.split("/").filter(x, x != "")[1] : ("x-gateway-model-name" in request.headers   ? request.headers["x-gateway-model-name"]   : ""))'
                ttl: 60
              metrics: false
              opa:
                allValues: false
                rego: "allow {\n\tobject.get(input.auth.metadata[\"subscription-info\"], \"name\", \"\") != \"\"\n\tobject.get(input.auth.metadata[\"subscription-info\"], \"error\", \"\") == \"\"\n\tphase := object.get(input.auth.metadata[\"subscription-info\"], \"phase\", \"\")\n\tany([phase == \"Active\", phase == \"Degraded\"])\n\tobject.get(input.auth.metadata[\"subscription-info\"], \"deletionTimestamp\", \"\") == \"\"\n}"
              priority: 0
              when:
                - predicate: '(size(request.path.split("/").filter(x, x != "")) >= 2 && request.path.split("/").filter(x, x != "")[0] != "v1" && request.path.split("/").filter(x, x != "")[0] != "maas-api" || "x-gateway-model-name" in request.headers)'
          metadata:
            apiKeyValidation:
              cache:
                key:
                  selector: 'request.headers.authorization.matches("^Bearer sk-oai-.*") ? request.headers.authorization.replace("Bearer ", "") : request.headers["x-api-key"]'
                ttl: 60
              http:
                body:
                  expression: '{"key": request.headers.authorization.matches("^Bearer sk-oai-.*") ? request.headers.authorization.replace("Bearer ", "") : request.headers["x-api-key"]}'
                contentType: application/json
                method: POST
                url: 'https://maas-api.redhat-ai-gateway-infra.svc.cluster.local:8443/internal/v1/api-keys/validate'
              metrics: false
              priority: 0
              when:
                - predicate: 'request.headers.authorization.matches("^Bearer sk-oai-.*") || ("x-api-key" in request.headers && request.headers["x-api-key"].matches("^sk-oai-.*"))'
            subscription-info:
              cache:
                key:
                  selector: '((has(auth.metadata) && has(auth.metadata.apiKeyValidation)) ? auth.metadata.apiKeyValidation.userId : (has(auth.identity.preferred_username) ? auth.identity.preferred_username : (has(auth.identity.sub) ? auth.identity.sub : auth.identity.user.username))) + "|" + ((has(auth.metadata) && has(auth.metadata.apiKeyValidation)) ? auth.metadata.apiKeyValidation.groups : (has(auth.identity.groups) ? auth.identity.groups : auth.identity.user.groups)).join(",") + "|" + ((has(auth.metadata) && has(auth.metadata.apiKeyValidation)) ? auth.metadata.apiKeyValidation.subscription : ("x-maas-subscription" in request.headers ? request.headers["x-maas-subscription"] : "")) + "|" + (size(request.path.split("/").filter(x, x != "")) >= 2 && request.path.split("/").filter(x, x != "")[0] != "v1" && request.path.split("/").filter(x, x != "")[0] != "maas-api" ? request.path.split("/").filter(x, x != "")[0] + "/" + request.path.split("/").filter(x, x != "")[1] : ("x-gateway-model-name" in request.headers   ? request.headers["x-gateway-model-name"]   : ""))'
                ttl: 60
              http:
                body:
                  expression: |-
                    {
                      "groups": (has(auth.metadata) && has(auth.metadata.apiKeyValidation)) ? auth.metadata.apiKeyValidation.groups : (has(auth.identity.groups) ? auth.identity.groups : auth.identity.user.groups),
                      "username": (has(auth.metadata) && has(auth.metadata.apiKeyValidation)) ? auth.metadata.apiKeyValidation.username : (has(auth.identity.preferred_username) ? auth.identity.preferred_username : (has(auth.identity.sub) ? auth.identity.sub : auth.identity.user.username)),
                      "requestedSubscription": (has(auth.metadata) && has(auth.metadata.apiKeyValidation)) ? auth.metadata.apiKeyValidation.subscription : ("x-maas-subscription" in request.headers ? request.headers["x-maas-subscription"] : ""),
                      "requestedModel": (size(request.path.split("/").filter(x, x != "")) >= 2 && request.path.split("/").filter(x, x != "")[0] != "v1" && request.path.split("/").filter(x, x != "")[0] != "maas-api" ? request.path.split("/").filter(x, x != "")[0] + "/" + request.path.split("/").filter(x, x != "")[1] : ("x-gateway-model-name" in request.headers   ? request.headers["x-gateway-model-name"]   : ""))
                    }
                contentType: application/json
                method: POST
                url: 'https://maas-api.redhat-ai-gateway-infra.svc.cluster.local:8443/internal/v1/subscriptions/select'
              metrics: false
              priority: 1
              when:
                - predicate: '(size(request.path.split("/").filter(x, x != "")) >= 2 && request.path.split("/").filter(x, x != "")[0] != "v1" && request.path.split("/").filter(x, x != "")[0] != "maas-api" || "x-gateway-model-name" in request.headers)'
          response:
            success:
              filters:
                identity:
                  json:
                    properties:
                      selected_subscription:
                        expression: 'has(auth.metadata["subscription-info"].name) ? auth.metadata["subscription-info"].name : ""'
                      selected_subscription_key:
                        expression: '(has(auth.metadata["subscription-info"].namespace) && has(auth.metadata["subscription-info"].name)) ? auth.metadata["subscription-info"].namespace + "/" + auth.metadata["subscription-info"].name + "@" + (has(auth.metadata["subscription-info"].resolvedModel) && auth.metadata["subscription-info"].resolvedModel != "" ? auth.metadata["subscription-info"].resolvedModel : (size(request.path.split("/").filter(x, x != "")) >= 2 && request.path.split("/").filter(x, x != "")[0] != "v1" && request.path.split("/").filter(x, x != "")[0] != "maas-api" ? request.path.split("/").filter(x, x != "")[0] + "/" + request.path.split("/").filter(x, x != "")[1] : ("x-gateway-model-name" in request.headers   ? request.headers["x-gateway-model-name"]   : ""))) : ""'
                      subscription_error:
                        expression: 'has(auth.metadata["subscription-info"].error) ? auth.metadata["subscription-info"].error : ""'
                      groups_str:
                        expression: '((has(auth.metadata) && has(auth.metadata.apiKeyValidation)) ? auth.metadata.apiKeyValidation.groups : (has(auth.identity.groups) ? auth.identity.groups : auth.identity.user.groups)).join(",")'
                      userid:
                        expression: '(has(auth.metadata) && has(auth.metadata.apiKeyValidation)) ? auth.metadata.apiKeyValidation.username : (has(auth.identity.preferred_username) ? auth.identity.preferred_username : (has(auth.identity.sub) ? auth.identity.sub : auth.identity.user.username))'
                      keyName:
                        expression: '(has(auth.metadata) && has(auth.metadata.apiKeyValidation)) ? auth.metadata.apiKeyValidation.keyName : ""'
                      subscription_error_message:
                        expression: 'has(auth.metadata["subscription-info"].message) ? auth.metadata["subscription-info"].message : ""'
                      subscription_info:
                        expression: 'has(auth.metadata["subscription-info"].name) ? auth.metadata["subscription-info"] : {}'
                      groups:
                        expression: '(has(auth.metadata) && has(auth.metadata.apiKeyValidation)) ? auth.metadata.apiKeyValidation.groups : (has(auth.identity.groups) ? auth.identity.groups : auth.identity.user.groups)'
                      keyId:
                        expression: '(has(auth.metadata) && has(auth.metadata.apiKeyValidation)) ? auth.metadata.apiKeyValidation.keyId : ""'
                  metrics: true
                  priority: 0
              headers:
                X-MaaS-Group:
                  metrics: false
                  plain:
                    expression: 'size(auth.metadata.apiKeyValidation.groups) > 0 ? ''["'' + auth.metadata.apiKeyValidation.groups.join(''","'') + ''"]'' : ''[]'''
                  priority: 0
                  when:
                    - predicate: 'request.headers.authorization.matches("^Bearer sk-oai-.*") || ("x-api-key" in request.headers && request.headers["x-api-key"].matches("^sk-oai-.*"))'
                X-MaaS-Group-Token:
                  key: X-MaaS-Group
                  metrics: false
                  plain:
                    expression: 'has(auth.identity.groups) ? (size(auth.identity.groups) > 0 && auth.identity.groups.all(g, g.matches(''^[A-Za-z0-9:._/-]+$'')) ? ''["system:authenticated","'' + auth.identity.groups.join(''","'') + ''"]'' : ''["system:authenticated"]'') : (has(auth.identity.user.groups) && size(auth.identity.user.groups) > 0 ? ''["system:authenticated","'' + auth.identity.user.groups.join(''","'') + ''"]'' : ''["system:authenticated"]'')'
                  priority: 1
                  when:
                    - predicate: '!(request.headers.authorization.matches("^Bearer sk-oai-.*") || ("x-api-key" in request.headers && request.headers["x-api-key"].matches("^sk-oai-.*")))'
                X-MaaS-Subscription:
                  metrics: false
                  plain:
                    expression: '(has(auth.metadata) && has(auth.metadata.apiKeyValidation)) ? auth.metadata.apiKeyValidation.subscription : ("x-maas-subscription" in request.headers ? request.headers["x-maas-subscription"] : "")'
                  priority: 0
                  when:
                    - predicate: (has(auth.metadata) && has(auth.metadata.apiKeyValidation) && auth.metadata.apiKeyValidation.subscription != "") || "x-maas-subscription" in request.headers
                X-MaaS-Username:
                  metrics: false
                  plain:
                    selector: auth.metadata.apiKeyValidation.username
                  priority: 0
                  when:
                    - predicate: 'request.headers.authorization.matches("^Bearer sk-oai-.*") || ("x-api-key" in request.headers && request.headers["x-api-key"].matches("^sk-oai-.*"))'
                X-MaaS-Username-Token:
                  key: X-MaaS-Username
                  metrics: false
                  plain:
                    expression: 'has(auth.identity.preferred_username) ? auth.identity.preferred_username : (has(auth.identity.sub) ? auth.identity.sub : auth.identity.user.username)'
                  priority: 1
                  when:
                    - predicate: '!(request.headers.authorization.matches("^Bearer sk-oai-.*") || ("x-api-key" in request.headers && request.headers["x-api-key"].matches("^sk-oai-.*")))'
            unauthenticated:
              code: 401
              message:
                value: Authentication required
            unauthorized:
              body:
                expression: 'has(auth.metadata["subscription-info"].message) ? auth.metadata["subscription-info"].message : "Access denied"'
              code: 403
              headers:
                content-type:
                  value: text/plain
                x-ext-auth-reason:
                  expression: 'has(auth.metadata["subscription-info"].error) ? auth.metadata["subscription-info"].error : "unauthorized"'
        strategy: atomic
        when:
          - predicate: request.path != "/maas-api/health" || request.method != "GET"
      targetRef:
        group: gateway.networking.k8s.io
        kind: Gateway
        name: maas-default-gateway

    cc @jland-redhat

  5. modified the milestones: v0.5.0, 1.0.0 on Sep 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

Relationships

None yet

Development

No branches or pull requests

Issue actions