Skip to content

feat: integrating OIDC Authentication with maas API AuthPolicy#598

Open
dmytro-zaharnytskyi wants to merge 2 commits intomainfrom
feature/external-oidc
Open

feat: integrating OIDC Authentication with maas API AuthPolicy#598
dmytro-zaharnytskyi wants to merge 2 commits intomainfrom
feature/external-oidc

Conversation

@dmytro-zaharnytskyi
Copy link
Collaborator

@dmytro-zaharnytskyi dmytro-zaharnytskyi commented Mar 24, 2026

JIRA - https://redhat.atlassian.net/browse/RHOAIENG-47977

Adding optional external OIDC authentication to the maas-api AuthPolicy so maas can accept Keycloak-issued JWTs in addition to the existing flow. + sample of Keycloak installation.

Not ready for review (awaits changes to how we modify AuthPolicy, coming change with a new CRD introduction)

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for external OIDC providers as an alternative authentication method.
    • Introduced Keycloak integration for OIDC testing and development.
    • Enabled API key minting from OIDC tokens for interoperability with legacy workflows.
  • Documentation

    • Updated architecture guide to reflect OIDC authentication flows.
    • Added comprehensive Keycloak setup and configuration guide.
    • Enhanced token management documentation with OIDC usage patterns and examples.
  • Tests

    • Added end-to-end tests for OIDC token validation and API key minting.

dmytro-zaharnytskyi and others added 2 commits March 20, 2026 14:46
Copy of this PR without keycloak -
#578

JIRA - 
https://redhat.atlassian.net/browse/RHOAIENG-47977

---------

Signed-off-by: Dmytro Zaharnytskyi <zdmytro@redhat.com>
Add Keycloak deployment scripts and configuration examples for external
OIDC support

  ## Description

