Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 38 additions & 32 deletions AuthBridge/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <token>`
- 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:**
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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).
Expand Down
18 changes: 9 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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).
Expand Down
Loading