diff --git a/AuthBridge/CLAUDE.md b/AuthBridge/CLAUDE.md new file mode 100644 index 000000000..dd6d15400 --- /dev/null +++ b/AuthBridge/CLAUDE.md @@ -0,0 +1,320 @@ +# CLAUDE.md - AuthBridge + +This file provides context for Claude (AI assistant) when working with the `AuthBridge` codebase. +For the full monorepo context (webhook, CI/CD, Helm, cross-component relationships), see [`../CLAUDE.md`](../CLAUDE.md). +For the webhook internals, see [`../kagenti-webhook/CLAUDE.md`](../kagenti-webhook/CLAUDE.md). + +## What AuthBridge Does + +AuthBridge provides **zero-trust, transparent token management** for Kubernetes workloads. It combines three capabilities: + +1. **Automatic Identity** -- Workloads obtain SPIFFE IDs from SPIRE and auto-register as Keycloak clients +2. **Inbound JWT Validation** -- Incoming requests are validated (signature, issuer, audience) by an Envoy ext-proc +3. **Outbound Token Exchange** -- Outgoing requests get their tokens automatically exchanged for the correct target audience (OAuth 2.0 RFC 8693) + +All of this happens transparently via sidecar injection -- no application code changes required. + +## Directory Structure + +``` +AuthBridge/ +├── AuthProxy/ # Envoy + ext-proc sidecar (Go) +│ ├── go-processor/ +│ │ └── main.go # gRPC ext-proc: inbound validation + outbound token exchange +│ ├── main.go # Example pass-through proxy app (NOT a core component) +│ ├── init-iptables.sh # iptables setup (outbound + inbound, Istio ambient compatible) +│ ├── entrypoint-envoy.sh # Starts go-processor + Envoy +│ ├── Dockerfile # auth-proxy example app image +│ ├── Dockerfile.envoy # envoy-with-processor (Envoy 1.28 + go-processor) +│ ├── Dockerfile.init # proxy-init (Alpine + iptables) +│ ├── Makefile # Build/deploy targets for quickstart +│ ├── go.mod # Go module (github.com/kagenti/kagenti-extensions/AuthBridge/AuthProxy) +│ ├── k8s/ # Standalone K8s manifests for AuthProxy +│ │ ├── auth-proxy-deployment.yaml +│ │ └── go-processor-deployment.yaml +│ └── quickstart/ # Standalone quickstart (no SPIFFE) +│ ├── README.md # Step-by-step tutorial +│ ├── setup_keycloak.py # Creates Keycloak clients/scopes for quickstart demo +│ ├── requirements.txt # python-keycloak==5.3.1 +│ ├── demo-app/ +│ │ ├── main.go # Target service: JWT validation on :8081, TLS echo on :8443 +│ │ └── Dockerfile +│ └── k8s/ +│ └── demo-app-deployment.yaml +│ +├── client-registration/ # Keycloak auto-registration (Python) +│ ├── client_registration.py # Main script: register client, write secret +│ ├── Dockerfile # Python 3.12-slim, UID/GID 1000 +│ ├── requirements.txt # python-keycloak==5.3.1, pyjwt==2.10.1 +│ ├── README.md # Detailed docs with SPIFFE/non-SPIFFE examples +│ ├── example_deployment.yaml # Example without SPIFFE +│ └── example_deployment_spiffe.yaml# Example with SPIFFE +│ +├── k8s/ # Full AuthBridge demo manifests (with webhook) +│ ├── authbridge-deployment.yaml # Full demo with SPIFFE +│ ├── authbridge-deployment-no-spiffe.yaml # Full demo without SPIFFE +│ ├── agent-deployment-webhook.yaml # Agent deployment (webhook injects sidecars) +│ ├── agent-deployment-webhook-no-spiffe.yaml +│ ├── auth-target-deployment-webhook.yaml # Target service deployment +│ └── configmaps-webhook.yaml # All 4 required ConfigMaps for webhook injection +│ +├── setup_keycloak.py # Keycloak setup for full demo (SPIFFE flow) +├── setup_keycloak-webhook.py # Keycloak setup for webhook-injected deployments +├── demo.md # Full demo walkthrough (manual + SPIFFE) +├── demo-webhook.md # Demo walkthrough for webhook-based injection +├── README.md # AuthBridge overview and architecture +└── requirements.txt # python-keycloak==5.3.1 +``` + +## Component Details + +### AuthProxy (go-processor/main.go) + +The core ext-proc that handles both traffic directions: + +**Inbound path** (`x-authbridge-direction: inbound`): +- Validates JWT signature via JWKS (auto-refreshing cache from `TOKEN_URL`-derived JWKS endpoint) +- Validates issuer claim against `ISSUER` env var +- Optionally validates audience against `EXPECTED_AUDIENCE` env var +- Returns 401 with JSON error body for invalid/missing tokens +- Removes `x-authbridge-direction` header before forwarding to app + +**Outbound path** (no direction header): +- Reads `Authorization: Bearer ` from request +- Performs OAuth 2.0 Token Exchange (RFC 8693) against Keycloak +- Replaces Authorization header with the exchanged token +- If config is incomplete or exchange fails, passes request through unchanged + +**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` +- JWKS URL is derived from TOKEN_URL: replaces `/token` suffix with `/certs` + +**Key types:** +- `Config` struct -- holds client credentials and token exchange params (thread-safe via `sync.RWMutex`) +- `processor` struct -- implements `ExternalProcessorServer` gRPC interface +- `tokenExchangeResponse` -- JSON response from Keycloak token endpoint + +### init-iptables.sh + +Extensively documented shell script that sets up iptables for transparent traffic interception. Key features: + +- **Outbound**: `PROXY_OUTPUT` chain in `nat OUTPUT`, redirects to Envoy port 15123 +- **Inbound**: `PROXY_INBOUND` chain in `nat PREROUTING`, redirects to Envoy port 15124 +- **Istio ambient mesh coexistence**: Handles ztunnel fwmark (0x539), HBONE port (15008), route_localnet sysctl +- **Exclusions**: SSH (22), loopback, configurable `OUTBOUND_PORTS_EXCLUDE` and `INBOUND_PORTS_EXCLUDE` +- **Envoy UID 1337**: Excluded from outbound redirect to prevent loops +- **Mangle rule**: Sets fwmark on Envoy's local delivery to prevent ISTIO_OUTPUT redirect loop +- Uses `-I 1` (insert first) for chain ordering stability with Istio CNI + +**Environment variables:** +| Variable | Default | Description | +|----------|---------|-------------| +| `PROXY_PORT` | 15123 | Envoy outbound listener | +| `INBOUND_PROXY_PORT` | 15124 | Envoy inbound listener | +| `PROXY_UID` | 1337 | Envoy process UID (excluded from redirect) | +| `OUTBOUND_PORTS_EXCLUDE` | (empty) | Comma-separated ports to exclude | +| `INBOUND_PORTS_EXCLUDE` | (empty) | Comma-separated ports to exclude | + +### client_registration.py + +Idempotent Python script that: +1. Reads SPIFFE ID from `/opt/jwt_svid.token` JWT `sub` claim (if `SPIRE_ENABLED=true`) +2. Falls back to `CLIENT_NAME` env var as client ID (if SPIRE disabled) +3. Creates or reuses a Keycloak client with token exchange enabled +4. Retrieves the client secret and writes to `SECRET_FILE_PATH` (in cluster deployments, the webhook sets `SECRET_FILE_PATH=/shared/client-secret.txt` to match the shared-volume contract) + +**Keycloak client configuration created:** +- `publicClient: False` (confidential/authenticated) +- `serviceAccountsEnabled: True` (allows `client_credentials` grant) +- `standardFlowEnabled: True` +- `directAccessGrantsEnabled: True` +- `standard.token.exchange.enabled: True` + +**Dependencies:** `python-keycloak==5.3.1`, `pyjwt==2.10.1` + +### Envoy Configuration (configmaps-webhook.yaml) + +The `envoy-config` ConfigMap contains the full Envoy YAML with: + +**Listeners:** +- `outbound_listener` (port 15123): + - `tls_inspector` + `original_dst` listener filters + - TLS filter chain: `tcp_proxy` passthrough to `original_destination` cluster + - Raw buffer filter chain: `http_connection_manager` with ext_proc + router +- `inbound_listener` (port 15124): + - `original_dst` listener filter + - Injects `x-authbridge-direction: inbound` request header + - `http_connection_manager` with ext_proc + router + +**Clusters:** +- `original_destination` -- ORIGINAL_DST type (routes to original IP/port) +- `ext_proc_cluster` -- STATIC, points to localhost:9090 (go-processor), HTTP/2 + +### Demo App (quickstart/demo-app/main.go) + +A test target service with two servers: +- HTTP on `:8081` -- Validates JWT (issuer, audience, signature via JWKS) +- HTTPS on `:8443` -- Simple echo (`tls-ok`), no JWT validation, self-signed cert + +Used for testing both the token exchange (HTTP) and TLS passthrough (HTTPS) paths. + +## Keycloak Setup Scripts + +There are **three** setup scripts for different scenarios: + +| Script | Location | Use Case | +|--------|----------|----------| +| `setup_keycloak.py` | `AuthBridge/` | Full SPIFFE demo (creates realm, auth-target client, agent-spiffe-aud scope, alice user) | +| `setup_keycloak-webhook.py` | `AuthBridge/` | Webhook-injected deployments (parameterized namespace/SA) | +| `setup_keycloak.py` | `AuthBridge/AuthProxy/quickstart/` | Standalone AuthProxy quickstart (creates application-caller, authproxy, demoapp clients) | + +**Common Keycloak defaults across all scripts:** +- URL: `http://keycloak.localtest.me:8080` +- Realm: `demo` +- Admin: `admin` / `admin` + +## Required ConfigMaps for Webhook Injection + +When the kagenti-webhook injects sidecars, four ConfigMaps must exist in the target namespace. All are defined in `k8s/configmaps-webhook.yaml`: + +| ConfigMap | Consumer | Key Fields | +|-----------|----------|------------| +| `environments` | client-registration | `KEYCLOAK_URL`, `KEYCLOAK_REALM`, `KEYCLOAK_ADMIN_USERNAME`, `KEYCLOAK_ADMIN_PASSWORD`, `SPIRE_ENABLED` | +| `authbridge-config` | envoy-proxy (ext-proc) | `TOKEN_URL`, `ISSUER`, `TARGET_AUDIENCE`, `TARGET_SCOPES` | +| `spiffe-helper-config` | spiffe-helper | `helper.conf` (SPIRE agent address, cert paths, JWT SVID config) | +| `envoy-config` | envoy-proxy | `envoy.yaml` (full Envoy configuration) | + +## Shared Volume Contract + +Sidecars communicate through files on shared volumes: + +| Path | Writer | Reader | Content | +|------|--------|--------|---------| +| `/opt/jwt_svid.token` | spiffe-helper | client-registration | JWT SVID from SPIRE | +| `/shared/client-id.txt` | client-registration | envoy-proxy (ext-proc) | SPIFFE ID or CLIENT_NAME | +| `/shared/client-secret.txt` | client-registration | envoy-proxy (ext-proc) | Keycloak client secret | + +## Build and Deploy + +### AuthProxy (standalone quickstart, no webhook) + +```bash +cd AuthBridge/AuthProxy + +# Build all images (auth-proxy, demo-app, proxy-init, envoy-with-processor) +make build-images + +# Load into Kind cluster +make load-images # Uses KIND_CLUSTER_NAME env var (default: kagenti) + +# Deploy auth-proxy + demo-app +make deploy + +# Clean up +make undeploy +``` + +### Full Demo with Webhook + +```bash +# 1. Setup Keycloak (requires port-forward to Keycloak) +cd AuthBridge +pip install -r requirements.txt +python setup_keycloak.py # For SPIFFE demo +# or +python setup_keycloak-webhook.py # For webhook-injected demo + +# 2. Apply ConfigMaps to target namespace +kubectl apply -f k8s/configmaps-webhook.yaml -n + +# 3. Deploy workloads (webhook auto-injects sidecars) +kubectl apply -f k8s/authbridge-deployment.yaml # With SPIFFE +# or +kubectl apply -f k8s/authbridge-deployment-no-spiffe.yaml # Without SPIFFE +``` + +## Important Port Mapping + +| Port | Component | Protocol | Purpose | +|------|-----------|----------|---------| +| 15123 | Envoy | TCP | Outbound listener (iptables redirects app traffic here) | +| 15124 | Envoy | TCP | Inbound listener (iptables redirects incoming traffic here) | +| 9090 | go-processor | gRPC | Ext-proc server (called by Envoy) | +| 9901 | Envoy | HTTP | Admin interface (bound to 127.0.0.1) | +| 8080 | auth-proxy | HTTP | Example app (NOT part of sidecar) | +| 8081 | demo-app | HTTP | Demo target (JWT validation) | +| 8443 | demo-app | HTTPS | Demo target (TLS echo, no JWT) | + +## Code Conventions + +### Go (AuthProxy, go-processor, demo-app) +- Go 1.23 (module: `github.com/kagenti/kagenti-extensions/AuthBridge/AuthProxy`) +- Logging with `log.Printf` (stdlib), prefixed by `[Config]`, `[Token Exchange]`, `[Inbound]`, `[JWT Debug]` +- Thread-safe config via `sync.RWMutex` in the `Config` struct +- gRPC ext-proc using `envoyproxy/go-control-plane` types +- JWT validation with `lestrrat-go/jwx/v2` + +### Python (client-registration, setup scripts) +- Python 3.12 syntax (type hints: `str | None`) +- `python-keycloak` library for all Keycloak admin API calls +- `PyJWT` for JWT decoding (signature verification disabled -- uses `verify_signature: False`) +- Idempotent: all `get_or_create_*` helper functions check existence before creating +- UID/GID 1000 in Dockerfile **must match** `ClientRegistrationUID`/`ClientRegistrationGID` in `kagenti-webhook/internal/webhook/injector/container_builder.go` + +### Shell (init-iptables.sh) +- `set -e` (exit on error) +- Extensive inline documentation explaining iptables chain ordering, Istio interactions, and debugging tips +- Idempotent: uses `iptables -N ... 2>/dev/null || true` and `iptables -F` before adding rules + +## Common Tasks for Code Changes + +### Modifying Token Exchange Logic +- Edit `go-processor/main.go`, function `exchangeToken()` +- The token exchange POST parameters follow RFC 8693 exactly +- Test by rebuilding: `make docker-build-envoy && make load-images` + +### Modifying Inbound JWT Validation +- Edit `go-processor/main.go`, functions `validateInboundJWT()` and `handleInbound()` +- JWKS cache is initialized in `initJWKSCache()` and auto-refreshes +- Direction detection: `x-authbridge-direction: inbound` header (injected by Envoy inbound listener config) + +### Adding New iptables Rules +- Edit `init-iptables.sh` +- Follow the existing pattern: document the rule's purpose, Istio interaction, and chain ordering +- Test with and without Istio ambient mesh if possible +- Rebuild: `make docker-build-init && make load-images` + +### Modifying Client Registration +- Edit `client-registration/client_registration.py` +- The `register_client()` function is idempotent +- Keycloak client payload is the main configuration point +- Test: `kubectl delete pod -n ` to trigger re-registration + +### Adding New Keycloak Resources to Setup +- Edit the appropriate `setup_keycloak*.py` script +- Use the `get_or_create_*` helper pattern for idempotency +- All scripts use `python-keycloak` library (KeycloakAdmin class) + +### Changing Envoy Configuration +- Edit the `envoy.yaml` section in `k8s/configmaps-webhook.yaml` +- Key listener/cluster names: `outbound_listener`, `inbound_listener`, `original_destination`, `ext_proc_cluster` +- After changes, re-apply the ConfigMap and restart pods + +## Gotchas and Known Issues + +1. **Credential file race condition**: The ext-proc waits up to 60s for `/shared/client-id.txt` and `/shared/client-secret.txt`. If client-registration takes longer (e.g., Keycloak slow to start), the ext-proc will fall back to env vars which may be empty. + +2. **ISSUER vs TOKEN_URL**: `ISSUER` must be the Keycloak **frontend URL** (what appears in the `iss` claim of tokens), while `TOKEN_URL` is the **internal service URL**. These are often different in Kubernetes (e.g., `http://keycloak.localtest.me:8080` vs `http://keycloak-service.keycloak.svc:8080`). + +3. **Keycloak port exclusion**: When using iptables interception, Keycloak's port (8080) must be excluded from redirect via `OUTBOUND_PORTS_EXCLUDE=8080`. Otherwise, token exchange requests from the ext-proc get redirected back to Envoy, creating a loop. + +4. **TLS passthrough is one-way**: Outbound HTTPS traffic passes through Envoy without token exchange. There is no mechanism to exchange tokens for HTTPS destinations. Only HTTP outbound traffic gets token exchange. + +5. **Virtualenv directory**: For local development you may create `AuthProxy/quickstart/venv/`, but it should be gitignored and is not committed to the repo. + +6. **Demo SPIFFE ID is hardcoded**: `setup_keycloak.py` hardcodes `AGENT_SPIFFE_ID = "spiffe://localtest.me/ns/authbridge/sa/agent"`. Change this if using a different namespace/SA. + +7. **Admin credentials in ConfigMap**: `configmaps-webhook.yaml` stores Keycloak admin credentials in a ConfigMap (not a Secret). This is for demo only -- production should use Kubernetes Secrets. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..c8fa2d1af --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,322 @@ +# CLAUDE.md - Kagenti Extensions + +This file provides context for Claude (AI assistant) when working with the `kagenti-extensions` monorepo. + +## Repository Overview + +**kagenti-extensions** is a monorepo containing Kubernetes security extensions for the [Kagenti](https://github.com/kagenti/kagenti) ecosystem. It provides **zero-trust authentication** for Kubernetes workloads through automatic sidecar injection, transparent token exchange, and dynamic Keycloak client registration using SPIFFE/SPIRE identities. + +**GitHub:** `github.com/kagenti/kagenti-extensions` +**Container registry:** `ghcr.io/kagenti/kagenti-extensions/` +**License:** Apache 2.0 + +## Top-Level Directory Structure + +``` +kagenti-extensions/ +├── kagenti-webhook/ # Kubernetes admission webhook (Go, Kubebuilder) +├── AuthBridge/ # Authentication bridge components +│ ├── AuthProxy/ # Envoy + ext-proc sidecar (Go) — token validation & exchange +│ │ ├── go-processor/ # gRPC ext-proc server (inbound JWT validation, outbound token exchange) +│ │ └── quickstart/ # Standalone demo (Keycloak setup script, demo-app) +│ ├── client-registration/ # Keycloak auto-registration (Python) +│ ├── k8s/ # Example K8s deployment manifests +│ └── spiffe-token-helper/ # SPIFFE helper utilities +├── charts/ +│ └── kagenti-webhook/ # Helm chart for the webhook +├── .github/ +│ ├── workflows/ # CI/CD (ci.yaml, build.yaml, goreleaser.yml, pr-verifier.yaml, spellcheck) +│ └── ISSUE_TEMPLATE/ # Bug report, feature request, epic templates +├── .goreleaser.yaml # GoReleaser config (webhook binary + ko image + Helm chart) +├── .pre-commit-config.yaml # Pre-commit hooks (trailing whitespace, go fmt/vet, helmlint) +└── CLAUDE.md # This file +``` + +## The Three Major Components + +### 1. kagenti-webhook (Go / Kubebuilder) + +A Kubernetes **mutating admission webhook** that intercepts workload creation (Deployments, StatefulSets, DaemonSets, Jobs, CronJobs) and automatically injects AuthBridge sidecar containers. + +**Location:** `kagenti-webhook/` +**Language:** Go 1.24, controller-runtime v0.22, Kubebuilder v4 +**Detailed guide:** [`kagenti-webhook/CLAUDE.md`](kagenti-webhook/CLAUDE.md) + +**Key facts:** +- Three registered webhooks: **AuthBridge** (recommended), MCPServer (deprecated), Agent (deprecated) +- AuthBridge webhook path: `/mutate-workloads-authbridge` +- Injection controlled via pod labels (`kagenti.io/type`, `kagenti.io/inject`, `kagenti.io/spire`) and namespace labels (`kagenti-enabled: "true"`) +- Shared `PodMutator` instance across all webhooks (in `internal/webhook/injector/`) +- **AuthBridge path** injects: `proxy-init` (init), `envoy-proxy`, `spiffe-helper` (gated by `kagenti.io/spire` label), `kagenti-client-registration` (gated by `--enable-client-registration` flag) +- **Legacy path** (deprecated MCPServer/Agent) injects: `spiffe-helper`, `kagenti-client-registration`, `envoy-proxy` (no `proxy-init` — legacy does not call `InjectInitContainers`) +- Build: `cd kagenti-webhook && make build` / `make test` / `make docker-build` +- Local dev: `cd kagenti-webhook && make local-dev CLUSTER=` + +### 2. AuthProxy (Go) + +An **Envoy proxy with a gRPC external processor** that provides transparent traffic interception for both inbound JWT validation and outbound OAuth 2.0 token exchange (RFC 8693). + +**Location:** `AuthBridge/AuthProxy/` +**Language:** Go 1.23 (ext-proc processor) +**Detailed guide:** [`AuthBridge/CLAUDE.md`](AuthBridge/CLAUDE.md) +**Key files:** +- `go-processor/main.go` — The ext-proc gRPC server. Handles: + - **Inbound**: Validates JWT (signature via JWKS, issuer, optional audience) + - **Outbound HTTP**: Performs token exchange, replaces Authorization header + - **Outbound HTTPS**: TLS passthrough (no interception) + - Direction detected via `x-authbridge-direction` header injected by Envoy +- `main.go` — Example pass-through proxy application (not a core component) +- `init-iptables.sh` — iptables setup for transparent traffic interception. Extensively documented for Istio ambient mesh coexistence. +- `entrypoint-envoy.sh` — Starts go-processor in background, then Envoy in foreground +- `Dockerfile` — auth-proxy example app image +- `Dockerfile.envoy` — Envoy + go-processor combined image (`envoy-with-processor`) +- `Dockerfile.init` — proxy-init (Alpine + iptables) + +**Build:** `cd AuthBridge/AuthProxy && make build-images` +**Deploy:** `make load-images && make deploy` + +**Important ports:** +| Port | Component | Purpose | +|------|-----------|---------| +| 15123 | Envoy | Outbound listener (iptables redirects here) | +| 15124 | Envoy | Inbound listener (iptables redirects here) | +| 9090 | go-processor | gRPC ext-proc server | +| 9901 | Envoy | Admin interface | +| 8080 | auth-proxy | Example application (not part of sidecar) | + +### 3. Client Registration (Python) + +A Python script that **automatically registers Kubernetes workloads as Keycloak OAuth2 clients** using their SPIFFE identity. + +**Location:** `AuthBridge/client-registration/` +**Language:** Python 3.12 +**Detailed guide:** [`AuthBridge/CLAUDE.md`](AuthBridge/CLAUDE.md) +**Key files:** +- `client_registration.py` — Main script. Idempotent: creates client if not exists, always retrieves and writes secret. +- `Dockerfile` — Python 3.12-slim, runs as UID/GID 1000 (must match `ClientRegistrationUID`/`ClientRegistrationGID` in webhook's `container_builder.go`) +- `requirements.txt` — `python-keycloak`, `PyJWT` + +**Flow:** +1. Waits for `/opt/jwt_svid.token` (from spiffe-helper) if SPIRE enabled +2. Extracts SPIFFE ID from JWT `sub` claim (or uses `CLIENT_NAME` env var) +3. Registers client in Keycloak via admin API +4. Writes client secret to a shared file: + - By default, to `/shared/secret.txt` + - When run via the webhook, `SECRET_FILE_PATH` is set so the secret is written to `/shared/client-secret.txt` + +## How the Components Work Together + +``` + Workload Creation + │ + ▼ + ┌─────────────────────┐ + │ kagenti-webhook │ Intercepts CREATE/UPDATE + │ (admission webhook)│ via MutatingWebhookConfiguration + └──────────┬──────────┘ + │ Injects sidecars + ▼ + ┌────────────────────────────────────┐ + │ WORKLOAD POD │ + │ │ + │ proxy-init (init) ─► iptables │ + │ │ + │ spiffe-helper ──► SPIRE Agent │ + │ │ writes JWT SVID │ + │ ▼ │ + │ client-registration ──► Keycloak │ + │ │ writes client secret │ + │ ▼ │ + │ envoy-proxy (+ go-processor) │ + │ - Inbound: JWT validation │ + │ - Outbound: token exchange │ + │ │ │ + │ Your Application │ + └────────────────────────────────────┘ +``` + +## CI/CD Workflows + +| Workflow | Trigger | Purpose | +|----------|---------|---------| +| `ci.yaml` | PR to main/release-* | Go fmt, vet, build across all Go modules | +| `build.yaml` | Tag push (`v*`) or manual | Multi-arch Docker builds for: client-registration, auth-proxy, proxy-init, envoy-with-processor, demo-app | +| `goreleaser.yml` | Tag push (`v*`) | GoReleaser binary + ko image for webhook, Helm chart package + push | +| `pr-verifier.yaml` | PR to main/release-* | Semantic PR title validation (conventional commits) | +| `spellcheck_action.yml` | PR | Spellcheck on markdown files | + +### PR Title Convention + +PRs must follow **conventional commits** format: + +``` +: +``` + +Types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, `revert` + +## Container Images + +All images are pushed to `ghcr.io/kagenti/kagenti-extensions/`: + +| Image | Source | Description | +|-------|--------|-------------| +| `kagenti-webhook` | `kagenti-webhook/Dockerfile` | Admission webhook manager (Go binary in distroless) | +| `envoy-with-processor` | `AuthBridge/AuthProxy/Dockerfile.envoy` | Envoy 1.28 + go-processor ext-proc | +| `proxy-init` | `AuthBridge/AuthProxy/Dockerfile.init` | Alpine + iptables init container | +| `client-registration` | `AuthBridge/client-registration/Dockerfile` | Python Keycloak client registrar | +| `auth-proxy` | `AuthBridge/AuthProxy/Dockerfile` | Example pass-through proxy (for demos) | +| `demo-app` | `AuthBridge/AuthProxy/quickstart/demo-app/Dockerfile` | Demo target service | + +## Helm Chart + +**Location:** `charts/kagenti-webhook/` +**Published to:** `oci://ghcr.io/kagenti/kagenti-extensions/kagenti-webhook-chart` + +Key values: +- `image.repository` / `image.tag` — Webhook manager image (tag is `__PLACEHOLDER__` in source, replaced at release time) +- `webhook.enableClientRegistration` — Controls `--enable-client-registration` flag +- `certManager.enabled` — Uses cert-manager for webhook TLS certificates +- Templates include: Deployment, Service, ServiceAccount, RBAC, CertManager Certificate/Issuer, MutatingWebhookConfigurations (authbridge, mcpserver, agent) + +Install: +```bash +helm install kagenti-webhook oci://ghcr.io/kagenti/kagenti-extensions/kagenti-webhook-chart \ + --version \ + --namespace kagenti-webhook-system \ + --create-namespace +``` + +## Pre-commit Hooks + +Install: `pre-commit install` + +Hooks: +- `trailing-whitespace`, `end-of-file-fixer`, `check-added-large-files` (max 1024KB), `check-yaml`, `check-json`, `check-merge-conflict`, `mixed-line-ending` +- `helmlint` — Runs on `charts/` directory +- `go-fmt`, `go-vet`, `go-mod-tidy` — Runs on `kagenti-webhook/` Go files + +## Languages and Tech Stack + +| Area | Technology | +|------|------------| +| Webhook | Go 1.24, controller-runtime v0.22, Kubebuilder v4 | +| AuthProxy ext-proc | Go 1.23, envoy-control-plane, lestrrat-go/jwx | +| Client Registration | Python 3.12, python-keycloak, PyJWT | +| Proxy | Envoy 1.28 | +| Traffic interception | iptables (via init container) | +| Identity | SPIFFE/SPIRE (JWT-SVIDs) | +| Auth provider | Keycloak (OAuth2/OIDC, token exchange RFC 8693) | +| Packaging | Docker, ko, GoReleaser, Helm 3 | +| Testing | Ginkgo/Gomega (Go), envtest (controller-runtime) | +| CI | GitHub Actions | + +## External Dependencies and Services + +| Service | Required | Purpose | +|---------|----------|---------| +| Kubernetes | Yes | Target platform (v1.25+ recommended) | +| cert-manager | Yes (for webhook) | TLS certificates for webhook server | +| Keycloak | Yes (for AuthBridge) | OAuth2/OIDC provider, token exchange | +| SPIRE | Optional | SPIFFE identity (JWT-SVIDs) for workloads | +| Prometheus | Optional | Metrics collection (ServiceMonitor) | + +## ConfigMaps Expected at Runtime + +When the webhook injects sidecars, the target namespace needs these ConfigMaps: + +| ConfigMap | Used by | Keys | +|-----------|---------|------| +| `environments` | client-registration | `KEYCLOAK_URL`, `KEYCLOAK_REALM`, `KEYCLOAK_ADMIN_USERNAME`, `KEYCLOAK_ADMIN_PASSWORD` | +| `authbridge-config` | envoy-proxy (ext-proc) | `TOKEN_URL`, `ISSUER`, `TARGET_AUDIENCE`, `TARGET_SCOPES` | +| `spiffe-helper-config` | spiffe-helper | SPIFFE helper configuration file | +| `envoy-config` | envoy-proxy | Envoy YAML configuration | + +## Common Development Tasks + +### Building Everything Locally + +```bash +# Webhook +cd kagenti-webhook && make build && make test + +# AuthProxy images +cd AuthBridge/AuthProxy && make build-images + +# Client registration (no separate build needed, uses Dockerfile directly) +``` + +### Running the Full Demo + +1. Set up a Kind cluster with SPIRE + Keycloak (use [Kagenti Ansible installer](https://github.com/kagenti/kagenti/blob/main/docs/install.md)) +2. Deploy the webhook: `cd kagenti-webhook && make local-dev CLUSTER=` +3. Follow `AuthBridge/demo.md` for the complete AuthBridge demo + +### Quick Webhook Iteration + +```bash +cd kagenti-webhook +./scripts/webhook-rollout.sh # Build + deploy to Kind +# or with AuthBridge demo setup: +AUTHBRIDGE_DEMO=true ./scripts/webhook-rollout.sh +``` + +### Adding a New Component Image to CI + +1. Add entry to `.github/workflows/build.yaml` matrix (`image_config` array) +2. Provide `name`, `context`, and `dockerfile` fields +3. Image will be pushed to `ghcr.io/kagenti/kagenti-extensions/` + +## Code Style and Conventions + +### Go Code (webhook, AuthProxy) +- Use `go fmt` (enforced by pre-commit and CI) +- Use `go vet` (enforced by pre-commit and CI) +- Kubebuilder markers (`+kubebuilder:webhook:...`) generate webhook manifests -- run `make manifests` after changes +- Logger names: lowercase-hyphenated (e.g., `logf.Log.WithName("pod-mutator")`) +- Apache 2.0 license header in all Go files (template at `kagenti-webhook/hack/boilerplate.go.txt`) + +### Python Code (client-registration) +- Python 3.12+ syntax (type hints with `str | None`) +- Dependencies in `requirements.txt` (version-pinned, e.g. `python-keycloak==5.3.1`) +- UID/GID 1000 in Dockerfile must match `ClientRegistrationUID`/`ClientRegistrationGID` in webhook's `container_builder.go` + +### Kubernetes Manifests +- Example deployment YAMLs in `AuthBridge/k8s/` +- Helm templates in `charts/kagenti-webhook/templates/` +- Helm templates excluded from YAML check in pre-commit (they contain Go template syntax) + +### Shell Scripts +- `set -euo pipefail` (strict mode) +- Extensive inline documentation (especially `init-iptables.sh`) + +## Important Cross-Component Relationships + +1. **UID/GID Sync:** The `client-registration` Dockerfile creates a user with UID/GID 1000. The webhook's `container_builder.go` sets `runAsUser: 1000` / `runAsGroup: 1000`. These MUST match. + +2. **Envoy Proxy UID:** Envoy runs as UID 1337. The `proxy-init` iptables rules exclude this UID from redirection to prevent loops. Both `container_builder.go` and `init-iptables.sh` use this value. + +3. **Shared Volume Contract:** The sidecars communicate through shared volumes: + - `/opt/jwt_svid.token` — spiffe-helper writes, client-registration reads + - `/shared/client-id.txt` — client-registration writes, envoy-proxy reads + - `/shared/client-secret.txt` — client-registration writes, envoy-proxy reads + +4. **Port Coordination:** Envoy listens on 15123 (outbound) and 15124 (inbound). The ext-proc listens on 9090. The `proxy-init` iptables rules redirect to these ports. The webhook's `container_builder.go` exposes these ports on the container spec. + +5. **Image References:** Default image tags are hardcoded in `kagenti-webhook/internal/webhook/injector/container_builder.go` and paralleled in `kagenti-webhook/internal/webhook/config/defaults.go`. The CI in `build.yaml` builds the images. All three must stay in sync. + +6. **Replace Directive:** `kagenti-webhook/go.mod` has a `replace` directive for `github.com/kagenti/operator`. Be careful when updating this dependency. + +## Gotchas and Known Issues + +1. **Config system not wired in:** `kagenti-webhook/internal/webhook/config/` (PlatformConfig, FeatureGates, loaders) exists but is NOT used by the injector. Container builder uses hardcoded constants. This is a known gap. + +2. **Two Go modules:** The repo has two independent Go modules (`kagenti-webhook/go.mod` and `AuthBridge/AuthProxy/go.mod`) with different Go versions (1.24 vs 1.23). They do not share code. + +3. **Helm chart tag placeholder:** `charts/kagenti-webhook/values.yaml` uses `tag: "__PLACEHOLDER__"`. The goreleaser workflow replaces this at release time. For local dev, override with `--set image.tag=`. + +4. **Avoid committing venvs:** Virtual environment directories (e.g. `AuthBridge/AuthProxy/quickstart/venv/`) should be gitignored (the repo's `.gitignore` has a `venv` pattern). Do not create and commit new virtual environments under version control. + +5. **CI Go version alignment:** Ensure the Go version in `ci.yaml` matches the highest Go version required across all modules (currently Go 1.24, matching `kagenti-webhook/go.mod`). + +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. diff --git a/kagenti-webhook/CLAUDE.md b/kagenti-webhook/CLAUDE.md new file mode 100644 index 000000000..63f2455db --- /dev/null +++ b/kagenti-webhook/CLAUDE.md @@ -0,0 +1,249 @@ +# CLAUDE.md - Kagenti Webhook + +This file provides context for Claude (AI assistant) when working with the `kagenti-webhook` codebase. +For the full monorepo context (AuthProxy, client-registration, CI/CD, Helm, cross-component relationships), see [`../CLAUDE.md`](../CLAUDE.md). + +## Project Overview + +**kagenti-webhook** is a Kubernetes mutating admission webhook that automatically injects sidecar containers into workload pods to enable secure service-to-service authentication via Keycloak and optional SPIFFE/SPIRE identity. It is built with the [Kubebuilder](https://book.kubebuilder.io/) framework and uses [controller-runtime](https://github.com/kubernetes-sigs/controller-runtime). + +The project lives inside the larger `kagenti-extensions` monorepo. The Helm chart is at `../charts/kagenti-webhook/`. The CI workflow is at `../.github/workflows/build.yaml`. + +## Architecture Summary + +There are **three** registered webhooks, but only one is actively recommended: + +| Webhook | Status | Path | Handles | +|---------|--------|------|---------| +| **AuthBridge** | **Active / Recommended** | `/mutate-workloads-authbridge` | Deployments, StatefulSets, DaemonSets, Jobs, CronJobs | +| MCPServer | Deprecated | `/mutate-toolhive-stacklok-dev-v1alpha1-mcpserver` | `MCPServer` CR (`toolhive.stacklok.dev/v1alpha1`) | +| Agent | Deprecated | `/mutate-agent-kagenti-dev-v1alpha1-agent` | `Agent` CR (`agent.kagenti.dev/v1alpha1`) | + +All three webhooks share a single `PodMutator` instance created in `cmd/main.go`. + +### Injection Decision Flow + +**AuthBridge** (active path): +1. `kagenti.io/type` label must be `agent` or `tool` -- otherwise skip. +2. `kagenti.io/inject: enabled` label forces injection ON. +3. `kagenti.io/inject: disabled` (or any non-`enabled` value) forces injection OFF. +4. If no pod label, fall back to namespace label `kagenti-enabled: "true"`. + +**Legacy** (deprecated path): +1. CR annotation `kagenti.dev/inject: "false"` opts out. +2. CR annotation `kagenti.dev/inject: "true"` opts in. +3. Fall back to namespace label/annotation. + +### Injected Containers + +**AuthBridge always injects:** +- `proxy-init` (init container) -- iptables redirect setup. +- `envoy-proxy` (sidecar) -- Envoy service mesh proxy for traffic management. + +**Conditionally injected:** +- `spiffe-helper` (sidecar) -- gated by `kagenti.io/spire: enabled` pod label. Obtains JWT-SVIDs from SPIRE agent. +- `kagenti-client-registration` (sidecar) -- gated by `--enable-client-registration` flag (default `true`). Registers with Keycloak; uses SPIFFE identity when SPIRE is enabled, otherwise uses static `CLIENT_NAME`. + +**Legacy webhooks (deprecated) always inject:** `spiffe-helper` + `kagenti-client-registration` + `envoy-proxy` (no `proxy-init` — legacy path does not call `InjectInitContainers`). + +## Directory Structure + +``` +kagenti-webhook/ +├── cmd/main.go # Entrypoint: flags, manager setup, webhook registration +├── internal/webhook/ +│ ├── config/ # Platform configuration (not yet wired into injector) +│ │ ├── types.go # PlatformConfig struct (images, proxy, resources, etc.) +│ │ ├── defaults.go # CompiledDefaults() hardcoded fallback config +│ │ ├── feature_gates.go # FeatureGates struct (global sidecar enable/disable) +│ │ ├── feature_gate_loader.go # File watcher + loader for feature gates +│ │ └── loader.go # File watcher + loader for PlatformConfig +│ ├── injector/ # Shared mutation logic (the core engine) +│ │ ├── pod_mutator.go # PodMutator: ShouldMutate, NeedsMutation, InjectAuthBridge, etc. +│ │ ├── container_builder.go # Build* functions for each injected container +│ │ ├── volume_builder.go # BuildRequiredVolumes / BuildRequiredVolumesNoSpire +│ │ └── namespace_checker.go # CheckNamespaceInjectionEnabled / IsNamespaceInjectionEnabled +│ └── v1alpha1/ # Webhook handlers +│ ├── authbridge_webhook.go # AuthBridge (recommended): raw admission.Handler +│ ├── mcpserver_webhook.go # MCPServer (deprecated): CustomDefaulter + CustomValidator +│ ├── agent_webhook.go # Agent (deprecated): CustomDefaulter + CustomValidator +│ ├── webhook_suite_test.go # ENVTEST-based test setup (Ginkgo) +│ └── mcpserver_webhook_test.go # Unit test stubs for MCPServer webhook +├── config/ # Kustomize manifests (CRDs, RBAC, webhook configs, etc.) +├── test/ +│ ├── e2e/ # End-to-end tests (Kind cluster, Ginkgo) +│ └── utils/ # Test helpers (Run, LoadImageToKind, CertManager, etc.) +├── scripts/ +│ └── webhook-rollout.sh # Build + deploy to Kind cluster script +├── Makefile # Build, test, deploy targets +├── Dockerfile # Multi-stage Go build -> distroless +├── go.mod / go.sum # Go 1.24, controller-runtime v0.22 +└── PROJECT # Kubebuilder project metadata +``` + +## Key Packages and Dependencies + +| Package | Version | Purpose | +|---------|---------|---------| +| `sigs.k8s.io/controller-runtime` | v0.22.1 | Manager, webhook server, envtest | +| `k8s.io/api` | v0.34.1 | Kubernetes API types | +| `github.com/kagenti/operator` | v0.2.0-alpha.12 | Agent CR types (via replace directive) | +| `github.com/stacklok/toolhive` | v0.3.7 | MCPServer CR types | +| `github.com/onsi/ginkgo/v2` | v2.26.0 | BDD test framework | +| `github.com/onsi/gomega` | v1.38.2 | Test matchers | +| `github.com/fsnotify/fsnotify` | v1.9.0 | Config file watching | + +**Go version:** 1.24.4 (toolchain 1.24.8), with `godebug default=go1.23`. + +**Replace directive:** `github.com/kagenti/operator` is replaced with `github.com/kagenti/kagenti-operator/kagenti-operator`. + +## Build and Test Commands + +```bash +# Build binary +make build + +# Run unit tests (requires envtest binaries) +make test + +# Run e2e tests (requires Kind cluster) +make test-e2e + +# Lint +make lint +make lint-fix + +# Build Docker image +make docker-build IMG= + +# Local development with Kind +make local-dev CLUSTER= + +# Quick rebuild + rollout (uses scripts/webhook-rollout.sh) +./scripts/webhook-rollout.sh + +# Generate manifests (CRDs, RBAC, webhook configs) +make manifests + +# Generate deepcopy methods +make generate +``` + +### Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `ENABLE_WEBHOOKS` | (unset = true) | Set to `"false"` to disable all webhook registration | +| `CLUSTER` | `kagenti` | Kind cluster name for local dev | +| `NAMESPACE` | `kagenti-webhook-system` | Deployment namespace | +| `AUTHBRIDGE_DEMO` | `false` | Enable AuthBridge demo setup in rollout script | +| `DOCKER_IMPL` | (auto-detect) | Force container runtime (`docker` or `podman`) | + +### CLI Flags (cmd/main.go) + +| Flag | Default | Description | +|------|---------|-------------| +| `--metrics-bind-address` | `0` (disabled) | Metrics endpoint bind address | +| `--health-probe-bind-address` | `:8081` | Health/ready probe address | +| `--leader-elect` | `false` | Enable leader election | +| `--metrics-secure` | `true` | Serve metrics over HTTPS | +| `--enable-client-registration` | `true` | Inject client-registration sidecar | +| `--webhook-cert-path` | `""` | TLS cert directory for webhook server | +| `--enable-http2` | `false` | Enable HTTP/2 (disabled by default for CVE mitigation) | + +## Code Conventions and Patterns + +### Naming Conventions +- **Constants** follow `CamelCase` (e.g., `SpiffeHelperContainerName`, `DefaultNamespaceLabel`). +- **Logger names** use lowercase-hyphenated format (e.g., `logf.Log.WithName("pod-mutator")`). +- **Webhook handler types** are `{Resource}Webhook`, `{Resource}CustomDefaulter`, `{Resource}CustomValidator`. +- **Builder functions** are `Build{Component}Container()` or `Build{Component}ContainerWithSpireOption()`. +- Container name constants must match what is checked in `isAlreadyInjected()` for idempotency. + +### Architecture Patterns +- **Shared PodMutator**: All webhooks share one `injector.PodMutator` instance, created in `main()` and passed to each webhook setup function. This ensures consistent mutation logic. +- **Two mutation paths**: `MutatePodSpec()` (legacy, always enables SPIRE) vs `InjectAuthBridge()` (new, SPIRE is optional). When removing deprecated code, delete `MutatePodSpec`, `ShouldMutate`, and `CheckNamespaceInjectionEnabled`. +- **Idempotency**: `AuthBridgeWebhook.isAlreadyInjected()` checks for existing sidecars before injection. +- **Container existence checks**: `containerExists()` and `volumeExists()` helpers prevent duplicate injection. +- **Kubebuilder markers**: Webhook path markers (e.g., `+kubebuilder:webhook:path=...`) in Go comments generate the webhook manifests. Do not change these without running `make manifests`. + +### ConfigMap Dependencies at Runtime +Injected sidecars expect these ConfigMaps to exist in the target namespace: +- `environments` -- `KEYCLOAK_URL`, `KEYCLOAK_REALM`, `KEYCLOAK_ADMIN_USERNAME`, `KEYCLOAK_ADMIN_PASSWORD` +- `authbridge-config` -- `TOKEN_URL`, `ISSUER`, `TARGET_AUDIENCE`, `TARGET_SCOPES` +- `spiffe-helper-config` -- SPIFFE helper configuration (when SPIRE is enabled) +- `envoy-config` -- Envoy proxy configuration + +### Security Model +- `proxy-init` runs as an init container with a short lifetime (iptables setup). +- `envoy-proxy` runs as UID 1337. +- `client-registration` runs as UID/GID 1000. +- `spiffe-helper` uses no explicit security context. +- Istio exclusion annotations (`sidecar.istio.io/inject`, `ambient.istio.io/redirection`) are defined as constants but not yet actively applied. + +### Test Infrastructure +- **Unit tests**: Use controller-runtime's `envtest` with Ginkgo/Gomega. Test setup is in `webhook_suite_test.go`. Run with `make test`. +- **E2E tests**: Require a Kind cluster with CertManager and Prometheus. Run with `make test-e2e`. Test setup installs CRDs, deploys the controller, and validates pod status + metrics. +- **Test binaries path**: ENVTEST binaries are expected in `bin/k8s/` (auto-discovered by `getFirstFoundEnvTestBinaryDir()`). + +## Common Tasks for Code Changes + +### Adding a New Injected Sidecar +1. Add container name constant in `injector/pod_mutator.go`. +2. Add `Build{Name}Container()` function in `injector/container_builder.go`. +3. Add any required volumes in `injector/volume_builder.go` (both `BuildRequiredVolumes` and `BuildRequiredVolumesNoSpire` if applicable). +4. Call the builder in `InjectSidecarsWithSpireOption()` in `pod_mutator.go`. +5. Update `isAlreadyInjected()` in `authbridge_webhook.go` to check for the new container name. +6. Update `internal/webhook/config/types.go` and `defaults.go` with image/resource defaults. + +### Adding a New Supported Workload Type +1. Add a new `case` in `AuthBridgeWebhook.Handle()` in `authbridge_webhook.go`. +2. Update the kubebuilder webhook marker to include the new resource in the `resources` list. +3. Run `make manifests` to regenerate webhook configuration YAML. +4. Update `scripts/webhook-rollout.sh` to include the new resource in the webhook rules. +5. Update the Helm chart template `charts/kagenti-webhook/templates/authbridge-mutatingwebhook.yaml`. + +### Modifying Injection Logic +- Injection decision logic lives in `pod_mutator.go` in `NeedsMutation()` (AuthBridge) and `ShouldMutate()` (legacy). +- Namespace checks are in `namespace_checker.go`. +- Changes to label/annotation keys require updating the constants at the top of `pod_mutator.go`. + +### Removing Deprecated Code +When removing legacy (Agent/MCPServer) webhook support: +1. Remove `mcpserver_webhook.go`, `agent_webhook.go`, and their tests. +2. Remove `ShouldMutate()`, `MutatePodSpec()`, `InjectSidecars()`, `InjectVolumes()` from `pod_mutator.go`. +3. Remove `CheckNamespaceInjectionEnabled()` from `namespace_checker.go`. +4. Remove legacy scheme registration and webhook setup calls from `cmd/main.go`. +5. Remove `github.com/kagenti/operator` and `github.com/stacklok/toolhive` from `go.mod`. +6. Remove legacy Helm templates (`agent-*.yaml`, `mcpserver-*.yaml`). +7. Run `make manifests` and `make generate`. + +### Updating Container Images +- Default images are defined as constants in `injector/container_builder.go` (`DefaultEnvoyImage`, `DefaultProxyInitImage`) and inline in `BuildSpiffeHelperContainer()` and `BuildClientRegistrationContainerWithSpireOption()`. +- The `internal/webhook/config/defaults.go` file has a parallel set of defaults in `CompiledDefaults()` -- keep them in sync (or wire the config system into the injector, which is a TODO). +- The GitHub Actions CI builds images defined in `../.github/workflows/build.yaml`. + +### Helm Chart +- Located at `../charts/kagenti-webhook/`. +- Key values: `image.repository`, `image.tag`, `webhook.enabled`, `webhook.enableClientRegistration`, `certManager.enabled`. +- AuthBridge webhook configuration template: `templates/authbridge-mutatingwebhook.yaml`. + +## Gotchas and Known Issues + +1. **Config system not wired in**: `internal/webhook/config/` (PlatformConfig, FeatureGates, loaders) exists but is **not yet used** by the injector. Container builder still uses hardcoded constants. This is a known gap. + +2. **Replace directive in go.mod**: `github.com/kagenti/operator` is replaced -- be careful when updating dependencies or adding new imports from the kagenti-operator project. + +3. **Kubebuilder markers**: The `+kubebuilder:webhook` comments generate webhook manifests. If you change the path, resources, or groups, you must run `make manifests` to regenerate. + +4. **AuthBridge uses raw admission.Handler**: Unlike the legacy webhooks (which use `CustomDefaulter`/`CustomValidator`), the AuthBridge webhook registers directly via `mgr.GetWebhookServer().Register()`. This is because it handles multiple resource types in a single handler. + +5. **Idempotency check**: `isAlreadyInjected()` checks for all four injected components (`envoy-proxy`, `spiffe-helper`, `kagenti-client-registration` in sidecar containers, `proxy-init` in init containers). If any one is found, re-admission is short-circuited. + +6. **ENVTEST binary path**: Tests assume envtest binaries are in `bin/k8s/`. Run `make setup-envtest` to download them before running tests from an IDE. + +7. **Helm chart image tag placeholder**: `values.yaml` uses `tag: "__PLACEHOLDER__"` -- this must be overridden at install time. + +## License + +Apache License 2.0. Copyright 2025. All Go files include the license header from `hack/boilerplate.go.txt`.