diff --git a/AuthBridge/CLAUDE.md b/AuthBridge/CLAUDE.md index 222e5c799..08e4a8508 100644 --- a/AuthBridge/CLAUDE.md +++ b/AuthBridge/CLAUDE.md @@ -67,20 +67,27 @@ The core ext-proc that handles both traffic directions: - Removes `x-authbridge-direction` header before forwarding to app **Outbound path** (no direction header): -- Resolves the destination host against `routes.yaml` (per-host exchange config) -- If the host matches a `passthrough: true` route, forwards unchanged -- If no route matches and `DEFAULT_OUTBOUND_POLICY` is `"passthrough"` (the default), forwards unchanged -- If a route matches with audience/scopes, performs OAuth 2.0 Token Exchange (RFC 8693) -- Replaces the Authorization header with the exchanged token on success +- Default policy is **passthrough** -- outbound requests pass through unchanged unless a route matches +- Uses a **route resolver** to match the request's `Host` header against patterns in `authproxy-routes` ConfigMap +- If a route matches: reads `target_audience` and `token_scopes` from the route, obtains a token via `client_credentials` grant, and injects it as `Authorization: Bearer ` +- If no route matches: applies the default outbound policy (`passthrough` or `exchange`) - Returns 503 if exchange fails for a routed host (prevents unauthenticated calls) +- The `DEFAULT_OUTBOUND_POLICY` env var controls the fallback behavior (default: `passthrough`) + +**Route resolver (outbound):** +- Reads `/etc/authproxy-routes/routes.yaml` (path controlled by `ROUTES_CONFIG_PATH` env var) +- Each route entry has: `host` (glob pattern), `target_audience`, `token_scopes` +- Host matching uses `filepath.Match` semantics (supports `*`, `?`, `[...]` patterns) +- Most commonly, `host` is a plain Kubernetes service name (e.g., `github-tool-mcp`) because the HTTP client sets the Host header from the URL hostname +- Routes file is loaded once at startup; restart the pod to pick up changes **Configuration loading:** - Waits up to 60s for credential files from client-registration (`waitForCredentials`) - Reads `CLIENT_ID` from `/shared/client-id.txt` (file) or `CLIENT_ID` env var (fallback) - Reads `CLIENT_SECRET` from `/shared/client-secret.txt` (file) or `CLIENT_SECRET` env var (fallback) -- Static config from env vars: `TOKEN_URL`, `ISSUER`, `EXPECTED_AUDIENCE`, `TARGET_AUDIENCE`, `TARGET_SCOPES` -- `DEFAULT_OUTBOUND_POLICY` env var: `"passthrough"` (default) or `"exchange"` (legacy) -- Routes from `ROUTES_CONFIG_PATH` (default `/etc/authproxy/routes.yaml`) +- Static config from env vars: `TOKEN_URL`, `ISSUER`, `EXPECTED_AUDIENCE` +- Outbound route config from `ROUTES_CONFIG_PATH` (default `/etc/authproxy-routes/routes.yaml`) +- Default outbound policy from `DEFAULT_OUTBOUND_POLICY` env var: `"passthrough"` (default) or `"exchange"` (legacy) - JWKS URL is derived from TOKEN_URL: replaces `/token` suffix with `/certs` **Key types:** @@ -166,34 +173,27 @@ There are **four** setup scripts for different demo scenarios: When the kagenti-webhook injects sidecars, these ConfigMaps must exist in the target namespace. All required ones are defined in `demos/webhook/k8s/configmaps-webhook.yaml`: -| ConfigMap | Consumer | Required | Key Fields | -|-----------|----------|----------|------------| -| `environments` | client-registration | Yes | `KEYCLOAK_URL`, `KEYCLOAK_REALM`, `KEYCLOAK_ADMIN_USERNAME`, `KEYCLOAK_ADMIN_PASSWORD`, `SPIRE_ENABLED` | -| `authbridge-config` | envoy-proxy (ext-proc) | Yes | `TOKEN_URL`, `ISSUER`, `TARGET_AUDIENCE`, `TARGET_SCOPES`, `DEFAULT_OUTBOUND_POLICY` | -| `authproxy-routes` | envoy-proxy (ext-proc) | No | `routes.yaml` — per-target token exchange routes (host, audience, scopes, passthrough) | -| `spiffe-helper-config` | spiffe-helper | Yes (SPIRE) | `helper.conf` (SPIRE agent address, cert paths, JWT SVID config) | -| `envoy-config` | envoy-proxy | Yes | `envoy.yaml` (full Envoy configuration) | - -### Outbound Token Exchange Policy - -The go-processor defaults to **passthrough** for outbound requests that don't match any route in `authproxy-routes`. This means agents work out-of-the-box with any LLM provider without configuring exclusions. Token exchange only happens for hosts with explicit entries in `authproxy-routes`. +| Resource | Kind | Consumer | Key Fields | +|----------|------|----------|------------| +| `environments` | ConfigMap | client-registration | `KEYCLOAK_URL`, `KEYCLOAK_REALM`, `SPIRE_ENABLED` | +| `keycloak-admin-secret` | Secret | client-registration | `KEYCLOAK_ADMIN_USERNAME`, `KEYCLOAK_ADMIN_PASSWORD` | +| `authbridge-config` | ConfigMap | envoy-proxy (ext-proc) | `TOKEN_URL`, `ISSUER`, `DEFAULT_OUTBOUND_POLICY` (optional) | +| `authproxy-routes` | ConfigMap (optional) | envoy-proxy (ext-proc) | `routes.yaml` with per-host token exchange rules | +| `spiffe-helper-config` | ConfigMap | spiffe-helper | `helper.conf` (SPIRE agent address, cert paths, JWT SVID config) | +| `envoy-config` | ConfigMap | envoy-proxy | `envoy.yaml` (full Envoy configuration) | -To configure per-target exchange, create an `authproxy-routes` ConfigMap: +**`authproxy-routes` format** (`routes.yaml`): ```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: authproxy-routes -data: - routes.yaml: | - - host: "target-service.team1.svc.cluster.local" - target_audience: "target-client-id" - token_scopes: "openid target-aud" - - host: "otel-collector.*.svc.cluster.local" - passthrough: true +routes: + - host: "github-tool-mcp" + target_audience: "github-tool" + token_scopes: "openid github-tool-aud github-full-access" + - host: "auth-target-*" + target_audience: "auth-target" + token_scopes: "openid auth-target-aud" ``` -Set `DEFAULT_OUTBOUND_POLICY: "exchange"` in `authbridge-config` to restore legacy behavior (exchange all outbound traffic). +The go-processor defaults to **passthrough** for outbound requests that don't match any route. Token exchange only happens for hosts with explicit entries in `authproxy-routes`. Set `DEFAULT_OUTBOUND_POLICY: "exchange"` in `authbridge-config` to restore legacy behavior. ## Shared Volume Contract @@ -330,6 +330,12 @@ kubectl apply -f k8s/auth-target-deployment-webhook.yaml # Target service 9. **iptables backend auto-detection**: `init-iptables.sh` auto-detects `iptables-legacy` vs `iptables-nft`. Override with `IPTABLES_CMD` env var if needed. Always verify with proxy-init logs after deployment. +10. **Route host patterns must match HTTP Host header**: The `host` field in `authproxy-routes` is matched against the HTTP `Host` header, which is set by the HTTP client from the URL hostname. For in-cluster calls, this is the **short Kubernetes service name** from `MCP_URL` (e.g., `github-tool-mcp`), not the FQDN. Using the wrong pattern (e.g., `*.github-issue-tool*.svc.cluster.local`) will silently fall through to the default passthrough policy. + +11. **Keycloak scope assignment for dynamically registered clients**: When `client-registration` auto-registers an agent as a Keycloak client, the client may not inherit all necessary scopes. The agent's own audience scope (e.g., `agent-team1-git-issue-agent-aud`) must be a **default** client scope for inbound JWT audience validation to work. Token exchange scopes (e.g., `github-tool-aud`, `github-full-access`) must be **optional** client scopes for `client_credentials` grants with explicit `scope=` to succeed. Re-run the demo's `setup_keycloak.py` after the agent is deployed to assign these scopes to the registered client. + +12. **Outbound passthrough is the safe default**: The `DEFAULT_OUTBOUND_POLICY` defaults to `passthrough`, which means outbound traffic to LLM inference endpoints (e.g., Ollama via `host.docker.internal`) passes through without token exchange. If this were set to `exchange`, all outbound HTTP calls would attempt token exchange and fail for non-Keycloak destinations. + ## DCO Sign-Off (Mandatory) All commits **must** include a `Signed-off-by` trailer (Developer Certificate of Origin). diff --git a/CLAUDE.md b/CLAUDE.md index dfc95ecf5..9dea9070e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -196,24 +196,20 @@ Hooks: | SPIRE | Optional | SPIFFE identity (JWT-SVIDs) for workloads | | Prometheus | Optional | Metrics collection (ServiceMonitor) | -## ConfigMaps Expected at Runtime +## ConfigMaps and Secrets Expected at Runtime -When the webhook injects sidecars, the target namespace needs these ConfigMaps: +When the webhook injects sidecars, the target namespace needs these resources: | Resource | Kind | Used by | Keys | |----------|------|---------|------| | `environments` | ConfigMap | client-registration | `KEYCLOAK_URL`, `KEYCLOAK_REALM` | | `keycloak-admin-secret` | Secret | client-registration | `KEYCLOAK_ADMIN_USERNAME`, `KEYCLOAK_ADMIN_PASSWORD` | -| `authbridge-config` | ConfigMap | envoy-proxy (ext-proc) | `TOKEN_URL`, `ISSUER`, `TARGET_AUDIENCE`, `TARGET_SCOPES`, `DEFAULT_OUTBOUND_POLICY` | -| `authproxy-routes` | ConfigMap (optional) | envoy-proxy (ext-proc) | `routes.yaml` — per-target token exchange routes | +| `authbridge-config` | ConfigMap | envoy-proxy (ext-proc) | `TOKEN_URL`, `ISSUER`, `DEFAULT_OUTBOUND_POLICY` (optional, defaults to `passthrough`) | +| `authproxy-routes` | ConfigMap (optional) | envoy-proxy (ext-proc) | `routes.yaml` -- per-host token exchange rules (see AuthBridge/CLAUDE.md for format) | | `spiffe-helper-config` | ConfigMap | spiffe-helper | SPIFFE helper configuration file | | `envoy-config` | ConfigMap | envoy-proxy | Envoy YAML configuration | -### Outbound Token Exchange Policy - -The go-processor defaults to **passthrough** for outbound requests that don't match any route in `authproxy-routes`. This means agents work out-of-the-box with any LLM provider (Ollama, OpenAI, etc.) without needing token exchange exclusions. Only hosts with explicit route entries in `authproxy-routes` get token exchange. - -Set `DEFAULT_OUTBOUND_POLICY: "exchange"` in `authbridge-config` to restore the legacy behavior (exchange for all outbound traffic when global config is set). +**Note:** `authproxy-routes` is optional. Without it, all outbound traffic passes through unchanged (the default policy is `passthrough`). Only create it when the agent needs to call services that require token exchange. Set `DEFAULT_OUTBOUND_POLICY: "exchange"` in `authbridge-config` to restore the legacy behavior. ## Common Development Tasks @@ -306,6 +302,10 @@ AUTHBRIDGE_DEMO=true ./scripts/webhook-rollout.sh 6. **Envoy config not embedded:** The envoy-proxy sidecar mounts `envoy-config` ConfigMap at `/etc/envoy`. This ConfigMap must exist in the target namespace before workloads are created. +7. **Outbound policy is passthrough by default:** The go-processor defaults to passing outbound traffic through unchanged. Token exchange only happens for hosts explicitly listed in the `authproxy-routes` ConfigMap. Setting `TARGET_AUDIENCE`/`TARGET_SCOPES` in `authbridge-config` alone does nothing unless a route references them (or the default policy is changed to `exchange`). + +8. **Route host patterns use short service names:** The `host` field in `authproxy-routes` matches against the HTTP `Host` header, which is typically just the short Kubernetes service name (e.g., `github-tool-mcp`), not the FQDN. Glob patterns (`*`) are supported but the most common case is a plain service name. + ## DCO Sign-Off (Mandatory) All commits **must** include a `Signed-off-by` trailer (Developer Certificate of Origin).