diff --git a/AuthBridge/AuthProxy/go-processor/main.go b/AuthBridge/AuthProxy/go-processor/main.go index d04a3f05e..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 != "" { @@ -327,6 +342,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) { @@ -491,11 +552,40 @@ 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 { - 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) + return denyOutboundRequest("client credentials token acquisition failed") } } else { log.Println("[Token Exchange] Missing configuration, skipping token exchange") diff --git a/AuthBridge/demos/github-issue/demo-manual.md b/AuthBridge/demos/github-issue/demo-manual.md index 7cda60624..40bbde8d0 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 | @@ -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: demo +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://...`). --- @@ -512,7 +507,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 +518,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 +535,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 +576,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 ``` @@ -616,8 +611,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" \ @@ -627,11 +622,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 +643,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 +664,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 +750,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 +759,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 +771,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 @@ -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 `demo`). 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 "demo", 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: demo +# 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 ``` @@ -880,7 +859,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 @@ -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 2243a2851..0485b6b15 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 | @@ -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: demo -``` - --- ## Step 3: Create the GitHub Tool Secrets @@ -453,33 +449,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. @@ -536,8 +530,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" \ @@ -545,11 +539,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 +559,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 +605,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 +618,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 +643,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='[ @@ -688,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 `demo`). 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: demo +# 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 ``` @@ -731,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 ``` @@ -786,7 +779,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` --- @@ -836,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 48619950a..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: "demo" - # 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,18 +20,16 @@ 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: name: authbridge-config namespace: team1 data: - TOKEN_URL: "http://keycloak-service.keycloak.svc:8080/realms/demo/protocol/openid-connect/token" - # ISSUER: Required. Must match the "iss" claim in tokens (Keycloak frontend URL). - ISSUER: "http://keycloak.localtest.me:8080/realms/demo" + TOKEN_URL: "http://keycloak-service.keycloak.svc:8080/realms/kagenti/protocol/openid-connect/token" + 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. @@ -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 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..d67cc695f 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") @@ -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 = [ { @@ -243,7 +244,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, @@ -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 # ---------------------------------------------------------------