This PR adds Keycloak deployment automation and configuration examples
to support external OIDC authentication for MaaS
([RHAISTRAT-1120](https://redhat.atlassian.net/browse/RHAISTRAT-1120)).

  ### Changes Included

  **Deployment Scripts:**
  - `scripts/setup-keycloak.sh` - Automated Keycloak deployment
    - Auto-installs Keycloak operator from community-operators catalog
    - Creates `keycloak-system` namespace
    - Auto-detects cluster domain for hostname configuration
    - Deploys POC-grade Keycloak instance (1 replica, HTTP enabled)
- Creates HTTPRoute for external access via existing MaaS Gateway API
- Displays commands to securely retrieve admin credentials (no passwords
in output)
    - Idempotent - safe to run multiple times

  - `scripts/cleanup-keycloak.sh` - Proper cleanup script
    - Deletes Keycloak instance (allows operator to clean up properly)
    - Deletes `keycloak-system` namespace
    - Optional CRD deletion with `--delete-crds` flag
    - Confirmation prompts (skip with `--force`)

  **Configuration Examples:**
  - `examples/keycloak/README.md` - Comprehensive configuration guide
    - Admin console access instructions
    - Step-by-step realm creation via web UI
    - OIDC client configuration for MaaS (client ID: `maas`)
    - Group mapper configuration for JWT tokens
    - Token verification examples
    - Troubleshooting guide

- `examples/keycloak/test-realms/` - Multi-tenant test realms for
development
- `tenant-a-realm.json` - Test realm with Engineering, Site-Reliability,
Project-Alpha groups
- `tenant-b-realm.json` - Test realm with Product-Security,
Project-Omega groups
    - `apply-test-realms.sh` - One-command deployment of test realms
- Clear warnings about test-only use (hardcoded passwords, wildcard
redirects)

  ### Design Decisions

1. **Opt-in deployment** - Keycloak is not required for basic MaaS,
suitable for `--enable-keycloak` flag in future deploy.sh integration
2. **No realms by default** - Admins configure realms post-deployment
via Admin Console (easiest method)
3. **Separate namespace** - Uses `keycloak-system` to isolate from MaaS
components
4. **POC-grade configuration** - Suitable for development/testing;
production deployments should use external database and TLS
5. **Test realms as examples** - Optional multi-tenant demo
configuration for quick testing

  ## How Has This Been Tested?

  ### Manual Testing

  1. **Deployed Keycloak**
     ```bash
     ./scripts/setup-keycloak.sh
  - Verified operator installation and CRD creation
  - Verified Keycloak pod startup and readiness
  - Verified HTTPRoute creation and hostname auto-detection
  - Retrieved admin credentials from secret

  2. Applied test realms
  ./examples/keycloak/test-realms/apply-test-realms.sh
    - Verified ConfigMap creation
    - Verified Keycloak restart and realm import
    - Confirmed realms visible in Admin Console
  3. Tested OIDC token generation

3. Following the instructions in
examples/keycloak/test-realms/README.md:

CLUSTER_DOMAIN=$(kubectl get ingresses.config.openshift.io cluster -o
jsonpath='{.spec.domain}')
  KEYCLOAK_HOST="keycloak.${CLUSTER_DOMAIN}"

  curl -k -X POST \

"https://${KEYCLOAK_HOST}/realms/tenant-a/protocol/openid-connect/token"
\
    -d "grant_type=password" \
    -d "client_id=test-client" \
    -d "username=alice_lead" \
    -d "password=letmein" | jq .

  3. Verified groups claim in decoded token:
  $ echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null | jq .
  {
    "exp": 1774014245,
    "iat": 1774013945,
    "jti": "onrtro:c72d50c7-9ccd-e06b-9bd1-cb1e5d51efb8",
"iss":
"https://keycloak.apps.rosa.j8bqr-55hr9-zr9.dv8i.p3.openshiftapps.com/realms/tenant-a",
    "sub": "b9ac9148-252a-4919-8830-b3d9f6af3d20",
    "typ": "Bearer",
    "azp": "test-client",
    "sid": "MztLpDc3-0cNcaCNekpuH10q",
    "acr": "1",
    "allowed-origins": [
      "*"
    ],
    "scope": "profile email",
    "email_verified": true,
    "groups": [
      "Engineering",
      "Project-Alpha"
    ],
    "preferred_username": "alice_lead"
  }

3. ✅ Groups claim correctly populated - Token contains ["Engineering",
"Project-Alpha"] for user alice_lead
  4. Tested cleanup
  ./scripts/cleanup-keycloak.sh
    - Verified Keycloak instance deletion
    - Verified namespace deletion
    - Verified proper cleanup order (no orphaned resources)

  Testing Environment

  - Platform: OpenShift (ROSA)
  - Cluster version: 4.x
  - Gateway: Existing MaaS Gateway API setup
  - Keycloak Operator: community-operators (fast channel)

  Merge criteria:

- [ ] The commits are squashed in a cohesive manner and have meaningful
messages.
- [x] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [x] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
  * Added Keycloak deployment for external OIDC authentication in MaaS.
  * Added `--enable-keycloak` flag to enable Keycloak during deployment.
* Included pre-configured test realms with sample users and groups for
development.

* **Documentation**
* Added comprehensive guides for Keycloak configuration, setup, and
verification.
  * Included troubleshooting steps and token validation instructions.

* **Chores**
  * Added cleanup utilities for removing Keycloak deployments.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
@openshift-ci openshift-ci bot requested review from ryancham715 and usize March 24, 2026 00:46
@openshift-ci
Copy link

openshift-ci bot commented Mar 24, 2026

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: dmytro-zaharnytskyi

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:
  • OWNERS [dmytro-zaharnytskyi]

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 24, 2026

📝 Walkthrough

Walkthrough

This PR integrates external OIDC identity provider support into the MaaS platform. It introduces Keycloak deployment automation via new setup/cleanup scripts, extends the deployment workflow to patch AuthPolicy resources with OIDC configuration, adds Kuadrant AuthPolicy specifications for JWT token validation, and provides comprehensive documentation and E2E test coverage for OIDC-to-API-key token minting flows.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes


Security-first findings

CWE-295: Insufficient Certificate Validation – OIDC Issuer URL

Location: scripts/deploy.sh (resolve_external_oidc_issuer() function) and scripts/data/maas-api-authpolicy-external-oidc-patch.yaml

The OIDC issuer URL is injected into the AuthPolicy without validation. The deployment script reads OIDC_ISSUER_URL from environment or params.env but never validates that:

  • The URL is reachable
  • The issuer's OIDC discovery endpoint (/.well-known/openid-configuration) returns valid metadata
  • The issuer certificate chain is valid (relevant for non-production deployments with self-signed certs)

The maas-api-authpolicy-external-oidc-patch.yaml uses a placeholder __OIDC_ISSUER_URL__ that gets substituted, but there's no pre-flight validation. Kuadrant will attempt JWT validation against this URL at runtime, which could silently fail or accept invalid tokens if the issuer is misconfigured.

Recommendation: Add resolve_external_oidc_issuer() validation: perform an HTTP GET to ${OIDC_ISSUER_URL}/.well-known/openid-configuration, verify 200 status, and parse the response to confirm it contains required fields (issuer, jwks_uri). Fail deployment if validation fails.


CWE-798: Use of Hard-Coded Credentials

Location: docs/samples/install/keycloak/test-realms/tenant-a-realm.json and tenant-b-realm.json

Test realm users are seeded with hardcoded password letmein. While these are explicitly documented as test fixtures:

  • The password appears in version control
  • Test realm deployment instructions don't emphasize that test realms must never be used in production
  • No automated safeguard prevents accidental production deployment

Recommendation: Add a deployment-time assertion in docs/samples/install/keycloak/test-realms/apply-test-realms.sh that exits if KEYCLOAK_NAMESPACE is not explicitly set to keycloak-system or if any production-like environment variable is detected. Document a breaking warning in docs/samples/install/keycloak/README.md that test realms are for development only.


CWE-200: Exposure of Sensitive Information – JWT Token Handling

Location: test/e2e/tests/test_external_oidc.py (logging)

The test module lacks credential redaction. If OIDC credentials (OIDC_USERNAME, OIDC_PASSWORD) are printed during test execution, they appear in test logs and CI artifacts (acknowledged by the ARTIFACT_DIR collection in the test README).

Recommendation: Wrap any test output that references credentials with a sanitization function that redacts tokens and passwords before printing. Example:

def _redact_token(token_str):
    if len(token_str) > 20:
        return token_str[:10] + "...[redacted]"
    return "[redacted]"

CWE-862: Missing Authorization Check – AuthPolicy Patch Application

Location: scripts/deploy.sh (configure_maas_api_authpolicy() function)

The script patches the live maas-api-auth-policy AuthPolicy after deployment, annotating it with opendatahub.io/managed=false to prevent operator reconciliation. However:

  • The script doesn't verify that the user running deploy.sh has RBAC permissions to patch AuthPolicy resources (Kuadrant API group)
  • Failure to patch is not fatal; the script continues even if the patch fails, leaving the system in an inconsistent state (old policy applies)
  • No validation confirms the patch was actually applied

Recommendation: Add explicit RBAC checks before patching:

kubectl auth can-i patch authpolicies.kuadrant.io -n "${MAAS_NAMESPACE}" || \
  die "RBAC: cannot patch AuthPolicy in ${MAAS_NAMESPACE}"

Make patch failure fatal by using set -o pipefail and explicit error handling.


CWE-434: Unrestricted Upload of File with Dangerous Type – Keycloak Realm Import

Location: scripts/setup-keycloak.sh and docs/samples/install/keycloak/test-realms/apply-test-realms.sh

Both scripts deploy ConfigMaps containing Keycloak realm JSON. The realm files are read from the repository directly without validation:

  • No JSON schema validation before mounting as Keycloak import data
  • Malformed realm JSON could cause operator reconciliation loops or operator crashes
  • No integrity checks (checksums, signatures) on realm files

Recommendation: Add JSON validation using jq:

jq empty "${realm_file}" || die "Invalid JSON in ${realm_file}"

Validate critical fields exist: realm, clients, groups, users.


CWE-377: Insecure Temporary File

Location: scripts/setup-keycloak.sh and scripts/deploy.sh

Scripts generate temporary files (e.g., patched YAML) without explicit temporary directories or cleanup guarantees. No evidence of trap cleanup handlers for temp files on early exit.

Recommendation: Add trap handlers:

temp_dir=$(mktemp -d) || die "Cannot create temp dir"
trap "rm -rf ${temp_dir}" EXIT

CWE-400: Uncontrolled Resource Consumption – Keycloak Polling

Location: scripts/setup-keycloak.sh (pod readiness wait loop)

The script polls for Keycloak pod readiness with a 60-second timeout and 1-second sleep intervals (60 iterations). If the cluster is under load or autoscaling, the timeout silently completes with a warning but continues deployment. No exponential backoff; fixed 1-second polling could hammer the API server on large clusters.

Recommendation: Implement exponential backoff and increase timeout. Use kubectl wait instead of polling:

kubectl wait --for=condition=Ready pod \
  -l app.kubernetes.io/name=keycloak \
  -n keycloak-system \
  --timeout=5m 2>/dev/null || warn "Keycloak pods not ready; continuing anyway"

CWE-347: Improper Verification of Cryptographic Signature – OIDC Token Validation

Location: scripts/data/maas-api-authpolicy-external-oidc-patch.yaml

The AuthPolicy references issuerUrl but does not explicitly configure:

  • JWKS URI (should be auto-discovered from issuer's .well-known/openid-configuration)
  • Audience (aud) claim validation
  • Token expiration (exp) claim validation (should be enforced by default, but not documented)

If the OIDC provider is compromised or returns misconfigured JWKS, Kuadrant could accept invalid tokens. The policy does not require standard claims beyond what's implicitly validated.

Recommendation: Explicitly document in scripts/data/maas-api-authpolicy-external-oidc-patch.yaml that:

  1. Kuadrant will validate exp, iat, and signature via JWKS auto-discovery
  2. Add explicit audience field if the OIDC client is configured with a specific audience
  3. Document the required OIDC client configuration (e.g., redirect URIs, grant types) in deployment docs

CWE-269: Improper Access Control – Namespace Deletion Without Verification

Location: scripts/cleanup-keycloak.sh

The cleanup script deletes the keycloak-system namespace asynchronously (--wait=false) and polls for deletion. However:

  • No verification that only Keycloak resources exist in this namespace
  • If a user accidentally places critical workloads in keycloak-system, they're silently deleted
  • The confirmation prompt only lists the Keycloak instance name, not all namespace contents

Recommendation: Before namespace deletion, list all resources:

echo "Resources to be deleted:"
kubectl get all,configmap,secret -n keycloak-system
read -p "Confirm deletion? [y/N]: " confirm

Additional actionable issues

Missing error context in E2E tests

Location: test/e2e/tests/test_external_oidc.py

Test assertions lack descriptive error messages. Example:

assert response.status_code == 200

Should be:

assert response.status_code == 200, f"OIDC token endpoint returned {response.status_code}: {response.text}"

Inconsistent environment variable naming

Location: scripts/deploy.sh, test/e2e/scripts/prow_run_smoke_test.sh, deployment/overlays/odh/params.env

OIDC configuration uses three different name formats:

  • OIDC_ISSUER_URL (env var, hyphenated in source)
  • oidc-issuer-url (params.env key, hyphenated)
  • oidc_issuer_url (potential inconsistency in scripts)

This inconsistency could cause environment lookups to silently fall back to defaults. Standardize on OIDC_* (uppercase underscore) for environment variables and oidc-* (lowercase hyphenated) for Kubernetes ConfigMap keys.


Missing cleanup for failed deployments

Location: scripts/setup-keycloak.sh

If the script exits early (e.g., operator not ready), the partially-created keycloak-system namespace, OperatorGroup, and Subscription remain. No trap handler cleans up on failure.

Recommendation: Add:

trap 'warn "Setup failed. Run ./scripts/cleanup-keycloak.sh to remove partial deployment."' EXIT
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically identifies the main change: adding OIDC authentication integration to the maas API AuthPolicy. It directly corresponds to the primary objective and is concise without unnecessary noise.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

🧹 Nitpick comments (2)
scripts/cleanup-keycloak.sh (2)

95-98: Quote $REPLY to prevent word splitting (CWE-78).

-  if [[ ! $REPLY =~ ^[Yy]$ ]]; then
+  if [[ ! "$REPLY" =~ ^[Yy]$ ]]; then

As per coding guidelines: "Quote all variables to prevent injection".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/cleanup-keycloak.sh` around lines 95 - 98, The conditional uses an
unquoted shell variable REPLY which can cause word-splitting or injection;
update the test in the interactive prompt handling so the REPLY variable is
quoted inside the [[ ... ]] pattern match (i.e., reference "$REPLY" in the if
condition that currently reads if [[ ! $REPLY =~ ^[Yy]$ ]]; then) so the script
safely evaluates the user input.

112-119: Keycloak CR deletion lacks timeout handling.

If kubectl delete keycloak hangs (e.g., webhook issues), the script blocks indefinitely. The subsequent sleep 5 is also arbitrary and may be insufficient for operator reconciliation.

Proposed fix
 if kubectl get keycloak "$KEYCLOAK_NAME" -n "$NAMESPACE" &>/dev/null; then
-  kubectl delete keycloak "$KEYCLOAK_NAME" -n "$NAMESPACE"
+  kubectl delete keycloak "$KEYCLOAK_NAME" -n "$NAMESPACE" --timeout=60s
   echo "  Waiting for operator to clean up resources..."
-  sleep 5
+  kubectl wait --for=delete keycloak/"$KEYCLOAK_NAME" -n "$NAMESPACE" --timeout=30s 2>/dev/null || sleep 5
   echo "✓ Keycloak instance deleted"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/cleanup-keycloak.sh` around lines 112 - 119, The Keycloak deletion
block can hang and currently uses an arbitrary sleep; replace the simple
delete+sleep pattern with a deletion that enforces a maximum wait: call kubectl
delete keycloak "$KEYCLOAK_NAME" -n "$NAMESPACE" and then use a bounded wait
(e.g. kubectl wait --for=delete keycloak/"$KEYCLOAK_NAME" -n "$NAMESPACE"
--timeout=<reasonable-duration>) or implement a loop that polls kubectl get
keycloak "$KEYCLOAK_NAME" -n "$NAMESPACE" with a counter and sleep interval,
aborting after a max retry count and emitting an error/exit code if the CR is
not removed; remove the arbitrary sleep 5 and ensure you surface timeout/failure
using the script exit status.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/hack/cleanup-odh.sh:
- Around line 129-136: The assignment to SCRIPT_DIR can result in an empty
string if the subshell cd operations fail, which makes the subsequent [[ -f
"${SCRIPT_DIR}/scripts/cleanup-keycloak.sh" ]] test unreliable; update the
SCRIPT_DIR computation and guard to ensure it succeeded: compute SCRIPT_DIR in a
way that fails fast (e.g., use a subshell that exits non-zero on cd failure or
test each cd), then check that SCRIPT_DIR is non-empty and points to an existing
directory before running the -f test and invoking scripts/cleanup-keycloak.sh;
reference the SCRIPT_DIR variable, the conditional test [[ -f
"${SCRIPT_DIR}/scripts/cleanup-keycloak.sh" ]], and the INCLUDE_CRDS branch when
adding the validation and early exit or error handling.

In `@deployment/overlays/odh/maas-api-auth-policy-oidc-patch.yaml`:
- Around line 10-15: The oidc-identities block currently only sets jwt.issuerUrl
(issuerUrl) so add an authorization rule under spec.rules.authorization that
enforces audience/authorized-party checks: create a rule (e.g.,
jwt-audience-validation) using patternMatching with selector auth.identity.aud
operator eq value "maas-api" and also add an alternative check for
auth.identity.azp when present; apply the same change pattern to the
corresponding scripts/data/maas-api-authpolicy-external-oidc-patch.yaml to keep
external patch in sync.

In `@docs/samples/install/keycloak/README.md`:
- Around line 164-179: Remove the insecure curl option by deleting "-k" from the
token request (the curl command that posts to
"https://${KEYCLOAK_HOST}/realms/${REALM_NAME}/protocol/openid-connect/token")
and rely on system CA validation or supply a CA bundle (e.g., --cacert) instead;
for JWT decoding, change the payload decode pipeline that uses TOKEN and "echo
$TOKEN | cut -d'.' -f2 | base64 -d" to perform base64url decoding with URL-safe
character translation and proper padding (replace '-'/'_' to '+'/'/' and add '='
padding to a length multiple of 4) before calling base64 --decode (or an
equivalent base64url decode) so the JWT payload is decoded correctly.
- Around line 74-75: Replace the wildcard redirect URI and permissive Web
origins in the README instructions: change the "Valid redirect URIs:
`https://*.{your-cluster-domain}/*`" guidance to require explicit, exact
callback URLs (e.g., `https://app.example.com/oauth/callback`) and update "Web
origins: `+`" to list explicit origins (e.g., `https://app.example.com`) only;
ensure the text references that Keycloak/OpenID Connect requires exact string
matching of redirect_uri and that CORS must be restricted to the named origins
rather than using `+` or wildcards.

In `@docs/samples/install/keycloak/test-realms/apply-test-realms.sh`:
- Around line 68-109: The current kubectl patch uses JSON merge semantics which
will overwrite arrays under spec.unsupported.podTemplate (containers, volumes);
instead either refuse to run if spec.unsupported.podTemplate is already
populated or perform a safe merge: fetch the existing Keycloak CR (use
EXISTING_ARGS/kubectl get keycloak "$KEYCLOAK_NAME" -n "$NAMESPACE" -o json),
inspect spec.unsupported.podTemplate, and if it exists exit with a clear
message; otherwise build a merged object that appends/merges the new container
entry (name "keycloak", args including "--import-realm" but avoid hardcoding the
full args list) and the test-realms volume into the existing arrays, then apply
the merged CR via kubectl apply -f - (or use kubectl patch --type=json with
explicit add operations targeting specific array positions) to avoid clobbering
containers or volumes; ensure references to CONFIGMAP_NAME, KEYCLOAK_NAME,
spec.unsupported.podTemplate, containers, and volumes are used to locate and
merge the correct fields.

In `@docs/samples/install/keycloak/test-realms/README.md`:
- Around line 173-189: Update the README to prevent leaving test realms and
credentials active: either (A) add explicit commands to delete the persisted
realms (e.g., show how to run kcadm.sh inside the Keycloak pod or call the
Keycloak REST API to delete realms "tenant-a" and "tenant-b" and remove users
"alice_lead", "bob_sre", "charlie_sec_lead", "grace_dev"), or (B) state that
./scripts/cleanup-keycloak.sh is the only supported cleanup and remove the
partial ConfigMap+restart instructions; if keeping the partial approach, add a
clear warning that deleting the ConfigMap (keycloak-test-realms) and restarting
the StatefulSet (maas-keycloak) does NOT remove persisted realms and leaves the
hardcoded password "letmein" active. Ensure the README references the ConfigMap
name (keycloak-test-realms), the StatefulSet name (maas-keycloak), the realm
names (tenant-a, tenant-b), user names, and the cleanup script name so
maintainers know which resources to target.

In `@scripts/cleanup-keycloak.sh`:
- Around line 66-70: The current conditional ([[ "$DELETE_CRDS" == true ]] || [[
"$FORCE" == true ]]) lets --force trigger CRD deletion; change it so CRDs are
deleted only when DELETE_CRDS is true (remove FORCE from this OR check).
Specifically, update the if condition to check only DELETE_CRDS (e.g., if [[
"$DELETE_CRDS" == true ]]; then ...) and keep FORCE used only to skip any
interactive confirmation step prior to deletion (use FORCE to bypass
read/confirm logic, not to enable deletion itself). Ensure references to
DELETE_CRDS and FORCE remain intact so the delete block only runs when
DELETE_CRDS is true and FORCE merely suppresses prompts.

In `@scripts/data/maas-api-authpolicy-external-oidc-patch.yaml`:
- Around line 15-20: The patch is adding plain.expression to X-MaaS-Username-OC
while an earlier patch leaves plain.selector present, causing an
invalid/ambiguous header definition; update the X-MaaS-Username-OC entry so that
the plain block does not retain selector when you add expression — specifically
remove or unset plain.selector and ensure plain contains only expression
(has(auth.identity.preferred_username) ? ...) so the merge from the earlier
patch doesn't leave both plain.selector and plain.expression present; target the
X-MaaS-Username-OC plain block in this YAML to replace/remove the selector
before adding expression.

In `@scripts/deploy.sh`:
- Around line 1486-1489: The script currently treats failures from
patch_authpolicy_from_template as non-fatal (calls log_warn then returns 0),
which lets deployment succeed despite AuthPolicy not being applied; change both
occurrences (the block using patch_authpolicy_from_template "$authpolicy_name"
"$api_keys_patch" "$NAMESPACE" and the similar block at the other location) to
treat failure as fatal by returning a non-zero status or exiting non-zero (e.g.,
replace the current log_warn + return 0 with log_error + return 1 or an exit 1)
so the deployment aborts on AuthPolicy patch failure.
- Around line 1480-1483: This patch is mutating the generated
kuadrant.io/AuthPolicy (using authpolicy_name and
opendatahub.io/managed="false") which will be reverted by the maas controller;
instead update the owning MaaS resource/template that produces the AuthPolicy
(see maas-controller/pkg/controller/maas/maasauthpolicy_controller.go) so
ownership and annotations persist. Replace the kubectl annotate calls that touch
authpolicy_name (and the audience patch sections) with logic that patches the
MaaS CR or its template (the resource that creates maas-api-auth-policy) to
include opendatahub.io/managed="false" and any audience modifications, ensuring
the controller will reconcile the generated AuthPolicy with those desired values
rather than overwriting them.

In `@scripts/setup-keycloak.sh`:
- Around line 26-30: The usage banner currently points to the hardcoded secret
name "keycloak-initial-admin" which is inconsistent with the rest of the script
and docs; change the banner to reference the dynamic secret name using the
KEYCLOAK_NAME variable (i.e., replace "keycloak-initial-admin" with
"${KEYCLOAK_NAME}-initial-admin" so it matches occurrences like
${KEYCLOAK_NAME}-initial-admin printed later and in
docs/samples/install/keycloak/README.md).
- Around line 91-95: Replace the unpinned Subscription channel value to pin a
tested Keycloak Operator release: set a specific startingCSV in the Subscription
(instead of relying solely on channel: fast) and change installPlanApproval to
Manual (or provide startingCSV with Automatic if you intentionally pin a
version), and make the operator source and channel configurable inputs (e.g.,
variables used when rendering the Subscription) with a known-good default;
update the subscription block that currently contains the fields channel, name,
source, sourceNamespace, installPlanApproval to accept configurable inputs and
include startingCSV to fix the non-reproducible upgrade behavior.

In `@test/e2e/tests/test_external_oidc.py`:
- Around line 65-68: The test test_oidc_token_can_create_api_key prints a
truncated API key prefix which leaks credential material; update the
print/logging in that test (the print in test_oidc_token_can_create_api_key that
uses data.get('key')) to stop printing any part of the key and only output the
API key id (data.get('id')) or remove the print entirely—locate the print
statement in test_oidc_token_can_create_api_key and replace it so only the id is
logged (or no secret-derived values are included).
- Around line 25-40: _request_oidc_token currently reuses global TLS_VERIFY
causing tests to disable TLS for external OIDC token exchange; introduce a
separate OIDC_TLS_VERIFY env flag (default true) and use it for the
requests.post verify parameter in _request_oidc_token instead of TLS_VERIFY;
parse OIDC_TLS_VERIFY into a boolean (e.g., via existing env helper or a new
_bool_env/_optional_env) and document default behavior so password-grant
requests to OIDC_TOKEN_URL always verify TLS unless explicitly overridden.

---

Nitpick comments:
In `@scripts/cleanup-keycloak.sh`:
- Around line 95-98: The conditional uses an unquoted shell variable REPLY which
can cause word-splitting or injection; update the test in the interactive prompt
handling so the REPLY variable is quoted inside the [[ ... ]] pattern match
(i.e., reference "$REPLY" in the if condition that currently reads if [[ !
$REPLY =~ ^[Yy]$ ]]; then) so the script safely evaluates the user input.
- Around line 112-119: The Keycloak deletion block can hang and currently uses
an arbitrary sleep; replace the simple delete+sleep pattern with a deletion that
enforces a maximum wait: call kubectl delete keycloak "$KEYCLOAK_NAME" -n
"$NAMESPACE" and then use a bounded wait (e.g. kubectl wait --for=delete
keycloak/"$KEYCLOAK_NAME" -n "$NAMESPACE" --timeout=<reasonable-duration>) or
implement a loop that polls kubectl get keycloak "$KEYCLOAK_NAME" -n
"$NAMESPACE" with a counter and sleep interval, aborting after a max retry count
and emitting an error/exit code if the CR is not removed; remove the arbitrary
sleep 5 and ensure you surface timeout/failure using the script exit status.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 2ef35d54-5825-421c-99e6-c5ef70a7b15f

📥 Commits

Reviewing files that changed from the base of the PR and between 362563d and a4733e7.

📒 Files selected for processing (20)
  • .github/hack/cleanup-odh.sh
  • deployment/overlays/odh/kustomization.yaml
  • deployment/overlays/odh/maas-api-auth-policy-oidc-patch.yaml
  • deployment/overlays/odh/params.env
  • docs/content/architecture.md
  • docs/content/configuration-and-management/token-management.md
  • docs/samples/install/keycloak/README.md
  • docs/samples/install/keycloak/test-realms/README.md
  • docs/samples/install/keycloak/test-realms/apply-test-realms.sh
  • docs/samples/install/keycloak/test-realms/tenant-a-realm.json
  • docs/samples/install/keycloak/test-realms/tenant-b-realm.json
  • scripts/README.md
  • scripts/cleanup-keycloak.sh
  • scripts/data/maas-api-authpolicy-api-keys-patch.yaml
  • scripts/data/maas-api-authpolicy-external-oidc-patch.yaml
  • scripts/deploy.sh
  • scripts/setup-keycloak.sh
  • test/e2e/README.md
  • test/e2e/scripts/prow_run_smoke_test.sh
  • test/e2e/tests/test_external_oidc.py

Comment on lines +129 to +136
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && cd ../.. && pwd)"
if [[ -f "${SCRIPT_DIR}/scripts/cleanup-keycloak.sh" ]]; then
# Pass --delete-crds if --include-crds was specified for this script
if $INCLUDE_CRDS; then
"${SCRIPT_DIR}/scripts/cleanup-keycloak.sh" --force --delete-crds 2>/dev/null || true
else
"${SCRIPT_DIR}/scripts/cleanup-keycloak.sh" --force 2>/dev/null || true
fi
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

SCRIPT_DIR may be empty if directory traversal fails.

If cd "$(dirname "${BASH_SOURCE[0]}")" or cd ../.. fails, SCRIPT_DIR becomes empty and the -f test passes vacuously on an empty path. With set -e, the subshell exits but the assignment still succeeds with empty string.

Proposed fix
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && cd ../.. && pwd)"
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" || SCRIPT_DIR=""
 if [[ -f "${SCRIPT_DIR}/scripts/cleanup-keycloak.sh" ]]; then
