From 3802f38a58174b7b42b55c1b9e978088811afe43 Mon Sep 17 00:00:00 2001 From: Mariusz Sabath Date: Wed, 25 Feb 2026 21:28:10 -0500 Subject: [PATCH 1/7] feat: migrate AuthBridge demo from demo realm to kagenti realm Migrate all GitHub Issue Agent demo files from the Keycloak "demo" realm to the shared "kagenti" realm. This aligns the demo with the platform-wide realm migration (kagenti#764) so that UI-issued tokens and demo tokens share the same issuer, enabling UI chat with AuthBridge-protected agents. Updated files: - k8s/configmaps.yaml: KEYCLOAK_REALM, TOKEN_URL, ISSUER - k8s/github-tool-deployment.yaml: ISSUER, JWKS_URL - k8s/git-issue-agent-deployment.yaml: JWKS_URI - setup_keycloak.py: default KEYCLOAK_REALM - demo-manual.md: all realm references in docs and commands Part of: kagenti/kagenti#767 Signed-off-by: Mariusz Sabath Made-with: Cursor --- AuthBridge/demos/github-issue/demo-manual.md | 38 +++++++++---------- .../demos/github-issue/k8s/configmaps.yaml | 6 +-- .../k8s/git-issue-agent-deployment.yaml | 2 +- .../k8s/github-tool-deployment.yaml | 4 +- .../demos/github-issue/setup_keycloak.py | 4 +- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/AuthBridge/demos/github-issue/demo-manual.md b/AuthBridge/demos/github-issue/demo-manual.md index 7cda60624..48380c9c6 100644 --- a/AuthBridge/demos/github-issue/demo-manual.md +++ b/AuthBridge/demos/github-issue/demo-manual.md @@ -72,7 +72,7 @@ providing end-to-end security: │ │ SPIRE (namespace: │ │ KEYCLOAK (namespace: │ │ │ │ spire) │ │ keycloak) │ │ │ │ │ │ │ │ -│ │ Provides SPIFFE │ │ - demo realm │ │ +│ │ Provides SPIFFE │ │ - kagenti realm │ │ │ │ identities (SVIDs) │ │ - token exchange │ │ │ └──────────────────────┘ └──────────────────────┘ │ └──────────────────────────────────────────────────────────────────────────────────┘ @@ -272,7 +272,7 @@ This creates: | Resource | Name | Purpose | |----------|------|---------| -| **Realm** | `demo` | Keycloak realm for the demo | +| **Realm** | `kagenti` | Keycloak realm for the demo | | **Client** | `github-tool` | Target audience for token exchange | | **Scope** | `agent-team1-git-issue-agent-aud` | Realm DEFAULT — auto-adds Agent's SPIFFE ID to all tokens | | **Scope** | `github-tool-aud` | Realm OPTIONAL — for exchanged tokens targeting the tool | @@ -304,7 +304,7 @@ Verify the ConfigMap has the correct values: ```bash kubectl get configmap environments -n team1 -o jsonpath='{.data.KEYCLOAK_REALM}' -# Expected: demo +# Expected: kagenti ``` > **Note:** If you're using a different namespace or service account, edit @@ -512,7 +512,7 @@ A properly signed token from a **different Keycloak realm** has a valid signatur the issuer does not match the configured `ISSUER` in `authbridge-config`: ```bash -# Get a valid token from the master realm (different issuer than "demo") +# Get a valid token from the master realm (different issuer than "kagenti") WRONG_ISSUER_TOKEN=$(kubectl exec test-client -n team1 -- curl -s \ "http://keycloak-service.keycloak.svc:8080/realms/master/protocol/openid-connect/token" \ -d "grant_type=password" \ @@ -523,7 +523,7 @@ WRONG_ISSUER_TOKEN=$(kubectl exec test-client -n team1 -- curl -s \ kubectl exec test-client -n team1 -- curl -s \ -H "Authorization: Bearer $WRONG_ISSUER_TOKEN" \ http://git-issue-agent:8080/ -# Expected: {"error":"unauthorized","message":"token validation failed: invalid issuer: expected http://keycloak.localtest.me:8080/realms/demo, got ..."} +# Expected: {"error":"unauthorized","message":"token validation failed: invalid issuer: expected http://keycloak.localtest.me:8080/realms/kagenti, got ..."} ``` > **Why this matters:** Even though the token is cryptographically valid (signed by @@ -540,7 +540,7 @@ echo "Agent Client ID: $CLIENT_ID" # Get a service account token (simulating what the UI would obtain) TOKEN=$(kubectl exec test-client -n team1 -- curl -s -X POST \ - "http://keycloak-service.keycloak.svc:8080/realms/demo/protocol/openid-connect/token" \ + "http://keycloak-service.keycloak.svc:8080/realms/kagenti/protocol/openid-connect/token" \ -d "grant_type=client_credentials" \ -d "client_id=$CLIENT_ID" \ -d "client_secret=$CLIENT_SECRET" | jq -r '.access_token') @@ -581,8 +581,8 @@ Expected (one line per request in 8b–8e): ``` [Inbound] Missing Authorization header [Inbound] JWT validation failed: failed to parse/validate token: ... -[Inbound] JWT validation failed: invalid issuer: expected http://keycloak.localtest.me:8080/realms/demo, got ... -[Inbound] Token validated - issuer: http://keycloak.localtest.me:8080/realms/demo, audience: [...] +[Inbound] JWT validation failed: invalid issuer: expected http://keycloak.localtest.me:8080/realms/kagenti, got ... +[Inbound] Token validated - issuer: http://keycloak.localtest.me:8080/realms/kagenti, audience: [...] [Inbound] JWT validation succeeded, forwarding request ``` @@ -627,11 +627,11 @@ echo "Admin token length: ${#ADMIN_TOKEN}" # Expected: Admin token length: 782 # If 0 or 4 (null), Keycloak is not reachable or credentials are wrong — stop here. -# Look up the agent's client in the demo realm. +# Look up the agent's client in the kagenti realm. # The client ID is the SPIFFE ID (URL-encoded in the query parameter). SPIFFE_ID="spiffe://localtest.me/ns/team1/sa/git-issue-agent" CLIENTS=$(curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \ - "http://keycloak-service.keycloak.svc:8080/admin/realms/demo/clients" \ + "http://keycloak-service.keycloak.svc:8080/admin/realms/kagenti/clients" \ --data-urlencode "clientId=$SPIFFE_ID" --get) INTERNAL_ID=$(echo "$CLIENTS" | jq -r ".[0].id") CLIENT_ID=$(echo "$CLIENTS" | jq -r ".[0].clientId") @@ -648,7 +648,7 @@ echo "Secret length: ${#CLIENT_SECRET}" # Get an OAuth token for the agent TOKEN=$(curl -s -X POST \ - "http://keycloak-service.keycloak.svc:8080/realms/demo/protocol/openid-connect/token" \ + "http://keycloak-service.keycloak.svc:8080/realms/kagenti/protocol/openid-connect/token" \ -d "grant_type=client_credentials" \ --data-urlencode "client_id=$CLIENT_ID" \ --data-urlencode "client_secret=$CLIENT_SECRET" | jq -r ".access_token") @@ -669,7 +669,7 @@ Token length: 1165 > **Troubleshooting:** If `INTERNAL_ID` shows `null`, the Keycloak query didn't find > the client. Verify `$ADMIN_TOKEN` is not empty (Keycloak reachable?) and that > `setup_keycloak.py` was run. You can also list all clients with: -> `curl -s -H "Authorization: Bearer $ADMIN_TOKEN" "http://keycloak-service.keycloak.svc:8080/admin/realms/demo/clients" | jq '.[].clientId'` +> `curl -s -H "Authorization: Bearer $ADMIN_TOKEN" "http://keycloak-service.keycloak.svc:8080/admin/realms/kagenti/clients" | jq '.[].clientId'` ### 9c. Send a prompt to the agent @@ -755,7 +755,7 @@ kubectl logs deployment/git-issue-agent -n team1 -c envoy-proxy 2>&1 | grep -i " Expected output: ``` -[Inbound] Token validated - issuer: http://keycloak.localtest.me:8080/realms/demo, audience: [spiffe://localtest.me/ns/team1/sa/git-issue-agent ...] +[Inbound] Token validated - issuer: http://keycloak.localtest.me:8080/realms/kagenti, audience: [spiffe://localtest.me/ns/team1/sa/git-issue-agent ...] [Inbound] JWT validation succeeded, forwarding request ``` @@ -764,7 +764,7 @@ If you ran the rejection tests (8b, 8c, 8d), you should also see: ``` [Inbound] Missing Authorization header [Inbound] JWT validation failed: failed to parse/validate token: ... -[Inbound] JWT validation failed: invalid issuer: expected http://keycloak.localtest.me:8080/realms/demo, got ... +[Inbound] JWT validation failed: invalid issuer: expected http://keycloak.localtest.me:8080/realms/kagenti, got ... ``` **Outbound token exchange logs** (RFC 8693 token exchange for the GitHub tool): @@ -776,7 +776,7 @@ kubectl logs deployment/git-issue-agent -n team1 -c envoy-proxy 2>&1 | grep "^20 Expected: ``` -[Token Exchange] Token URL: http://keycloak-service.keycloak.svc:8080/realms/demo/protocol/openid-connect/token +[Token Exchange] Token URL: http://keycloak-service.keycloak.svc:8080/realms/kagenti/protocol/openid-connect/token [Token Exchange] Client ID: spiffe://localtest.me/ns/team1/sa/git-issue-agent [Token Exchange] Audience: github-tool [Token Exchange] Scopes: openid github-tool-aud github-full-access @@ -812,7 +812,7 @@ kubectl delete pod test-client -n team1 --ignore-not-found **Symptom:** `{"error":"invalid_client","error_description":"Invalid client or Invalid client credentials"}` **Cause:** The client was registered in the wrong Keycloak realm (typically `master` instead -of `demo`). This happens when the `environments` ConfigMap is updated **after** the agent +of `kagenti`). This happens when the `environments` ConfigMap is updated **after** the agent pod has already started — the pod reads ConfigMap values at startup time. **Fix:** @@ -822,7 +822,7 @@ pod has already started — the pod reads ConfigMap values at startup time. kubectl exec deployment/git-issue-agent -n team1 -c kagenti-client-registration -- \ sh -c 'echo "KEYCLOAK_REALM=$KEYCLOAK_REALM"' -# 2. If it shows "master" instead of "demo", delete the stale client +# 2. If it shows "master" instead of "kagenti", delete the stale client kubectl exec test-client -n team1 -- sh -c ' ADMIN_TOKEN=$(curl -s http://keycloak-service.keycloak.svc:8080/realms/master/protocol/openid-connect/token \ -d "grant_type=password" -d "client_id=admin-cli" -d "username=admin" -d "password=admin" | jq -r ".access_token") @@ -838,7 +838,7 @@ echo "Deleted stale client from master realm"' # 3. Verify ConfigMap is correct, then restart kubectl get configmap environments -n team1 -o jsonpath='{.data.KEYCLOAK_REALM}' -# Should show: demo +# Should show: kagenti kubectl rollout restart deployment/git-issue-agent -n team1 ``` @@ -880,7 +880,7 @@ kubectl logs deployment/git-issue-agent -n team1 -c agent **Symptom:** Tool rejects the exchanged token **Fix:** Verify the tool's environment variables match the Keycloak configuration: -- `ISSUER` should be `http://keycloak.localtest.me:8080/realms/demo` +- `ISSUER` should be `http://keycloak.localtest.me:8080/realms/kagenti` - `AUDIENCE` should be `github-tool` ### Upstream Request Timeout diff --git a/AuthBridge/demos/github-issue/k8s/configmaps.yaml b/AuthBridge/demos/github-issue/k8s/configmaps.yaml index 48619950a..e162e8f33 100644 --- a/AuthBridge/demos/github-issue/k8s/configmaps.yaml +++ b/AuthBridge/demos/github-issue/k8s/configmaps.yaml @@ -23,7 +23,7 @@ metadata: data: SPIRE_ENABLED: "true" KEYCLOAK_URL: "http://keycloak-service.keycloak.svc:8080" - KEYCLOAK_REALM: "demo" + KEYCLOAK_REALM: "kagenti" # Demo/development only: do NOT use hardcoded admin credentials in production. # For production deployments, supply these via a Kubernetes Secret and reference # them from your Pod/Deployment using env.valueFrom.secretKeyRef instead. @@ -46,9 +46,9 @@ metadata: name: authbridge-config namespace: team1 data: - TOKEN_URL: "http://keycloak-service.keycloak.svc:8080/realms/demo/protocol/openid-connect/token" + TOKEN_URL: "http://keycloak-service.keycloak.svc:8080/realms/kagenti/protocol/openid-connect/token" # ISSUER: Required. Must match the "iss" claim in tokens (Keycloak frontend URL). - ISSUER: "http://keycloak.localtest.me:8080/realms/demo" + ISSUER: "http://keycloak.localtest.me:8080/realms/kagenti" # EXPECTED_AUDIENCE: The agent's SPIFFE ID. Validates inbound tokens are intended for this agent. # Format: spiffe://localtest.me/ns//sa/ # Update if deploying to a different namespace or using a different service account. diff --git a/AuthBridge/demos/github-issue/k8s/git-issue-agent-deployment.yaml b/AuthBridge/demos/github-issue/k8s/git-issue-agent-deployment.yaml index bbcf81802..59b8dd832 100644 --- a/AuthBridge/demos/github-issue/k8s/git-issue-agent-deployment.yaml +++ b/AuthBridge/demos/github-issue/k8s/git-issue-agent-deployment.yaml @@ -95,7 +95,7 @@ spec: # JWKS URI for validating incoming tokens from Keycloak - name: JWKS_URI - value: "http://keycloak-service.keycloak.svc:8080/realms/demo/protocol/openid-connect/certs" + value: "http://keycloak-service.keycloak.svc:8080/realms/kagenti/protocol/openid-connect/certs" resources: limits: cpu: 500m diff --git a/AuthBridge/demos/github-issue/k8s/github-tool-deployment.yaml b/AuthBridge/demos/github-issue/k8s/github-tool-deployment.yaml index 92084753a..e02c8af6b 100644 --- a/AuthBridge/demos/github-issue/k8s/github-tool-deployment.yaml +++ b/AuthBridge/demos/github-issue/k8s/github-tool-deployment.yaml @@ -65,9 +65,9 @@ spec: key: UPSTREAM_HEADER_TO_USE_IF_NOT_IN_AUDIENCE # Keycloak validation settings for incoming tokens - name: ISSUER - value: "http://keycloak.localtest.me:8080/realms/demo" + value: "http://keycloak.localtest.me:8080/realms/kagenti" - name: JWKS_URL - value: "http://keycloak-service.keycloak.svc.cluster.local:8080/realms/demo/protocol/openid-connect/certs" + value: "http://keycloak-service.keycloak.svc.cluster.local:8080/realms/kagenti/protocol/openid-connect/certs" - name: AUDIENCE value: "github-tool" resources: diff --git a/AuthBridge/demos/github-issue/setup_keycloak.py b/AuthBridge/demos/github-issue/setup_keycloak.py index f55e5e65c..c4d2a22cc 100644 --- a/AuthBridge/demos/github-issue/setup_keycloak.py +++ b/AuthBridge/demos/github-issue/setup_keycloak.py @@ -53,7 +53,7 @@ # Default configuration KEYCLOAK_URL = os.environ.get("KEYCLOAK_URL", "http://keycloak.localtest.me:8080") -KEYCLOAK_REALM = os.environ.get("KEYCLOAK_REALM", "demo") +KEYCLOAK_REALM = os.environ.get("KEYCLOAK_REALM", "kagenti") KEYCLOAK_ADMIN_USERNAME = os.environ.get("KEYCLOAK_ADMIN_USERNAME", "admin") KEYCLOAK_ADMIN_PASSWORD = os.environ.get("KEYCLOAK_ADMIN_PASSWORD", "admin") @@ -243,7 +243,7 @@ def main(): print(f"\n--- Setting up realm: {KEYCLOAK_REALM} ---") get_or_create_realm(master_admin, KEYCLOAK_REALM) - # Switch to demo realm + # Switch to target realm keycloak_admin = KeycloakAdmin( server_url=KEYCLOAK_URL, username=KEYCLOAK_ADMIN_USERNAME, From ff1e3e30ec71ddcfeec71e6aae765054ba5b7d8d Mon Sep 17 00:00:00 2001 From: Mariusz Sabath Date: Thu, 26 Feb 2026 11:45:04 -0500 Subject: [PATCH 2/7] feat: migrate demo-ui.md from demo realm to kagenti realm Update all Keycloak realm references in demo-ui.md from "demo" to "kagenti" to align with the platform-wide realm migration (kagenti#764). With the UI and demos now on the same realm, the UI chat with AuthBridge-protected agents works end-to-end. The Step 8 known-limitation workaround (kagenti-extensions#147) is replaced with instructions for direct UI chat. Part of: kagenti/kagenti#767 Signed-off-by: Mariusz Sabath Made-with: Cursor --- AuthBridge/demos/github-issue/demo-ui.md | 52 ++++++++++++------------ 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/AuthBridge/demos/github-issue/demo-ui.md b/AuthBridge/demos/github-issue/demo-ui.md index 2243a2851..d84c235c6 100644 --- a/AuthBridge/demos/github-issue/demo-ui.md +++ b/AuthBridge/demos/github-issue/demo-ui.md @@ -73,7 +73,7 @@ providing end-to-end security: │ │ SPIRE (namespace: │ │ KEYCLOAK (namespace: │ │ │ │ spire) │ │ keycloak) │ │ │ │ │ │ │ │ -│ │ Provides SPIFFE │ │ - demo realm │ │ +│ │ Provides SPIFFE │ │ - kagenti realm │ │ │ │ identities (SVIDs) │ │ - token exchange │ │ │ └──────────────────────┘ └──────────────────────┘ │ └──────────────────────────────────────────────────────────────────────────────────┘ @@ -163,7 +163,7 @@ This creates: | Resource | Name | Purpose | |----------|------|---------| -| **Realm** | `demo` | Keycloak realm for the demo | +| **Realm** | `kagenti` | Keycloak realm for the demo | | **Client** | `github-tool` | Target audience for token exchange | | **Scope** | `agent-team1-git-issue-agent-aud` | Realm DEFAULT — auto-adds Agent's SPIFFE ID to all tokens | | **Scope** | `github-tool-aud` | Realm OPTIONAL — for exchanged tokens targeting the tool | @@ -190,7 +190,7 @@ Verify: ```bash kubectl get configmap environments -n team1 -o jsonpath='{.data.KEYCLOAK_REALM}' -# Expected: demo +# Expected: kagenti ``` --- @@ -453,33 +453,31 @@ model is pulled (`ollama pull ibm/granite4:latest`). ## Step 8: Chat via Kagenti UI - - -> **Known limitation ([kagenti-extensions#147](https://github.com/kagenti/kagenti-extensions/issues/147)):** -> The Kagenti UI authenticates via Keycloak's `master` realm, -> but AuthBridge validates inbound JWTs against the `demo` realm JWKS. Since the -> go-processor currently supports only a single JWKS URL, UI chat requests receive -> a `401` ("key not found in key set"). The **Agent Card** page works because -> `/.well-known/*` paths bypass JWT validation -> ([PR #133](https://github.com/kagenti/kagenti-extensions/pull/133)). -> -> **Use [Step 9: Test via CLI](#step-9-test-via-cli-optional)** to test the full -> AuthBridge flow end-to-end with a valid `demo`-realm token. +With the platform-wide migration to the `kagenti` realm +([kagenti#764](https://github.com/kagenti/kagenti/pull/764)), both the Kagenti UI +and AuthBridge demos now use the same Keycloak realm. This resolves the previous +realm mismatch issue +([kagenti-extensions#147](https://github.com/kagenti/kagenti-extensions/issues/147)). 1. Navigate to the **Agent Catalog** in the Kagenti UI. 2. Select the `team1` namespace. 3. Under **Available Agents**, select `git-issue-agent` and click **View Details**. 4. Verify the **Agent Card** is visible (this confirms the agent is running and the `/.well-known/*` bypass is working). -5. Chat via the UI will not work until the realm mismatch is resolved (see note above). - Use the CLI test in Step 9 instead. +5. Use the **Chat** panel to send a message, e.g. "List issues in kagenti/kagenti repo". +6. The agent should respond with a list of GitHub issues. + +> **Troubleshooting:** If UI chat returns a `401`, verify that both the UI and +> AuthBridge are configured against the same `kagenti` realm. You can also use +> [Step 9: Test via CLI](#step-9-test-via-cli) to test the full AuthBridge flow +> independently. --- ## Step 9: Test via CLI Test the AuthBridge flow from the command line to verify inbound validation and -token exchange using a `demo`-realm token. +token exchange using a `kagenti`-realm token. > **Note:** The CLI test commands below use the same service name and port > (`git-issue-agent:8080`) as both the UI and manual deployments. @@ -545,11 +543,11 @@ ADMIN_TOKEN=$(curl -s http://keycloak-service.keycloak.svc:8080/realms/master/pr echo "Admin token length: ${#ADMIN_TOKEN}" -# Look up the agent's client in the demo realm. +# Look up the agent's client in the kagenti realm. # The client ID is the SPIFFE ID (URL-encoded in the query parameter). SPIFFE_ID="spiffe://localtest.me/ns/team1/sa/git-issue-agent" CLIENTS=$(curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \ - "http://keycloak-service.keycloak.svc:8080/admin/realms/demo/clients" \ + "http://keycloak-service.keycloak.svc:8080/admin/realms/kagenti/clients" \ --data-urlencode "clientId=$SPIFFE_ID" --get) INTERNAL_ID=$(echo "$CLIENTS" | jq -r ".[0].id") CLIENT_ID=$(echo "$CLIENTS" | jq -r ".[0].clientId") @@ -565,7 +563,7 @@ echo "Secret length: ${#CLIENT_SECRET}" # Get an OAuth token for the agent TOKEN=$(curl -s -X POST \ - "http://keycloak-service.keycloak.svc:8080/realms/demo/protocol/openid-connect/token" \ + "http://keycloak-service.keycloak.svc:8080/realms/kagenti/protocol/openid-connect/token" \ -d "grant_type=client_credentials" \ --data-urlencode "client_id=$CLIENT_ID" \ --data-urlencode "client_secret=$CLIENT_SECRET" | jq -r ".access_token") @@ -611,7 +609,7 @@ kubectl logs deployment/git-issue-agent -n team1 -c envoy-proxy 2>&1 | grep "\[I Expected: ``` -[Inbound] Token validated - issuer: http://keycloak.localtest.me:8080/realms/demo, audience: [spiffe://localtest.me/ns/team1/sa/git-issue-agent ...] +[Inbound] Token validated - issuer: http://keycloak.localtest.me:8080/realms/kagenti, audience: [spiffe://localtest.me/ns/team1/sa/git-issue-agent ...] [Inbound] JWT validation succeeded, forwarding request ``` @@ -624,7 +622,7 @@ kubectl logs deployment/git-issue-agent -n team1 -c envoy-proxy 2>&1 | grep "^20 Expected: ``` -[Token Exchange] Token URL: http://keycloak-service.keycloak.svc:8080/realms/demo/protocol/openid-connect/token +[Token Exchange] Token URL: http://keycloak-service.keycloak.svc:8080/realms/kagenti/protocol/openid-connect/token [Token Exchange] Client ID: spiffe://localtest.me/ns/team1/sa/git-issue-agent [Token Exchange] Audience: github-tool [Token Exchange] Scopes: openid github-tool-aud github-full-access @@ -649,7 +647,7 @@ If the agent is missing environment variables after UI deployment (e.g., `MCP_UR # Set missing env vars on the agent container kubectl set env deployment/git-issue-agent -n team1 -c agent \ MCP_URL="http://github-tool-mcp:9090/mcp" \ - JWKS_URI="http://keycloak-service.keycloak.svc:8080/realms/demo/protocol/openid-connect/certs" + JWKS_URI="http://keycloak-service.keycloak.svc:8080/realms/kagenti/protocol/openid-connect/certs" # If using OpenAI and the key is in a secret: kubectl patch deployment git-issue-agent -n team1 --type=json -p='[ @@ -689,7 +687,7 @@ kubectl rollout status deployment/git-issue-agent -n team1 --timeout=180s **Symptom:** `{"error":"invalid_client","error_description":"Invalid client or Invalid client credentials"}` **Cause:** The client was registered in the wrong Keycloak realm (typically `master` -instead of `demo`). This happens when the `environments` ConfigMap is updated **after** +instead of `kagenti`). This happens when the `environments` ConfigMap is updated **after** the agent pod has already started. This is especially common with UI deployments where the Kagenti Helm chart may have pre-existing ConfigMaps. @@ -698,7 +696,7 @@ the Kagenti Helm chart may have pre-existing ConfigMaps. ```bash # 1. Verify ConfigMap is correct kubectl get configmap environments -n team1 -o jsonpath='{.data.KEYCLOAK_REALM}' -# Should show: demo +# Should show: kagenti # 2. If wrong, re-apply ConfigMaps and restart kubectl apply -f demos/github-issue/k8s/configmaps.yaml @@ -786,7 +784,7 @@ direct curl to the tool gets `Connection reset by peer`. **Symptom:** Tool rejects the exchanged token **Fix:** Verify the tool's environment variables match the Keycloak configuration: -- `ISSUER` should be `http://keycloak.localtest.me:8080/realms/demo` +- `ISSUER` should be `http://keycloak.localtest.me:8080/realms/kagenti` - `AUDIENCE` should be `github-tool` --- From 22d2698fbbe2ea7099bb0e4f3e6cb0fd2aca2a39 Mon Sep 17 00:00:00 2001 From: Mariusz Sabath Date: Thu, 26 Feb 2026 23:17:26 -0500 Subject: [PATCH 3/7] fix: use kagenti realm for admin token in CLI test steps The CLI test steps were obtaining the Keycloak admin token from the master realm. Now that the admin user exists in the kagenti realm with realm-admin permissions, use the kagenti realm consistently throughout the demo to avoid cross-realm dependencies. The wrong-issuer test and troubleshooting sections intentionally retain master realm references. Signed-off-by: Mariusz Sabath Made-with: Cursor --- AuthBridge/demos/github-issue/demo-manual.md | 4 ++-- AuthBridge/demos/github-issue/demo-ui.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/AuthBridge/demos/github-issue/demo-manual.md b/AuthBridge/demos/github-issue/demo-manual.md index 48380c9c6..ad8bd3bbc 100644 --- a/AuthBridge/demos/github-issue/demo-manual.md +++ b/AuthBridge/demos/github-issue/demo-manual.md @@ -616,8 +616,8 @@ kubectl exec -it test-client -n team1 -- sh Inside the test-client pod, run: ```bash -# Get a Keycloak admin token -ADMIN_TOKEN=$(curl -s http://keycloak-service.keycloak.svc:8080/realms/master/protocol/openid-connect/token \ +# Get a Keycloak admin token from the kagenti realm +ADMIN_TOKEN=$(curl -s http://keycloak-service.keycloak.svc:8080/realms/kagenti/protocol/openid-connect/token \ -d "grant_type=password" \ -d "client_id=admin-cli" \ -d "username=admin" \ diff --git a/AuthBridge/demos/github-issue/demo-ui.md b/AuthBridge/demos/github-issue/demo-ui.md index d84c235c6..e3c72e196 100644 --- a/AuthBridge/demos/github-issue/demo-ui.md +++ b/AuthBridge/demos/github-issue/demo-ui.md @@ -534,8 +534,8 @@ kubectl exec -it test-client -n team1 -- sh Inside the pod, get credentials and send a request: ```bash -# Get a Keycloak admin token -ADMIN_TOKEN=$(curl -s http://keycloak-service.keycloak.svc:8080/realms/master/protocol/openid-connect/token \ +# Get a Keycloak admin token from the kagenti realm +ADMIN_TOKEN=$(curl -s http://keycloak-service.keycloak.svc:8080/realms/kagenti/protocol/openid-connect/token \ -d "grant_type=password" \ -d "client_id=admin-cli" \ -d "username=admin" \ From dc7f352d8c6b6f6704155be791a2b173f020ce98 Mon Sep 17 00:00:00 2001 From: Mariusz Sabath Date: Thu, 5 Mar 2026 22:21:17 -0500 Subject: [PATCH 4/7] refactor: slim demo configmaps to only override authbridge-config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Kagenti installer (kagenti/kagenti#851) now creates correct default ConfigMaps for all AuthBridge components: - authbridge-config with templatized kagenti realm - envoy-config with 300s timeouts and Lua filter for inbound direction Remove environments, spiffe-helper-config, and envoy-config from the demo's configmaps.yaml — only authbridge-config remains, overriding TARGET_AUDIENCE (github-tool), TARGET_SCOPES, and EXPECTED_AUDIENCE with demo-specific values. Update demo-ui.md Step 2 and demo-manual.md Step 3 to explain that only authbridge-config is overridden. Update troubleshooting sections to reference the installer's envoy-config defaults. Signed-off-by: Mariusz Sabath Assisted-By: Claude (Anthropic AI) --- AuthBridge/demos/github-issue/demo-manual.md | 70 +++--- AuthBridge/demos/github-issue/demo-ui.md | 41 ++-- .../demos/github-issue/k8s/configmaps.yaml | 218 +----------------- 3 files changed, 56 insertions(+), 273 deletions(-) diff --git a/AuthBridge/demos/github-issue/demo-manual.md b/AuthBridge/demos/github-issue/demo-manual.md index ad8bd3bbc..40bbde8d0 100644 --- a/AuthBridge/demos/github-issue/demo-manual.md +++ b/AuthBridge/demos/github-issue/demo-manual.md @@ -282,36 +282,31 @@ This creates: --- -## Step 3: Apply ConfigMaps +## Step 3: Apply Demo ConfigMaps -Apply the demo-specific ConfigMaps that configure AuthBridge for this demo. These -ConfigMaps tell the sidecars how to connect to Keycloak and where to exchange tokens. - -> **Important: Apply ConfigMaps BEFORE deploying the agent.** The agent pod reads -> ConfigMap values at startup. If the agent starts before the ConfigMaps are correct, -> the client will be registered in the wrong realm. If this happens, delete the stale -> Keycloak client, fix the ConfigMaps, and restart the agent deployment with -> `kubectl rollout restart deployment/git-issue-agent -n team1`. +The Kagenti installer creates default ConfigMaps (`environments`, +`spiffe-helper-config`, `envoy-config`, `authbridge-config`) with the correct +`kagenti` realm settings and 300s Envoy timeouts. This step overrides only +`authbridge-config` with demo-specific values — the token exchange target +audience (`github-tool`), scopes, and the agent's SPIFFE ID for inbound +audience validation. Apply this **before** deploying the agent. ```bash cd AuthBridge -# Apply the GitHub Issue demo ConfigMaps +# Override authbridge-config for this demo (sets TARGET_AUDIENCE=github-tool) kubectl apply -f demos/github-issue/k8s/configmaps.yaml ``` -Verify the ConfigMap has the correct values: +Verify the demo ConfigMap was applied: ```bash -kubectl get configmap environments -n team1 -o jsonpath='{.data.KEYCLOAK_REALM}' -# Expected: kagenti +kubectl get configmap authbridge-config -n team1 -o jsonpath='{.data.TARGET_AUDIENCE}' +# Expected: github-tool ``` > **Note:** If you're using a different namespace or service account, edit > `configmaps.yaml` and update the `namespace` and `EXPECTED_AUDIENCE` fields. -> If SPIRE is not available, set `SPIRE_ENABLED: "false"` in the `environments` -> ConfigMap and update `EXPECTED_AUDIENCE` to match the static client ID format -> (e.g., `team1/git-issue-agent` instead of `spiffe://...`). --- @@ -811,34 +806,18 @@ kubectl delete pod test-client -n team1 --ignore-not-found **Symptom:** `{"error":"invalid_client","error_description":"Invalid client or Invalid client credentials"}` -**Cause:** The client was registered in the wrong Keycloak realm (typically `master` instead -of `kagenti`). This happens when the `environments` ConfigMap is updated **after** the agent -pod has already started — the pod reads ConfigMap values at startup time. +**Cause:** The agent pod's `environments` ConfigMap was missing or incorrect at startup, +so the client-registration sidecar registered the client with wrong settings. **Fix:** ```bash -# 1. Check which realm the client was registered in -kubectl exec deployment/git-issue-agent -n team1 -c kagenti-client-registration -- \ - sh -c 'echo "KEYCLOAK_REALM=$KEYCLOAK_REALM"' - -# 2. If it shows "master" instead of "kagenti", delete the stale client -kubectl exec test-client -n team1 -- sh -c ' -ADMIN_TOKEN=$(curl -s http://keycloak-service.keycloak.svc:8080/realms/master/protocol/openid-connect/token \ - -d "grant_type=password" -d "client_id=admin-cli" -d "username=admin" -d "password=admin" | jq -r ".access_token") -# With SPIRE enabled (default), use the SPIFFE ID as client ID. -# With SPIRE disabled, use: CLIENT_ID="team1/git-issue-agent" -CLIENT_ID="spiffe://localtest.me/ns/team1/sa/git-issue-agent" -INTERNAL_ID=$(curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \ - --data-urlencode "clientId=$CLIENT_ID" \ - --get "http://keycloak-service.keycloak.svc:8080/admin/realms/master/clients" | jq -r ".[0].id") -curl -s -X DELETE -H "Authorization: Bearer $ADMIN_TOKEN" \ - "http://keycloak-service.keycloak.svc:8080/admin/realms/master/clients/$INTERNAL_ID" -echo "Deleted stale client from master realm"' - -# 3. Verify ConfigMap is correct, then restart +# 1. Verify the installer's environments ConfigMap has the correct realm kubectl get configmap environments -n team1 -o jsonpath='{.data.KEYCLOAK_REALM}' # Should show: kagenti + +# 2. Re-apply the demo ConfigMap and restart +kubectl apply -f demos/github-issue/k8s/configmaps.yaml kubectl rollout restart deployment/git-issue-agent -n team1 ``` @@ -889,9 +868,16 @@ kubectl logs deployment/git-issue-agent -n team1 -c agent **Cause:** The LLM inference takes longer than the Envoy route timeout. -**Fix:** The `envoy-config` ConfigMap sets the route timeout to 300 seconds. If you -still hit timeouts, increase the `timeout` values in both the outbound and inbound -route configs and the `ext_proc` filter, then restart the agent: +**Fix:** The installer's `envoy-config` ConfigMap sets route and ext_proc +timeouts to 300 seconds (5 min). If you still hit timeouts, verify the +ConfigMap has the correct values: + +```bash +kubectl get configmap envoy-config -n team1 -o jsonpath='{.data.envoy\.yaml}' | grep "timeout:" +``` + +If you see `30s` values instead of `300s`, reinstall Kagenti (the installer +creates the correct defaults) and restart the agent: ```bash kubectl rollout restart deployment/git-issue-agent -n team1 @@ -937,7 +923,7 @@ kubectl delete mutatingwebhookconfiguration kagenti-webhook-authbridge-mutating- | `demos/github-issue/demo-manual.md` | This guide | | `demos/github-issue/demo-ui.md` | UI-driven deployment guide | | `demos/github-issue/setup_keycloak.py` | Keycloak configuration script | -| `demos/github-issue/k8s/configmaps.yaml` | ConfigMaps for AuthBridge sidecars | +| `demos/github-issue/k8s/configmaps.yaml` | Demo-specific authbridge-config override | | `demos/github-issue/k8s/git-issue-agent-deployment.yaml` | Agent deployment with AuthBridge labels | | `demos/github-issue/k8s/github-tool-deployment.yaml` | GitHub tool deployment (no injection) | diff --git a/AuthBridge/demos/github-issue/demo-ui.md b/AuthBridge/demos/github-issue/demo-ui.md index e3c72e196..0485b6b15 100644 --- a/AuthBridge/demos/github-issue/demo-ui.md +++ b/AuthBridge/demos/github-issue/demo-ui.md @@ -173,26 +173,22 @@ This creates: --- -## Step 2: Apply ConfigMaps +## Step 2: Apply Demo ConfigMaps -> **Critical: Apply ConfigMaps BEFORE importing the agent in the UI.** The agent pod -> reads ConfigMap values at startup. If the agent starts before the ConfigMaps are -> correct, the client will be registered in the wrong Keycloak realm. +The Kagenti installer creates default ConfigMaps (`environments`, +`spiffe-helper-config`, `envoy-config`, `authbridge-config`) with the correct +`kagenti` realm settings and 300s Envoy timeouts. This step overrides only +`authbridge-config` with demo-specific values — the token exchange target +audience (`github-tool`), scopes, and the agent's SPIFFE ID for inbound +audience validation. ```bash cd AuthBridge -# Apply the GitHub Issue demo ConfigMaps +# Override authbridge-config for this demo (sets TARGET_AUDIENCE=github-tool) kubectl apply -f demos/github-issue/k8s/configmaps.yaml ``` -Verify: - -```bash -kubectl get configmap environments -n team1 -o jsonpath='{.data.KEYCLOAK_REALM}' -# Expected: kagenti -``` - --- ## Step 3: Create the GitHub Tool Secrets @@ -686,19 +682,17 @@ kubectl rollout status deployment/git-issue-agent -n team1 --timeout=180s **Symptom:** `{"error":"invalid_client","error_description":"Invalid client or Invalid client credentials"}` -**Cause:** The client was registered in the wrong Keycloak realm (typically `master` -instead of `kagenti`). This happens when the `environments` ConfigMap is updated **after** -the agent pod has already started. This is especially common with UI deployments where -the Kagenti Helm chart may have pre-existing ConfigMaps. +**Cause:** The agent pod's `environments` ConfigMap was missing or incorrect at startup, +so the client-registration sidecar registered the client with wrong settings. **Fix:** ```bash -# 1. Verify ConfigMap is correct +# 1. Verify the installer's environments ConfigMap has the correct realm kubectl get configmap environments -n team1 -o jsonpath='{.data.KEYCLOAK_REALM}' # Should show: kagenti -# 2. If wrong, re-apply ConfigMaps and restart +# 2. Re-apply the demo ConfigMap and restart kubectl apply -f demos/github-issue/k8s/configmaps.yaml kubectl rollout restart deployment/git-issue-agent -n team1 ``` @@ -729,17 +723,18 @@ Both the UI and manual deployments create `git-issue-agent:8080` (targetPort 800 **Cause:** The LLM inference takes longer than the Envoy route timeout. -**Fix:** The `envoy-config` ConfigMap sets the route timeout to 300 seconds (5 min). -If you still hit timeouts, check that the ConfigMap was applied correctly: +**Fix:** The installer's `envoy-config` ConfigMap sets route and ext_proc +timeouts to 300 seconds (5 min). If you still hit timeouts, verify the +ConfigMap has the correct values: ```bash kubectl get configmap envoy-config -n team1 -o jsonpath='{.data.envoy\.yaml}' | grep "timeout:" ``` -If you see `30s` values instead of `300s`, re-apply the ConfigMaps and restart: +If you see `30s` values instead of `300s`, reinstall Kagenti (the installer +creates the correct defaults) and restart the agent: ```bash -kubectl apply -f demos/github-issue/k8s/configmaps.yaml kubectl rollout restart deployment/git-issue-agent -n team1 ``` @@ -834,7 +829,7 @@ kubectl delete mutatingwebhookconfiguration kagenti-webhook-authbridge-mutating- | `demos/github-issue/demo-ui.md` | This guide | | `demos/github-issue/demo-manual.md` | Fully manual deployment guide | | `demos/github-issue/setup_keycloak.py` | Keycloak configuration script | -| `demos/github-issue/k8s/configmaps.yaml` | ConfigMaps for AuthBridge sidecars | +| `demos/github-issue/k8s/configmaps.yaml` | Demo-specific authbridge-config override | | `demos/github-issue/k8s/git-issue-agent-deployment.yaml` | Agent deployment YAML (manual only) | | `demos/github-issue/k8s/github-tool-deployment.yaml` | GitHub tool deployment YAML (manual only) | diff --git a/AuthBridge/demos/github-issue/k8s/configmaps.yaml b/AuthBridge/demos/github-issue/k8s/configmaps.yaml index e162e8f33..8ac55ba4c 100644 --- a/AuthBridge/demos/github-issue/k8s/configmaps.yaml +++ b/AuthBridge/demos/github-issue/k8s/configmaps.yaml @@ -1,34 +1,17 @@ -# ConfigMaps for GitHub Issue Agent + AuthBridge Demo +# Demo-specific ConfigMap overrides for GitHub Issue Agent + AuthBridge # -# These ConfigMaps configure AuthBridge sidecar injection for the GitHub Issue Agent. -# The agent gets sidecars injected by the kagenti-webhook, and these ConfigMaps -# tell those sidecars how to authenticate with Keycloak and perform token exchange. +# The Kagenti installer already creates correct defaults for environments, +# spiffe-helper-config, envoy-config, and authbridge-config (with the kagenti +# realm). This file only overrides authbridge-config with demo-specific values: +# - TARGET_AUDIENCE: github-tool (the MCP GitHub tool) +# - TARGET_SCOPES: scopes for token exchange to the tool +# - EXPECTED_AUDIENCE: the agent's SPIFFE ID for inbound validation # # Usage: # kubectl apply -f configmaps.yaml # # Note: These manifests default to namespace "team1". If you deploy to a different -# namespace, update the metadata.namespace fields and EXPECTED_AUDIENCE (or -# use kustomize) accordingly, and adjust the service account if needed. - ---- -# environments ConfigMap - Used by kagenti-client-registration container -# This configures how the client-registration sidecar connects to Keycloak -# to register the agent as an OAuth client. -apiVersion: v1 -kind: ConfigMap -metadata: - name: environments - namespace: team1 -data: - SPIRE_ENABLED: "true" - KEYCLOAK_URL: "http://keycloak-service.keycloak.svc:8080" - KEYCLOAK_REALM: "kagenti" - # Demo/development only: do NOT use hardcoded admin credentials in production. - # For production deployments, supply these via a Kubernetes Secret and reference - # them from your Pod/Deployment using env.valueFrom.secretKeyRef instead. - KEYCLOAK_ADMIN_USERNAME: "admin" - KEYCLOAK_ADMIN_PASSWORD: "admin" +# namespace, update the metadata.namespace and EXPECTED_AUDIENCE accordingly. --- # authbridge-config ConfigMap - Used by envoy-proxy for token exchange @@ -37,9 +20,8 @@ data: # 1. INBOUND validation: Validates JWT tokens on incoming requests to the agent # 2. OUTBOUND exchange: Exchanges tokens when the agent calls the GitHub tool # -# The TARGET_AUDIENCE is the Keycloak client representing the GitHub tool. -# When the agent calls the tool, AuthBridge exchanges the incoming token for -# a new one with aud=github-tool, which the tool validates. +# TOKEN_URL and ISSUER match the installer defaults (kagenti realm) and are +# included because kubectl apply replaces the entire ConfigMap. apiVersion: v1 kind: ConfigMap metadata: @@ -47,7 +29,6 @@ metadata: namespace: team1 data: TOKEN_URL: "http://keycloak-service.keycloak.svc:8080/realms/kagenti/protocol/openid-connect/token" - # ISSUER: Required. Must match the "iss" claim in tokens (Keycloak frontend URL). ISSUER: "http://keycloak.localtest.me:8080/realms/kagenti" # EXPECTED_AUDIENCE: The agent's SPIFFE ID. Validates inbound tokens are intended for this agent. # Format: spiffe://localtest.me/ns//sa/ @@ -59,182 +40,3 @@ data: # TARGET_SCOPES: Scopes to request in the exchanged token. # github-tool-aud adds "github-tool" to the token's audience claim. TARGET_SCOPES: "openid github-tool-aud github-full-access" - ---- -# spiffe-helper-config ConfigMap - Used by spiffe-helper container (SPIRE mode only) -apiVersion: v1 -kind: ConfigMap -metadata: - name: spiffe-helper-config - namespace: team1 -data: - helper.conf: | - agent_address = "/spiffe-workload-api/spire-agent.sock" - cmd = "" - cmd_args = "" - cert_dir = "/opt" - renew_signal = "" - svid_file_name = "svid.pem" - svid_key_file_name = "svid_key.pem" - svid_bundle_file_name = "svid_bundle.pem" - jwt_svids = [{jwt_audience="kagenti", jwt_svid_file_name="jwt_svid.token"}] - ---- -# envoy-config ConfigMap - Envoy proxy configuration for traffic interception -# This configures Envoy to: -# - Intercept outbound HTTP traffic and send it through ext-proc for token exchange -# - Pass through outbound HTTPS traffic as-is (TLS passthrough) -# - Intercept inbound HTTP traffic and send it through ext-proc for JWT validation -apiVersion: v1 -kind: ConfigMap -metadata: - name: envoy-config - namespace: team1 -data: - envoy.yaml: | - admin: - address: - socket_address: - protocol: TCP - address: 127.0.0.1 - port_value: 9901 - - static_resources: - listeners: - - name: outbound_listener - address: - socket_address: - protocol: TCP - address: 0.0.0.0 - port_value: 15123 - listener_filters: - - name: envoy.filters.listener.original_dst - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.listener.original_dst.v3.OriginalDst - - name: envoy.filters.listener.tls_inspector - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector - filter_chains: - # TLS passthrough: forward HTTPS traffic as-is via TCP proxy - - filter_chain_match: - transport_protocol: tls - filters: - - name: envoy.filters.network.tcp_proxy - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy - stat_prefix: outbound_tls_passthrough - cluster: original_destination - # Plaintext HTTP: inspect and process via ext_proc - - filter_chain_match: - transport_protocol: raw_buffer - filters: - - name: envoy.filters.network.http_connection_manager - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager - stat_prefix: outbound_http - codec_type: AUTO - route_config: - name: outbound_routes - virtual_hosts: - - name: catch_all - domains: ["*"] - routes: - - match: - prefix: "/" - route: - cluster: original_destination - timeout: 300s - http_filters: - - name: envoy.filters.http.ext_proc - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.ext_proc.v3.ExternalProcessor - grpc_service: - envoy_grpc: - cluster_name: ext_proc_cluster - timeout: 300s - processing_mode: - request_header_mode: SEND - response_header_mode: SKIP - request_body_mode: NONE - response_body_mode: NONE - - name: envoy.filters.http.router - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router - - - name: inbound_listener - address: - socket_address: - protocol: TCP - address: 0.0.0.0 - port_value: 15124 - listener_filters: - - name: envoy.filters.listener.original_dst - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.listener.original_dst.v3.OriginalDst - filter_chains: - - filters: - - name: envoy.filters.network.http_connection_manager - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager - stat_prefix: inbound_http - codec_type: AUTO - route_config: - name: inbound_routes - virtual_hosts: - - name: local_app - domains: ["*"] - routes: - - match: - prefix: "/" - route: - cluster: original_destination - timeout: 300s - http_filters: - # Lua filter injects x-authbridge-direction BEFORE ext_proc runs. - # Route-level request_headers_to_add is applied by the router filter - # (last in the chain), so ext_proc would never see it. - - name: envoy.filters.http.lua - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua - inline_code: | - function envoy_on_request(request_handle) - request_handle:headers():add("x-authbridge-direction", "inbound") - end - - name: envoy.filters.http.ext_proc - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.ext_proc.v3.ExternalProcessor - grpc_service: - envoy_grpc: - cluster_name: ext_proc_cluster - timeout: 300s - processing_mode: - request_header_mode: SEND - response_header_mode: SKIP - request_body_mode: NONE - response_body_mode: NONE - - name: envoy.filters.http.router - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router - - clusters: - - name: original_destination - connect_timeout: 30s - type: ORIGINAL_DST - lb_policy: CLUSTER_PROVIDED - original_dst_lb_config: - use_http_header: false - - - name: ext_proc_cluster - connect_timeout: 5s - type: STATIC - lb_policy: ROUND_ROBIN - http2_protocol_options: {} - load_assignment: - cluster_name: ext_proc_cluster - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: 127.0.0.1 - port_value: 9090 From 3f0773bf69bd24171261827b0d42494e9a233fcc Mon Sep 17 00:00:00 2001 From: Mariusz Sabath Date: Thu, 5 Mar 2026 23:04:07 -0500 Subject: [PATCH 5/7] fix: add agent audience scope to UI client in setup_keycloak.py Keycloak only auto-assigns realm default scopes to newly created clients. The UI kagenti client was created during install before the agent audience scope existed, so it does not inherit it. Without this scope, the UI token lacks the agent SPIFFE ID in the audience claim, causing AuthBridge to reject UI chat requests with invalid audience. Explicitly add the agent-ns-sa-aud scope to the kagenti UI client after creating it as a realm default. This is a temporary workaround until the client-registration sidecar handles this automatically (#169). Signed-off-by: Mariusz Sabath Assisted-By: Claude (Anthropic AI) --- .../demos/github-issue/setup_keycloak.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/AuthBridge/demos/github-issue/setup_keycloak.py b/AuthBridge/demos/github-issue/setup_keycloak.py index c4d2a22cc..d67cc695f 100644 --- a/AuthBridge/demos/github-issue/setup_keycloak.py +++ b/AuthBridge/demos/github-issue/setup_keycloak.py @@ -67,6 +67,7 @@ DEFAULT_NAMESPACE = "team1" DEFAULT_SERVICE_ACCOUNT = "git-issue-agent" SPIFFE_TRUST_DOMAIN = "localtest.me" +UI_CLIENT_ID = os.environ.get("UI_CLIENT_ID", "kagenti") DEMO_USERS = [ { @@ -352,6 +353,38 @@ def main(): except Exception as e: print(f"Note: Could not add 'github-full-access' as optional: {e}") + # --------------------------------------------------------------- + # Add agent audience scope to the Kagenti UI client + # --------------------------------------------------------------- + # Keycloak only auto-assigns realm default scopes to NEW clients. + # The UI client was created during install (before this scope existed), + # so we must add it explicitly. Without this, the UI's tokens won't + # include the agent's SPIFFE ID in the audience, and AuthBridge will + # reject UI chat requests with "invalid audience". + # + # TODO: Remove this workaround once the client-registration sidecar + # handles this automatically (kagenti/kagenti-extensions#169). + print(f"\n--- Adding agent audience scope to UI client '{UI_CLIENT_ID}' ---") + ui_client_internal_id = keycloak_admin.get_client_id(UI_CLIENT_ID) + if ui_client_internal_id: + try: + keycloak_admin.add_client_default_client_scope( + ui_client_internal_id, agent_spiffe_scope_id, {} + ) + print( + f"Added '{scope_name}' as default scope on client '{UI_CLIENT_ID}'." + ) + print(" → UI tokens will now include the agent's SPIFFE ID in audience.") + print(" → Users must log out and back in for the new scope to take effect.") + except Exception as e: + print(f"Note: Could not add scope to '{UI_CLIENT_ID}' client: {e}") + else: + print( + f"Warning: UI client '{UI_CLIENT_ID}' not found in realm " + f"'{KEYCLOAK_REALM}'. UI chat with this agent will require " + f"manually adding the '{scope_name}' scope to the UI client." + ) + # --------------------------------------------------------------- # Create demo users # --------------------------------------------------------------- From da6127875fe23537da3f8d1e9063b4370cb16b62 Mon Sep 17 00:00:00 2001 From: Mariusz Sabath Date: Thu, 5 Mar 2026 23:31:49 -0500 Subject: [PATCH 6/7] feat: add client_credentials fallback for outbound requests without Authorization header When an outbound request has no Authorization header, the ext_proc now falls back to a client_credentials grant using the agent's shared credentials (/shared/client-id.txt + /shared/client-secret.txt). This handles agent frameworks that do not propagate the inbound Bearer token to outbound tool calls. The token exchange path (RFC 8693) is still preferred when an Authorization header IS present, as it preserves the end user's identity (sub claim). The client_credentials fallback uses the agent's own identity instead. Closes #170 Signed-off-by: Mariusz Sabath Assisted-By: Claude (Anthropic AI) --- AuthBridge/AuthProxy/go-processor/main.go | 74 ++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/AuthBridge/AuthProxy/go-processor/main.go b/AuthBridge/AuthProxy/go-processor/main.go index d04a3f05e..3b3594920 100644 --- a/AuthBridge/AuthProxy/go-processor/main.go +++ b/AuthBridge/AuthProxy/go-processor/main.go @@ -327,6 +327,52 @@ func exchangeToken(clientID, clientSecret, tokenURL, subjectToken, audience, sco return tokenResp.AccessToken, nil } +// clientCredentialsGrant performs an OAuth 2.0 Client Credentials grant. +// Used as a fallback when no Authorization header is present on outbound requests. +// The agent's identity (client-id/client-secret from client-registration) is used +// to obtain a token scoped to the target audience. +func clientCredentialsGrant(clientID, clientSecret, tokenURL, audience, scopes string) (string, error) { + log.Printf("[Client Credentials] Starting client credentials grant") + log.Printf("[Client Credentials] Token URL: %s", tokenURL) + log.Printf("[Client Credentials] Client ID: %s", clientID) + log.Printf("[Client Credentials] Audience: %s", audience) + log.Printf("[Client Credentials] Scopes: %s", scopes) + + data := url.Values{} + data.Set("client_id", clientID) + data.Set("client_secret", clientSecret) + data.Set("grant_type", "client_credentials") + data.Set("audience", audience) + data.Set("scope", scopes) + + resp, err := http.PostForm(tokenURL, data) + if err != nil { + log.Printf("[Client Credentials] Failed to make request: %v", err) + return "", err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Printf("[Client Credentials] Failed to read response: %v", err) + return "", err + } + + if resp.StatusCode != http.StatusOK { + log.Printf("[Client Credentials] Failed with status %d: %s", resp.StatusCode, string(body)) + return "", status.Errorf(codes.Internal, "client credentials grant failed: %s", string(body)) + } + + var tokenResp tokenExchangeResponse + if err := json.Unmarshal(body, &tokenResp); err != nil { + log.Printf("[Client Credentials] Failed to parse response: %v", err) + return "", err + } + + log.Printf("[Client Credentials] Successfully obtained token") + return tokenResp.AccessToken, nil +} + func getHeaderValue(headers []*core.HeaderValue, key string) string { for _, header := range headers { if strings.EqualFold(header.Key, key) { @@ -495,7 +541,33 @@ func (p *processor) handleOutbound(ctx context.Context, headers *core.HeaderMap) log.Printf("[Token Exchange] Invalid Authorization header format") } } else { - log.Printf("[Token Exchange] No Authorization header found") + // No Authorization header on outbound — fall back to client_credentials. + // This handles agent frameworks that don't propagate the inbound token. + // The token uses the agent's identity rather than the end user's. + log.Printf("[Client Credentials] No Authorization header on outbound, falling back to client_credentials grant") + newToken, err := clientCredentialsGrant(clientID, clientSecret, tokenURL, targetAudience, targetScopes) + if err == nil { + log.Printf("[Client Credentials] Injecting token into outbound request") + return &v3.ProcessingResponse{ + Response: &v3.ProcessingResponse_RequestHeaders{ + RequestHeaders: &v3.HeadersResponse{ + Response: &v3.CommonResponse{ + HeaderMutation: &v3.HeaderMutation{ + SetHeaders: []*core.HeaderValueOption{ + { + Header: &core.HeaderValue{ + Key: "authorization", + RawValue: []byte("Bearer " + newToken), + }, + }, + }, + }, + }, + }, + }, + } + } + log.Printf("[Client Credentials] Failed to obtain token: %v", err) } } else { log.Println("[Token Exchange] Missing configuration, skipping token exchange") From 2beb1d5e156f58e83b910cf423596b8486792650 Mon Sep 17 00:00:00 2001 From: Mariusz Sabath Date: Fri, 6 Mar 2026 12:44:15 -0500 Subject: [PATCH 7/7] fix: return 503 when outbound token acquisition fails When clientCredentialsGrant() or exchangeToken() fails, the ext_proc was falling through and sending the outbound request without any Authorization header. This caused confusing downstream 401 errors far from the root cause. Now return 503 Service Unavailable with a clear error message so callers get an immediate signal that token acquisition failed. Addresses review feedback from @huang195 on PR #171. Assisted-By: Claude (Anthropic AI) Signed-off-by: Mariusz Sabath --- AuthBridge/AuthProxy/go-processor/main.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/AuthBridge/AuthProxy/go-processor/main.go b/AuthBridge/AuthProxy/go-processor/main.go index 3b3594920..f9cdf3671 100644 --- a/AuthBridge/AuthProxy/go-processor/main.go +++ b/AuthBridge/AuthProxy/go-processor/main.go @@ -269,6 +269,21 @@ func denyRequest(message string) *v3.ProcessingResponse { } } +// denyOutboundRequest returns a 503 Service Unavailable when outbound token +// acquisition fails, preventing unauthenticated requests from reaching downstream. +func denyOutboundRequest(message string) *v3.ProcessingResponse { + return &v3.ProcessingResponse{ + Response: &v3.ProcessingResponse_ImmediateResponse{ + ImmediateResponse: &v3.ImmediateResponse{ + Status: &typev3.HttpStatus{ + Code: typev3.StatusCode_ServiceUnavailable, + }, + Body: []byte(fmt.Sprintf(`{"error":"token_acquisition_failed","message":"%s"}`, message)), + }, + }, + } +} + // getHostFromHeaders extracts host from :authority (HTTP/2) or Host header func getHostFromHeaders(headers []*core.HeaderValue) string { if host := getHeaderValue(headers, ":authority"); host != "" { @@ -537,8 +552,10 @@ func (p *processor) handleOutbound(ctx context.Context, headers *core.HeaderMap) } } log.Printf("[Token Exchange] Failed to exchange token: %v", err) + return denyOutboundRequest("token exchange failed") } else { log.Printf("[Token Exchange] Invalid Authorization header format") + return denyOutboundRequest("invalid Authorization header format") } } else { // No Authorization header on outbound — fall back to client_credentials. @@ -568,6 +585,7 @@ func (p *processor) handleOutbound(ctx context.Context, headers *core.HeaderMap) } } log.Printf("[Client Credentials] Failed to obtain token: %v", err) + return denyOutboundRequest("client credentials token acquisition failed") } } else { log.Println("[Token Exchange] Missing configuration, skipping token exchange")