+if [[ -n "$SCRIPT_DIR" && -f "${SCRIPT_DIR}/scripts/cleanup-keycloak.sh" ]]; then
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/hack/cleanup-odh.sh around lines 129 - 136, The assignment to
SCRIPT_DIR can result in an empty string if the subshell cd operations fail,
which makes the subsequent [[ -f "${SCRIPT_DIR}/scripts/cleanup-keycloak.sh" ]]
test unreliable; update the SCRIPT_DIR computation and guard to ensure it
succeeded: compute SCRIPT_DIR in a way that fails fast (e.g., use a subshell
that exits non-zero on cd failure or test each cd), then check that SCRIPT_DIR
is non-empty and points to an existing directory before running the -f test and
invoking scripts/cleanup-keycloak.sh; reference the SCRIPT_DIR variable, the
conditional test [[ -f "${SCRIPT_DIR}/scripts/cleanup-keycloak.sh" ]], and the
INCLUDE_CRDS branch when adding the validation and early exit or error handling.

Comment on lines +10 to +15
oidc-identities:
when:
- predicate: '!request.headers.authorization.startsWith("Bearer sk-oai-") && request.headers.authorization.matches("^Bearer [^.]+\\.[^.]+\\.[^.]+$")'
jwt:
issuerUrl: https://oidc.example.invalid/realms/maas
priority: 1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Locate and examine the main YAML file
find . -name "maas-api-auth-policy-oidc-patch.yaml" -type f

Repository: opendatahub-io/models-as-a-service

Length of output: 140


🏁 Script executed:

# Find the mirror file mentioned in the review
find . -name "*authpolicy*external*oidc*" -type f

Repository: opendatahub-io/models-as-a-service

Length of output: 137


🏁 Script executed:

# Get a broader view of the auth policy files
find . -path "*/maas-api*" -name "*.yaml" | head -20

Repository: opendatahub-io/models-as-a-service

Length of output: 1181


🏁 Script executed:

cat -n ./deployment/overlays/odh/maas-api-auth-policy-oidc-patch.yaml

Repository: opendatahub-io/models-as-a-service

Length of output: 1788


🏁 Script executed:

cat -n ./scripts/data/maas-api-authpolicy-external-oidc-patch.yaml

Repository: opendatahub-io/models-as-a-service

Length of output: 1320


🏁 Script executed:

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

Repository: opendatahub-io/models-as-a-service

Length of output: 3869


🌐 Web query:

Authorino JWT validation audience claim supported fields kuadrant

💡 Result:

Authorino, part of the Kuadrant project, supports JWT validation primarily through OpenID Connect (OIDC) Discovery. For OIDC JWTs, Authorino verifies the JSON Web Signature (JWS), checks time validity (exp, nbf, iat claims), ensures the 'iss' claim matches the configured issuerUrl, and verifies the 'kid' header matches a cached JWK from the JWKS. There is no explicit configuration field in the jwt spec (issuerUrl, ttl, jwksUrl, etc.) for specifying expected audience values ('aud' claim). The standard JWT 'aud' claim validation is not directly supported or configurable in Authorino's core JWT authentication. Instead, users can access the validated JWT payload (including 'aud') in the authorization JSON via selectors like auth.identity.aud and enforce audience checks using authorization policies, such as pattern-matching rules or OPA Rego policies. For comparison, Authorino does explicitly support audience validation in kubernetesTokenReview via an 'audiences' array field, where token audiences must include specified values or the requested host/port. To enforce JWT audience: - Use patternMatching authorization: selector: auth.identity.aud, operator: in, value: ["expected-aud"]. - Or OPA policy checking input.auth.identity.aud. This is confirmed across official Kuadrant docs, GitHub features.md, CRD schema snippets, and user guides showing JWT configs without 'aud' fields but payload access for policies.

Citations:


🌐 Web query:

Kuadrant AuthPolicy JWT validation client binding issuerUrl only security

💡 Result:

In Kuadrant AuthPolicy, JWT validation is configured under spec.rules.authentication..jwt, with issuerUrl specifying the OpenID Connect issuer endpoint (e.g., http://keycloak.keycloak.svc.cluster.local:8080/realms/kuadrant). Authorino (underlying Kuadrant auth) uses OpenID Connect Discovery to fetch JWKS and configuration from the issuerUrl, verifies the JWT signature (JWS), kid claim match, and time validity. The decoded JWT payload becomes auth.identity for further use in authorization or rate limiting. No explicit "client binding" field like azp (authorized party, client_id) or audience validation is shown in examples or references; validation relies on standard OIDC checks from discovery (likely including aud if present in token and config). For security-focused client binding, use authorization rules (e.g., patternMatching on auth.identity.azp or auth.identity.aud) after JWT validation, such as: authorization: client-check: patternMatching: patterns: - selector: auth.identity.azp operator: eq value: "expected-client-id" This ensures only JWTs from authorized clients are accepted post-validation. issuerUrl must match the token's iss claim exactly for validation to succeed.

Citations:


🏁 Script executed:

# Check if there are any other JWT validation examples in the repo
rg -i "audience|authorized.*party" deployment/ --type yaml | head -20

Repository: opendatahub-io/models-as-a-service

Length of output: 341


🏁 Script executed:

# Search for any JWT validation documentation or comments in the repo
rg -i "jwt.*audience|aud.*claim" . --type yaml --type md | head -20

Repository: opendatahub-io/models-as-a-service

Length of output: 60


Add audience/authorized-party constraint via authorization rule.

Severity: major (CWE-287). Authorino's JWT authentication validates the issuer URL, JWS signature, and token timing, but does not support direct audience or client-binding validation in the jwt spec. With issuerUrl alone, any JWT minted by that realm—including tokens issued for different clients—will pass JWT validation and allow token replay against maas-api. Add an authorization rule in spec.rules.authorization to validate the audience claim:

authorization:
  jwt-audience-validation:
    patternMatching:
      patterns:
        - selector: auth.identity.aud
          operator: eq
          value: "maas-api"

Or validate authorized party (azp) if present. Mirror the same constraint in scripts/data/maas-api-authpolicy-external-oidc-patch.yaml.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@deployment/overlays/odh/maas-api-auth-policy-oidc-patch.yaml` around lines 10
- 15, The oidc-identities block currently only sets jwt.issuerUrl (issuerUrl) so
add an authorization rule under spec.rules.authorization that enforces
audience/authorized-party checks: create a rule (e.g., jwt-audience-validation)
using patternMatching with selector auth.identity.aud operator eq value
"maas-api" and also add an alternative check for auth.identity.azp when present;
apply the same change pattern to the corresponding
scripts/data/maas-api-authpolicy-external-oidc-patch.yaml to keep external patch
in sync.

Comment on lines +74 to +75
- **Valid redirect URIs:** `https://*.{your-cluster-domain}/*`
- **Web origins:** `+` (allows CORS from redirect URIs)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Keycloak/OpenID Connect, are exact redirect URIs and explicit web origins recommended over wildcard redirect URIs or Web origins: + for confidential clients?

💡 Result:

Yes, in Keycloak/OpenID Connect, exact redirect URIs and explicit web origins are recommended over wildcard redirect URIs or Web origins: + for confidential clients.

Citations:


🏁 Script executed:

# First, find the README file and check its size
fd "keycloak.*README" --type f

Repository: opendatahub-io/models-as-a-service

Length of output: 60


🌐 Web query:

CWE-346 Origin Validation Error and CWE-942 Permissive Cross-domain Policy - are these applicable to OIDC redirect URI and web origins security?

💡 Result:

Yes, both CWE-346 (Origin Validation Error) and CWE-942 (Permissive Cross-domain Policy with Untrusted Domains) are applicable to OIDC redirect URI and web origins security. CWE-346 applies to OIDC redirect URI validation. The OpenID Connect Core specification requires exact matching of the redirect_uri against pre-registered values using simple string comparison. Insufficient validation of the redirect_uri's origin (scheme, host) can lead to open redirects, SSRF, or open redirection attacks (related to CWE-601), where untrusted origins are accepted. Examples include failures to restrict dangerous schemes like javascript: or data:, or improper host/path validation, as seen in vulnerabilities like CVE-2020-10776 in Keycloak and analyses showing path confusion/parameter pollution risks. CWE-942 applies to web origins security in OIDC, particularly CORS configurations on OIDC endpoints (e.g., /.well-known/openid-configuration, /userinfo, /token). Permissive CORS policies (e.g., Access-Control-Allow-Origin: *) on these endpoints allow untrusted origins to make cross-origin requests, potentially leaking user info or tokens. Official docs recommend restricting to trusted origins, and implementations like Ory Hydra configure per-client allowed_cors_origins to avoid this.

Citations:


🏁 Script executed:

# Search for the exact file
find . -path "*/docs/samples/install/keycloak/README.md" -type f

Repository: opendatahub-io/models-as-a-service

Length of output: 119


🏁 Script executed:

# If found, read the file to verify the content at lines 74-75 and 164-179
cat -n docs/samples/install/keycloak/README.md | sed -n '70,80p'
cat -n docs/samples/install/keycloak/README.md | sed -n '160,180p'

Repository: opendatahub-io/models-as-a-service

Length of output: 1316


Restrict Keycloak client redirect URIs and web origins to explicit values.

Severity: major. Lines 74–75 recommend wildcard redirect URIs and Web origins: +, which violate OIDC security controls. On a shared cluster domain, this allows any route under the wildcard to receive authorization responses (CWE-346: Origin Validation Error) and permits unrestricted CORS access to OIDC endpoints (CWE-942: Permissive Cross-domain Policy). OpenID Connect requires exact string matching of redirect_uri values. Use explicit, non-wildcard callback URLs and origins.

Suggested fix
-   - **Valid redirect URIs:** `https://*.{your-cluster-domain}/*`
-   - **Web origins:** `+` (allows CORS from redirect URIs)
+   - **Valid redirect URIs:** `https://<exact-maas-host>/<exact-callback-path>`
+   - **Web origins:** `https://<exact-maas-host>`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/samples/install/keycloak/README.md` around lines 74 - 75, Replace the
wildcard redirect URI and permissive Web origins in the README instructions:
change the "Valid redirect URIs: `https://*.{your-cluster-domain}/*`" guidance
to require explicit, exact callback URLs (e.g.,
`https://app.example.com/oauth/callback`) and update "Web origins: `+`" to list
explicit origins (e.g., `https://app.example.com`) only; ensure the text
references that Keycloak/OpenID Connect requires exact string matching of
redirect_uri and that CORS must be restricted to the named origins rather than
using `+` or wildcards.

Comment on lines +164 to +179
curl -k -X POST \
"https://${KEYCLOAK_HOST}/realms/${REALM_NAME}/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password" \
-d "client_id=${CLIENT_ID}" \
-d "client_secret=${CLIENT_SECRET}" \
-d "username=${USERNAME}" \
-d "password=${PASSWORD}" \
| jq .

# Decode the access token to verify groups claim
# Copy the access_token from above output
TOKEN="eyJhbGci..."

# Decode (requires jq and base64)
echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null | jq .
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does curl's -k/--insecure option disable TLS certificate verification, and are JWT segments base64url encoded rather than standard base64?

💡 Result:

Yes, curl's -k/--insecure option disables TLS certificate verification. Yes, JWT segments are base64url encoded rather than standard base64.

Citations:


Remove -k from curl commands and fix JWT payload decoding.

Sending client secrets and passwords over HTTPS with curl -k (which disables certificate validation) enables MITM credential capture—CWE-295. The base64 decoding is also incorrect; JWT payloads are base64url-encoded, not standard base64, and require proper padding handling.

Suggested changes
- curl -k -X POST \
+ curl --fail --silent --show-error --cacert /path/to/router-ca.crt -X POST \
- echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null | jq .
+ TOKEN="$TOKEN" python - <<'PY'
+ import os, base64, json
+ payload = os.environ["TOKEN"].split(".")[1]
+ payload += "=" * (-len(payload) % 4)
+ print(json.dumps(json.loads(base64.urlsafe_b64decode(payload)), indent=2))
+ PY
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/samples/install/keycloak/README.md` around lines 164 - 179, Remove the
insecure curl option by deleting "-k" from the token request (the curl command
that posts to
"https://${KEYCLOAK_HOST}/realms/${REALM_NAME}/protocol/openid-connect/token")
and rely on system CA validation or supply a CA bundle (e.g., --cacert) instead;
for JWT decoding, change the payload decode pipeline that uses TOKEN and "echo
$TOKEN | cut -d'.' -f2 | base64 -d" to perform base64url decoding with URL-safe
character translation and proper padding (replace '-'/'_' to '+'/'/' and add '='
padding to a length multiple of 4) before calling base64 --decode (or an
equivalent base64url decode) so the JWT payload is decoded correctly.

Comment on lines +68 to +109
EXISTING_ARGS=$(kubectl get keycloak "$KEYCLOAK_NAME" -n "$NAMESPACE" \
-o jsonpath='{.spec.unsupported.podTemplate.spec.containers[0].args}' 2>/dev/null || echo "[]")

if echo "$EXISTING_ARGS" | grep -q "import-realm"; then
echo " Keycloak already configured for realm import"
else
# Patch Keycloak to enable realm import
kubectl patch keycloak "$KEYCLOAK_NAME" -n "$NAMESPACE" --type=merge -p '
{
"spec": {
"unsupported": {
"podTemplate": {
"spec": {
"containers": [
{
"name": "keycloak",
"args": [
"--verbose",
"start",
"--import-realm"
],
"volumeMounts": [
{
"name": "test-realms",
"mountPath": "/opt/keycloak/data/import"
}
]
}
],
"volumes": [
{
"name": "test-realms",
"configMap": {
"name": "'${CONFIGMAP_NAME}'"
}
}
]
}
}
}
}
}'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For kubectl patch --type=mergeon a Kubernetes CustomResource, are list fields replaced wholesale, and does the Keycloak Operator documentspec.unsupported.podTemplate as an unsupported escape hatch?

💡 Result:

For kubectl patch --type=merge on a Kubernetes CustomResource, list fields are replaced wholesale. Strategic merge patch, which intelligently merges lists based on patchStrategy and merge keys defined in built-in types, is not supported for custom resources. With --type=merge, it falls back to JSON merge patch semantics where providing a list replaces the entire existing list. The Keycloak Operator documents spec.unsupported.podTemplate as an unsupported escape hatch. Official documentation explicitly describes it as a "temporary workaround" for cases where no supported field exists, containing "highly experimental configuration options that are not completely tested and are Tech Preview," with "no guarantee exists that the Deployment will work as expected."

Citations:


Avoid merge-patching the unsupported pod template on CustomResources.

kubectl patch --type=merge on CustomResources uses JSON merge patch semantics (not strategic merge patch), which replaces array fields wholesale. This patch will clobber any existing spec.unsupported.podTemplate.spec.containers or volumes entries. The Keycloak Operator explicitly documents spec.unsupported.podTemplate as a "temporary workaround" and unsupported escape hatch with "no guarantee [the] Deployment will work as expected."

Additionally, hardcoding the full Keycloak args list tightly couples this import flow to operator and container internals.

Refuse to run when spec.unsupported.podTemplate is already populated, or refetch and merge the current object instead of replacing it. Minimum guard:

Guard to prevent clobbering existing unsupported pod settings
 if echo "$EXISTING_ARGS" | grep -q "import-realm"; then
   echo "  Keycloak already configured for realm import"
+elif kubectl get keycloak "$KEYCLOAK_NAME" -n "$NAMESPACE" \
+    -o jsonpath='{.spec.unsupported.podTemplate}' 2>/dev/null | grep -q .; then
+  echo "❌ ERROR: existing spec.unsupported.podTemplate detected; refusing to overwrite it" >&2
+  exit 1
 else
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
EXISTING_ARGS=$(kubectl get keycloak "$KEYCLOAK_NAME" -n "$NAMESPACE" \
-o jsonpath='{.spec.unsupported.podTemplate.spec.containers[0].args}' 2>/dev/null || echo "[]")
if echo "$EXISTING_ARGS" | grep -q "import-realm"; then
echo " Keycloak already configured for realm import"
else
# Patch Keycloak to enable realm import
kubectl patch keycloak "$KEYCLOAK_NAME" -n "$NAMESPACE" --type=merge -p '
{
"spec": {
"unsupported": {
"podTemplate": {
"spec": {
"containers": [
{
"name": "keycloak",
"args": [
"--verbose",
"start",
"--import-realm"
],
"volumeMounts": [
{
"name": "test-realms",
"mountPath": "/opt/keycloak/data/import"
}
]
}
],
"volumes": [
{
"name": "test-realms",
"configMap": {
"name": "'${CONFIGMAP_NAME}'"
}
}
]
}
}
}
}
}'
EXISTING_ARGS=$(kubectl get keycloak "$KEYCLOAK_NAME" -n "$NAMESPACE" \
-o jsonpath='{.spec.unsupported.podTemplate.spec.containers[0].args}' 2>/dev/null || echo "[]")
if echo "$EXISTING_ARGS" | grep -q "import-realm"; then
echo " Keycloak already configured for realm import"
elif kubectl get keycloak "$KEYCLOAK_NAME" -n "$NAMESPACE" \
-o jsonpath='{.spec.unsupported.podTemplate}' 2>/dev/null | grep -q .; then
echo "❌ ERROR: existing spec.unsupported.podTemplate detected; refusing to overwrite it" >&2
exit 1
else
# Patch Keycloak to enable realm import
kubectl patch keycloak "$KEYCLOAK_NAME" -n "$NAMESPACE" --type=merge -p '
{
"spec": {
"unsupported": {
"podTemplate": {
"spec": {
"containers": [
{
"name": "keycloak",
"args": [
"--verbose",
"start",
"--import-realm"
],
"volumeMounts": [
{
"name": "test-realms",
"mountPath": "/opt/keycloak/data/import"
}
]
}
],
"volumes": [
{
"name": "test-realms",
"configMap": {
"name": "'${CONFIGMAP_NAME}'"
}
}
]
}
}
}
}
}'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/samples/install/keycloak/test-realms/apply-test-realms.sh` around lines
68 - 109, The current kubectl patch uses JSON merge semantics which will
overwrite arrays under spec.unsupported.podTemplate (containers, volumes);
instead either refuse to run if spec.unsupported.podTemplate is already
populated or perform a safe merge: fetch the existing Keycloak CR (use
EXISTING_ARGS/kubectl get keycloak "$KEYCLOAK_NAME" -n "$NAMESPACE" -o json),
inspect spec.unsupported.podTemplate, and if it exists exit with a clear
message; otherwise build a merged object that appends/merges the new container
entry (name "keycloak", args including "--import-realm" but avoid hardcoding the
full args list) and the test-realms volume into the existing arrays, then apply
the merged CR via kubectl apply -f - (or use kubectl patch --type=json with
explicit add operations targeting specific array positions) to avoid clobbering
containers or volumes; ensure references to CONFIGMAP_NAME, KEYCLOAK_NAME,
spec.unsupported.podTemplate, containers, and volumes are used to locate and
merge the correct fields.

Comment on lines +1486 to +1489
if ! patch_authpolicy_from_template "$authpolicy_name" "$api_keys_patch" "$NAMESPACE"; then
log_warn " Failed to patch AuthPolicy with API key configuration"
return 0
fi
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

AuthPolicy patch failures should abort the deployment.

Both branches warn and return success, so --external-oidc can finish green while the API-key path or JWT validation was never installed. For auth configuration, fail closed.

Suggested fix
   log_info "  Patching AuthPolicy to ensure API key support..."
   if ! patch_authpolicy_from_template "$authpolicy_name" "$api_keys_patch" "$NAMESPACE"; then
-    log_warn "  Failed to patch AuthPolicy with API key configuration"
-    return 0
+    log_error "  Failed to patch AuthPolicy with API key configuration"
+    return 1
   fi
@@
   log_info "  Enabling OIDC JWT validation with issuer: $oidc_issuer_url"
   if ! patch_authpolicy_from_template "$authpolicy_name" "$oidc_patch" "$NAMESPACE" "$oidc_issuer_url"; then
-    log_warn "  Failed to patch AuthPolicy with external OIDC configuration"
-    return 0
+    log_error "  Failed to patch AuthPolicy with external OIDC configuration"
+    return 1
   fi

Also applies to: 1504-1507

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/deploy.sh` around lines 1486 - 1489, The script currently treats
failures from patch_authpolicy_from_template as non-fatal (calls log_warn then
returns 0), which lets deployment succeed despite AuthPolicy not being applied;
change both occurrences (the block using patch_authpolicy_from_template
"$authpolicy_name" "$api_keys_patch" "$NAMESPACE" and the similar block at the
other location) to treat failure as fatal by returning a non-zero status or
exiting non-zero (e.g., replace the current log_warn + return 0 with log_error +
return 1 or an exit 1) so the deployment aborts on AuthPolicy patch failure.

Comment on lines +26 to +30
# Access Keycloak Admin Console:
# 1. Get admin password:
# kubectl get secret -n keycloak-system keycloak-initial-admin \
# -o jsonpath='{.data.password}' | base64 -d
# 2. Navigate to: https://keycloak.{cluster-domain}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix the bootstrap secret name in the usage banner.

Lines 28-29 point readers at keycloak-initial-admin, but this file later prints ${KEYCLOAK_NAME}-initial-admin at Lines 294-299 and docs/samples/install/keycloak/README.md:19-24 uses maas-keycloak-initial-admin. Following the banner as written returns NotFound.

Suggested fix
-#      kubectl get secret -n keycloak-system keycloak-initial-admin \
+#      kubectl get secret -n keycloak-system maas-keycloak-initial-admin \
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/setup-keycloak.sh` around lines 26 - 30, The usage banner currently
points to the hardcoded secret name "keycloak-initial-admin" which is
inconsistent with the rest of the script and docs; change the banner to
reference the dynamic secret name using the KEYCLOAK_NAME variable (i.e.,
replace "keycloak-initial-admin" with "${KEYCLOAK_NAME}-initial-admin" so it
matches occurrences like ${KEYCLOAK_NAME}-initial-admin printed later and in
docs/samples/install/keycloak/README.md).

Comment on lines +91 to +95
channel: fast
name: keycloak-operator
source: community-operators
sourceNamespace: openshift-marketplace
installPlanApproval: Automatic
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

What stability guarantees does the Keycloak Operator fast channel provide, and is pinning a tested operator release recommended for reproducible installs?

💡 Result:

The Keycloak Operator's fast channel provides supported, production-quality releases that are monitored for stability before potential promotion to a stable channel, per standard OLM channel conventions. It delivers new minor and patch versions as soon as they are generally available, offering full support but intended for early adopters or pre-production testing to assess quality. No explicit stability guarantees like "no breaking changes" are stated in official Keycloak docs beyond general OLM practices. Pinning a specific tested operator release (via startingCSV in the Subscription or manual approval) is strongly recommended for reproducible installs. Official docs warn that automatic OLM upgrades can cause issues like database migration problems, CR incompatibilities, and lack of downgrade options, advising manual approval and testing before upgrades.

Citations:


Pin the Keycloak Operator to a tested release instead of subscribing to fast.

The unpinned fast channel subscription causes non-reproducible deployments: subsequent runs can pull different operator versions without any code change, while docs/samples/install/keycloak/test-realms/apply-test-realms.sh:68-109 depends on unsupported pod-template mutation. Official Keycloak documentation warns that automatic OLM upgrades can trigger database migration problems and CR incompatibilities. Pin a specific tested operator release via startingCSV in the Subscription or use manual approval, and expose the operator source/channel as configurable inputs with a known-good default.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/setup-keycloak.sh` around lines 91 - 95, Replace the unpinned
Subscription channel value to pin a tested Keycloak Operator release: set a
specific startingCSV in the Subscription (instead of relying solely on channel:
fast) and change installPlanApproval to Manual (or provide startingCSV with
Automatic if you intentionally pin a version), and make the operator source and
channel configurable inputs (e.g., variables used when rendering the
Subscription) with a known-good default; update the subscription block that
currently contains the fields channel, name, source, sourceNamespace,
installPlanApproval to accept configurable inputs and include startingCSV to fix
the non-reproducible upgrade behavior.

Comment on lines +25 to +40
def _request_oidc_token() -> str:
token_url = _required_env("OIDC_TOKEN_URL")
client_id = _required_env("OIDC_CLIENT_ID")
username = _required_env("OIDC_USERNAME")
password = _required_env("OIDC_PASSWORD")

response = requests.post(
token_url,
data={
"grant_type": "password",
"client_id": client_id,
"username": username,
"password": password,
},
timeout=30,
verify=TLS_VERIFY,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd test/e2e && find . -name "test_external_oidc.py" -o -name "prow_run_smoke_test.sh" -o -name "conftest.py" | head -20

Repository: opendatahub-io/models-as-a-service

Length of output: 160


🏁 Script executed:

cat -n test/e2e/tests/test_external_oidc.py | head -100

Repository: opendatahub-io/models-as-a-service

Length of output: 4415


🏁 Script executed:

cat test/e2e/scripts/prow_run_smoke_test.sh

Repository: opendatahub-io/models-as-a-service

Length of output: 33603


🏁 Script executed:

cat -n test/e2e/tests/conftest.py 2>/dev/null | head -100

Repository: opendatahub-io/models-as-a-service

Length of output: 4181


Split TLS verification for external OIDC token exchange.

Severity: major (CWE-295). _request_oidc_token() uses verify=TLS_VERIFY, which is disabled when E2E_SKIP_TLS_VERIFY=true (set globally in prow_run_smoke_test.sh for self-signed cluster certificates). This applies TLS verification bypass to the external IdP password-grant exchange against OIDC_TOKEN_URL, exposing real credentials (username, password, client_id) to MITM attacks. Introduce a dedicated OIDC_TLS_VERIFY setting that defaults to enabled, independent of cluster TLS configuration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/e2e/tests/test_external_oidc.py` around lines 25 - 40,
_request_oidc_token currently reuses global TLS_VERIFY causing tests to disable
TLS for external OIDC token exchange; introduce a separate OIDC_TLS_VERIFY env
flag (default true) and use it for the requests.post verify parameter in
_request_oidc_token instead of TLS_VERIFY; parse OIDC_TLS_VERIFY into a boolean
(e.g., via existing env helper or a new _bool_env/_optional_env) and document
default behavior so password-grant requests to OIDC_TOKEN_URL always verify TLS
unless explicitly overridden.

Comment on lines +65 to +68
def test_oidc_token_can_create_api_key(self, maas_api_base_url: str):
token = _request_oidc_token()
data = _create_oidc_api_key(maas_api_base_url, token)
print(f"[oidc] created api key id={data.get('id')} prefix={data.get('key', '')[:18]}...")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Stop printing API-key prefixes.

Severity: minor (CWE-532). Even a truncated sk-oai-* value ends up in retained CI artifacts and is still credential material. Log the key id only.

Proposed fix
-        print(f"[oidc] created api key id={data.get('id')} prefix={data.get('key', '')[:18]}...")
+        print(f"[oidc] created api key id={data.get('id')}")

As per coding guidelines, "Security vulnerabilities (provide severity, exploit scenario, and remediation code)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/e2e/tests/test_external_oidc.py` around lines 65 - 68, The test
test_oidc_token_can_create_api_key prints a truncated API key prefix which leaks
credential material; update the print/logging in that test (the print in
test_oidc_token_can_create_api_key that uses data.get('key')) to stop printing
any part of the key and only output the API key id (data.get('id')) or remove
the print entirely—locate the print statement in
test_oidc_token_can_create_api_key and replace it so only the id is logged (or
no secret-derived values are included).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants