diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 09cc047c6..9c290641b 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -30,18 +30,28 @@ jobs: dockerfile: Dockerfile.init # AuthBridge envoy-sidecar combined image — - # Envoy + authbridge (ext_proc) + spiffe-helper. Spiffe-helper - # starts conditionally based on SPIRE_ENABLED. + # Envoy + authbridge-envoy (ext_proc) + spiffe-helper. + # Spiffe-helper starts conditionally based on SPIRE_ENABLED. - name: authbridge-envoy context: ./authbridge - dockerfile: cmd/authbridge/Dockerfile.envoy + dockerfile: cmd/authbridge-envoy/Dockerfile # AuthBridge proxy-sidecar combined image (default mode) — - # authbridge-proxy + spiffe-helper. No Envoy, no gRPC. - # Spiffe-helper starts conditionally based on SPIRE_ENABLED. + # authbridge-proxy (full plugin set, includes parsers) + + # spiffe-helper. No Envoy, no gRPC. Spiffe-helper starts + # conditionally based on SPIRE_ENABLED. - name: authbridge context: ./authbridge - dockerfile: cmd/authbridge/Dockerfile.proxy + dockerfile: cmd/authbridge-proxy/Dockerfile + + # AuthBridge proxy-sidecar lite combined image — + # authbridge-lite (auth-only plugins, parsers dropped for + # binary size) + spiffe-helper. Same listener layout as the + # full proxy image; not yet referenced by the operator's + # default config. + - name: authbridge-lite + context: ./authbridge + dockerfile: cmd/authbridge-lite/Dockerfile steps: # 1. Checkout code diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c345b6b39..016e35887 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -93,12 +93,19 @@ jobs: run: go test -v -race -cover ./... go-ci-authbridge-cmd: - name: Go CI (authbridge cmd) + name: Go CI (authbridge ${{ matrix.binary }}) runs-on: ubuntu-latest timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + binary: + - authbridge-proxy + - authbridge-envoy + - authbridge-lite defaults: run: - working-directory: authbridge/cmd/authbridge + working-directory: authbridge/cmd/${{ matrix.binary }} env: # Disable go.work — the workspace at authbridge/ includes ./authproxy # which would pull in that module's dependencies. The replace directive @@ -109,8 +116,8 @@ jobs: - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 with: - go-version-file: authbridge/cmd/authbridge/go.mod - cache-dependency-path: authbridge/cmd/authbridge/go.sum + go-version-file: authbridge/cmd/${{ matrix.binary }}/go.mod + cache-dependency-path: authbridge/cmd/${{ matrix.binary }}/go.sum - name: Lint run: | diff --git a/CLAUDE.md b/CLAUDE.md index bfb5b675b..c4af590cf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,14 +30,20 @@ kagenti-extensions/ │ │ ├── routing/ # Host-to-audience router │ │ ├── auth/ # HandleInbound + HandleOutbound composition │ │ └── config/ # Mode presets, YAML config, validation -│ ├── cmd/authbridge/ # Unified binary + combined Dockerfiles + entrypoints -│ │ ├── listener/ # Protocol adapters (ext_proc, ext_authz) -│ │ ├── Dockerfile.envoy # envoy-sidecar combined image -│ │ ├── Dockerfile.proxy # proxy-sidecar combined image (default) -│ │ ├── entrypoint-envoy.sh -│ │ └── entrypoint-proxy.sh -│ ├── cmd/authbridge-proxy/ # Lite binary — proxy-sidecar mode only -│ ├── cmd/authbridge-envoy/ # Lite binary — envoy-sidecar mode only +│ ├── cmd/authbridge-proxy/ # proxy-sidecar mode (default): HTTP forward + reverse +│ │ │ # proxies, full plugin set including parsers +│ │ ├── main.go +│ │ ├── Dockerfile # proxy-sidecar combined image +│ │ └── entrypoint.sh +│ ├── cmd/authbridge-envoy/ # envoy-sidecar mode: ext_proc gRPC server, full plugin set +│ │ ├── main.go +│ │ ├── Dockerfile # envoy-sidecar combined image +│ │ └── entrypoint.sh +│ ├── cmd/authbridge-lite/ # proxy-sidecar mode, lite plugin set (no parsers) +│ │ │ # for size-optimized deployments +│ │ ├── main.go +│ │ ├── Dockerfile # proxy-sidecar lite combined image +│ │ └── entrypoint.sh │ ├── authproxy/ # iptables init container + standalone quickstart │ │ ├── quickstart/ # Standalone demo (no SPIFFE) │ │ └── k8s/ # Standalone K8s manifests @@ -53,22 +59,26 @@ kagenti-extensions/ ## Major Components -### 1. AuthBridge Unified Binary (Go) +### 1. AuthBridge Binaries (Go) -A **single binary** providing transparent traffic interception for both inbound JWT validation and outbound OAuth 2.0 token exchange (RFC 8693), supporting three deployment modes. +**Three mode-specific binaries** providing transparent traffic interception for both inbound JWT validation and outbound OAuth 2.0 token exchange (RFC 8693). Each binary is hardcoded to its deployment shape; mode is no longer selected at runtime. -**Location:** `authbridge/cmd/authbridge/` -**Library:** `authbridge/authlib/` +**Library:** `authbridge/authlib/` (shared) **Language:** Go 1.24 **Detailed guide:** [`authbridge/CLAUDE.md`](authbridge/CLAUDE.md) -**Core components:** -- `cmd/authbridge/main.go` — Unified binary (ext_proc, ext_authz, forward/reverse proxy modes) -- `authlib/` — Shared auth library (JWT validation, token exchange, caching, routing) -- `authproxy/init-iptables.sh` — Traffic interception setup (Istio ambient mesh compatible) -- `authproxy/Dockerfile.init` — Init container image +**Binaries:** +- `cmd/authbridge-proxy/` — proxy-sidecar mode (default): HTTP forward + reverse proxies, full plugin set (jwt-validation, token-exchange, a2a-parser, mcp-parser, inference-parser). No Envoy / no gRPC. +- `cmd/authbridge-envoy/` — envoy-sidecar mode: ext_proc gRPC server hooked into Envoy, full plugin set. +- `cmd/authbridge-lite/` — proxy-sidecar mode, lite plugin set (auth gates only, parsers dropped) for size-optimized deployments. -**Ports:** 15123 (outbound), 15124 (inbound), 9090 (ext-proc/ext-authz), 9901 (admin), 9093 (stats and config) +**Common:** +- `authlib/` — shared auth library (JWT validation, token exchange, caching, routing, all listener implementations, all plugins). +- `authproxy/init-iptables.sh` — traffic interception setup (Istio ambient mesh compatible). Used by envoy-sidecar mode only. +- `authproxy/Dockerfile.init` — proxy-init container image. + +**Ports (envoy-sidecar):** 15123 (outbound), 15124 (inbound), 9090 (ext-proc), 9901 (admin) +**Ports (proxy-sidecar / lite):** 8080 (reverse proxy), 8081 (forward proxy), 9091 (health), 9093 (stats), 9094 (session API) ### 2. Client Registration @@ -108,34 +118,29 @@ into workload pods. Default deployment shape (proxy-sidecar mode): and add a proxy-init container for iptables. ``` -## Unified AuthBridge Binary +## AuthBridge Binaries -The `cmd/authbridge/` directory contains a unified binary that replaces three separate -codebases (go-processor, waypoint, klaviger) with a single binary supporting three modes: +Three mode-specific binaries, one Dockerfile per binary: -| Mode | Interception | Listeners | Use Case | -|------|-------------|-----------|----------| -| `envoy-sidecar` | Envoy iptables + ext_proc | gRPC ext_proc on :9090 | Sidecar per agent pod | -| `waypoint` | Istio ambient + ext_authz | gRPC ext_authz + HTTP forward proxy | Shared service | -| `proxy-sidecar` | Reverse proxy + forward proxy | HTTP reverse proxy + forward proxy | Sidecar without Envoy | +| Binary | Mode | Listeners | Plugins | +|--------|------|-----------|---------| +| `cmd/authbridge-proxy/` | proxy-sidecar (default) | HTTP forward + reverse proxies | full (incl. parsers) | +| `cmd/authbridge-envoy/` | envoy-sidecar | gRPC ext_proc on :9090 | full (incl. parsers) | +| `cmd/authbridge-lite/` | proxy-sidecar | HTTP forward + reverse proxies | auth-only (jwt-validation + token-exchange, no parsers) | **Go modules:** -- `authbridge/authlib/` — pure library, no protocol deps (validation, exchange, cache, bypass, spiffe, routing, auth, config) -- `authbridge/cmd/authbridge/` — binary + listeners, imports authlib + gRPC/Envoy deps -- `authbridge/go.work` — workspace linking both modules for local development - -**Config format:** YAML with `${ENV_VAR}` expansion, mode presets, and startup validation. -Supports `keycloak_url` + `keycloak_realm` derivation for operator compatibility. +- `authbridge/authlib/` — pure library: validation, exchange, cache, bypass, spiffe, routing, auth, config, all listener implementations, all plugins. +- `authbridge/cmd/authbridge-{proxy,envoy,lite}/` — thin main packages that import authlib and start the listeners they need. +- `authbridge/go.work` — workspace linking authlib + the binaries for local development. -**Image:** `ghcr.io/kagenti/kagenti-extensions/authbridge-unified` — Envoy + authbridge -in one container (drop-in replacement for `envoy-with-processor`). +**Config format:** YAML with `${ENV_VAR}` expansion, mode presets, and startup validation. Supports `keycloak_url` + `keycloak_realm` derivation for operator compatibility. The `mode` field in YAML must match the binary (each binary rejects mismatched modes at boot). ## CI/CD Workflows | Workflow | Trigger | Purpose | |----------|---------|---------| -| `ci.yaml` | PR to main/release-* | Go fmt, vet, build, test for authproxy, authlib, and cmd/authbridge; Python tests | -| `build.yaml` | Tag push (`v*`) or manual | Multi-arch Docker builds for: client-registration, auth-proxy, proxy-init, authbridge, authbridge-unified, spiffe-helper, demo-app | +| `ci.yaml` | PR to main/release-* | Go fmt, vet, build, test for authproxy, authlib, and the cmd/authbridge-* binaries; Python tests | +| `build.yaml` | Tag push (`v*`) or manual | Multi-arch Docker builds for: proxy-init, authbridge (proxy-sidecar combined), authbridge-envoy (envoy-sidecar combined), authbridge-lite (proxy-sidecar lite combined) | | `security-scans.yaml` | PR to main | Dependency review, shellcheck, YAML lint, Hadolint, Bandit, Trivy, CodeQL | | `scorecard.yaml` | Weekly / push to main | OpenSSF Scorecard security health metrics | | `spellcheck_action.yml` | PR | Spellcheck on markdown files | @@ -157,12 +162,13 @@ All images are pushed to `ghcr.io/kagenti/kagenti-extensions/` from | Image | Source | Description | |-------|--------|-------------| -| **`authbridge`** | **`authbridge/cmd/authbridge/Dockerfile.proxy`** | **proxy-sidecar combined image (default mode): authbridge-proxy + spiffe-helper. No Envoy.** | -| `authbridge-envoy` | `authbridge/cmd/authbridge/Dockerfile.envoy` | envoy-sidecar combined image: Envoy + authbridge (ext_proc) + spiffe-helper | +| **`authbridge`** | **`authbridge/cmd/authbridge-proxy/Dockerfile`** | **proxy-sidecar combined image (default mode): authbridge-proxy (full plugin set incl. parsers) + spiffe-helper. No Envoy.** | +| `authbridge-envoy` | `authbridge/cmd/authbridge-envoy/Dockerfile` | envoy-sidecar combined image: Envoy + authbridge-envoy (ext_proc, full plugin set) + spiffe-helper | +| `authbridge-lite` | `authbridge/cmd/authbridge-lite/Dockerfile` | proxy-sidecar lite combined image: authbridge-lite (auth gates only, parsers dropped) + spiffe-helper. Same listener layout as `authbridge`; not yet referenced by the operator's default config | | `proxy-init` | `authbridge/authproxy/Dockerfile.init` | Alpine + iptables init container (envoy-sidecar mode only) | -In both combined images, `spiffe-helper` is started conditionally based -on the `SPIRE_ENABLED` env var (set by the operator when SPIRE +In all three combined images, `spiffe-helper` is started conditionally +based on the `SPIRE_ENABLED` env var (set by the operator when SPIRE identity is enabled for the workload). The legacy `authbridge-unified`, `authbridge-light`, `client-registration`, diff --git a/authbridge/CLAUDE.md b/authbridge/CLAUDE.md index 8ddfa2819..7f187e91e 100644 --- a/authbridge/CLAUDE.md +++ b/authbridge/CLAUDE.md @@ -4,15 +4,24 @@ This file provides context for Claude (AI assistant) when working with the `Auth For repo-level context (CI/CD, cross-component relationships), see [`../CLAUDE.md`](../CLAUDE.md). The sidecar injection webhook lives in [kagenti-operator](https://github.com/kagenti/kagenti-operator). -## Unified Binary +## Binaries -The `cmd/authbridge/` directory contains the unified authbridge binary that replaces the -old `go-processor` ext_proc server. It supports three modes (`envoy-sidecar`, `waypoint`, -`proxy-sidecar`) with shared auth logic in `authlib/`. See [`cmd/authbridge/README.md`](cmd/authbridge/README.md) -for config format and [`authlib/README.md`](authlib/README.md) for the library reference. +The unified `cmd/authbridge/` binary has been split into three mode-specific +binaries with shared auth logic in `authlib/`: -The old `authproxy/go-processor/` has been removed. All development targets -`authlib/` and `cmd/authbridge/`. +- `cmd/authbridge-proxy/` — proxy-sidecar mode (default). HTTP forward + reverse + proxies. Full plugin set (jwt-validation, token-exchange, a2a-parser, + mcp-parser, inference-parser). +- `cmd/authbridge-envoy/` — envoy-sidecar mode. ext_proc gRPC server hooked + into Envoy. Full plugin set. +- `cmd/authbridge-lite/` — proxy-sidecar mode, lite plugin set (auth gates + only, parsers dropped). For size-optimized deployments that don't need + protocol-aware session events. + +Each binary is hardcoded to its deployment shape; mode is no longer selected +at runtime. The YAML `mode:` field must match the binary or boot fails. + +See [`authlib/README.md`](authlib/README.md) for the library reference. ## What AuthBridge Does @@ -38,17 +47,20 @@ authbridge/ │ ├── auth/ # HandleInbound + HandleOutbound composition │ └── config/ # Mode presets, YAML config, validation │ -├── cmd/authbridge/ # Unified binary — 3 listener modes, 1 codebase -│ ├── listener/extauthz/ # ext_authz adapter (waypoint mode) -│ ├── listener/extproc/ # ext_proc adapter (envoy-sidecar mode) -│ ├── Dockerfile.envoy # envoy-sidecar combined image (Envoy + authbridge + spiffe-helper) -│ ├── Dockerfile.proxy # proxy-sidecar combined image (authbridge-proxy + spiffe-helper) -│ ├── entrypoint-envoy.sh # Process supervisor for Dockerfile.envoy -│ └── entrypoint-proxy.sh # Process supervisor for Dockerfile.proxy +├── cmd/authbridge-proxy/ # proxy-sidecar mode (default). Full plugin set. +│ ├── main.go +│ ├── Dockerfile # proxy-sidecar combined image (authbridge-proxy + spiffe-helper) +│ └── entrypoint.sh │ -├── cmd/authbridge-proxy/ # Lite binary — proxy-sidecar mode only, no gRPC +├── cmd/authbridge-envoy/ # envoy-sidecar mode. Full plugin set. +│ ├── main.go +│ ├── Dockerfile # envoy-sidecar combined image (Envoy + authbridge-envoy + spiffe-helper) +│ └── entrypoint.sh │ -├── cmd/authbridge-envoy/ # Lite binary — envoy-sidecar mode only +├── cmd/authbridge-lite/ # proxy-sidecar mode. Lite plugin set (no parsers). +│ ├── main.go +│ ├── Dockerfile # proxy-sidecar lite combined image +│ └── entrypoint.sh │ ├── authproxy/ # iptables init container + standalone quickstart │ ├── init-iptables.sh # iptables setup for envoy-sidecar mode @@ -82,10 +94,12 @@ authbridge/ ## Component Details -### AuthBridge Unified Binary (cmd/authbridge/) +### AuthBridge Binaries (cmd/authbridge-{proxy,envoy,lite}/) -The unified authbridge binary handles both traffic directions. Auth logic lives in `authlib/`, -with protocol-specific listeners in `cmd/authbridge/listener/`: +The mode-specific authbridge binaries handle both traffic directions. Auth logic +and all listener implementations live in `authlib/` (under `authlib/listener/`); +each binary's `main.go` just imports the listeners it needs and the plugins it +wants to register. **Inbound path** (`x-authbridge-direction: inbound`): - Validates JWT signature via JWKS (auto-refreshing cache from `TOKEN_URL`-derived JWKS endpoint) @@ -111,7 +125,7 @@ with protocol-specific listeners in `cmd/authbridge/listener/`: **Configuration loading:** - YAML config with `${ENV_VAR}` expansion, mode presets, and startup validation. -- Plugin settings are local to each plugin under `pipeline.*.plugins[].config`; the runtime YAML itself only carries `mode`, `listener`, `session`, `stats`, and the pipeline composition. See [`cmd/authbridge/README.md`](cmd/authbridge/README.md) for the per-mode YAML shape and [`docs/plugin-reference.md`](docs/plugin-reference.md) for the per-plugin decode pattern. +- Plugin settings are local to each plugin under `pipeline.*.plugins[].config`; the runtime YAML itself only carries `mode`, `listener`, `session`, `stats`, and the pipeline composition. See [`docs/plugin-reference.md`](docs/plugin-reference.md) for the per-plugin decode pattern. - The operator-supplied env vars (`KEYCLOAK_URL`, `KEYCLOAK_REALM`, `TOKEN_URL`, `ISSUER`, `DEFAULT_OUTBOUND_POLICY`, `CLIENT_ID`) are consumed by the default `authbridge-combined.yaml` via `${VAR}` expansion — they land inside the appropriate plugin's `config:` block rather than a top-level section. - `jwt-validation` derives `jwks_url` from `issuer` when omitted (appends `/protocol/openid-connect/certs`). - `token-exchange` derives `token_url` from `keycloak_url + keycloak_realm` when omitted (Keycloak convention). @@ -255,12 +269,15 @@ make load-images # Uses KIND_CLUSTER_NAME env var (default: k # Build the combined sidecar images (from authbridge/ context). # Pick the one matching your deployment mode: # -# authbridge-envoy = Envoy + authbridge (ext_proc) + spiffe-helper -# authbridge = authbridge-proxy + spiffe-helper (default mode, no Envoy) -cd .. && podman build -f cmd/authbridge/Dockerfile.envoy -t authbridge-envoy:latest . -podman build -f cmd/authbridge/Dockerfile.proxy -t authbridge:latest . +# authbridge = proxy-sidecar combined (authbridge-proxy + spiffe-helper, full plugins) +# authbridge-envoy = envoy-sidecar combined (Envoy + authbridge-envoy ext_proc + spiffe-helper) +# authbridge-lite = proxy-sidecar lite (authbridge-lite + spiffe-helper, no parsers) +cd .. && podman build -f cmd/authbridge-envoy/Dockerfile -t authbridge-envoy:latest . +podman build -f cmd/authbridge-proxy/Dockerfile -t authbridge:latest . +podman build -f cmd/authbridge-lite/Dockerfile -t authbridge-lite:latest . kind load docker-image authbridge-envoy:latest --name kagenti kind load docker-image authbridge:latest --name kagenti +kind load docker-image authbridge-lite:latest --name kagenti # Deploy auth-proxy + demo-app make deploy @@ -395,13 +412,13 @@ See [`docs/framework-architecture.md`](docs/framework-architecture.md#9-config-h ## Code Conventions -### Go (authlib, cmd/authbridge, demo-app) +### Go (authlib, cmd/authbridge-{proxy,envoy,lite}, demo-app) - Go 1.24 -- Two modules: `authbridge/authlib/` (pure library) and `authbridge/cmd/authbridge/` (binary + listeners) -- `authbridge/go.work` workspace links both modules for local development -- Logging with `log.Printf` (stdlib), prefixed by `[Config]`, `[Token Exchange]`, `[Inbound]`, `[JWT Debug]` -- gRPC ext-proc using `envoyproxy/go-control-plane` types (in cmd/authbridge) -- JWT validation with `lestrrat-go/jwx/v2` (in authlib/validation) +- Modules: `authbridge/authlib/` (pure library — all listeners, all plugins) and `authbridge/cmd/authbridge-{proxy,envoy,lite}/` (mode-specific binaries that wire listeners + plugins together) +- `authbridge/go.work` workspace links the modules for local development +- Logging with `log/slog`; the binaries log under their own name (`authbridge-proxy`, `authbridge-envoy`, `authbridge-lite`) +- gRPC ext-proc using `envoyproxy/go-control-plane` types (in `authlib/listener/extproc`) +- JWT validation with `lestrrat-go/jwx/v2` (in `authlib/plugins/jwtvalidation/validation`) ### Python (client-registration, setup scripts) - Python 3.12 syntax (type hints: `str | None`) diff --git a/authbridge/authlib/go.mod b/authbridge/authlib/go.mod index 9d9f136c9..6357aaa92 100644 --- a/authbridge/authlib/go.mod +++ b/authbridge/authlib/go.mod @@ -5,21 +5,30 @@ go 1.24.0 toolchain go1.24.5 require ( + github.com/envoyproxy/go-control-plane/envoy v1.37.0 + github.com/fsnotify/fsnotify v1.8.0 github.com/gobwas/glob v0.2.3 github.com/lestrrat-go/jwx/v2 v2.1.6 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 + google.golang.org/grpc v1.80.0 gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect - github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect github.com/goccy/go-json v0.10.3 // indirect github.com/lestrrat-go/blackmagic v1.0.3 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc v1.0.6 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/option v1.0.1 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/segmentio/asm v1.2.0 // indirect golang.org/x/crypto v0.48.0 // indirect + golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/authbridge/authlib/go.sum b/authbridge/authlib/go.sum index d338f51ea..f63e27c28 100644 --- a/authbridge/authlib/go.sum +++ b/authbridge/authlib/go.sum @@ -1,14 +1,32 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= +github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= +github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= +github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/lestrrat-go/blackmagic v1.0.3 h1:94HXkVLxkZO9vJI/w2u1T0DAoprShFd13xtnSINtDWs= github.com/lestrrat-go/blackmagic v1.0.3/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= @@ -21,6 +39,8 @@ github.com/lestrrat-go/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVf github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= @@ -30,10 +50,34 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/authbridge/cmd/authbridge/listener/extauthz/server.go b/authbridge/authlib/listener/extauthz/server.go similarity index 100% rename from authbridge/cmd/authbridge/listener/extauthz/server.go rename to authbridge/authlib/listener/extauthz/server.go diff --git a/authbridge/cmd/authbridge/listener/extauthz/server_test.go b/authbridge/authlib/listener/extauthz/server_test.go similarity index 100% rename from authbridge/cmd/authbridge/listener/extauthz/server_test.go rename to authbridge/authlib/listener/extauthz/server_test.go diff --git a/authbridge/cmd/authbridge/listener/extproc/server.go b/authbridge/authlib/listener/extproc/server.go similarity index 100% rename from authbridge/cmd/authbridge/listener/extproc/server.go rename to authbridge/authlib/listener/extproc/server.go diff --git a/authbridge/cmd/authbridge/listener/extproc/server_test.go b/authbridge/authlib/listener/extproc/server_test.go similarity index 100% rename from authbridge/cmd/authbridge/listener/extproc/server_test.go rename to authbridge/authlib/listener/extproc/server_test.go diff --git a/authbridge/cmd/README.md b/authbridge/cmd/README.md new file mode 100644 index 000000000..aace8b438 --- /dev/null +++ b/authbridge/cmd/README.md @@ -0,0 +1,66 @@ +# AuthBridge Binaries + +Three mode-specific authbridge binaries plus the `abctl` TUI. Each binary +is hardcoded to a single deployment shape; the YAML `mode:` field must +match the binary or boot fails. Mode is selected at build time by which +binary you run, not at runtime via a flag. + +## Binaries + +| Directory | Mode | Listeners | Plugins | Image (CI) | +|---|---|---|---|---| +| [`authbridge-proxy/`](authbridge-proxy/) | `proxy-sidecar` (default) | HTTP forward + reverse proxies | full (jwt-validation, token-exchange, a2a-parser, mcp-parser, inference-parser) | `ghcr.io/kagenti/kagenti-extensions/authbridge` | +| [`authbridge-envoy/`](authbridge-envoy/) | `envoy-sidecar` | gRPC ext_proc on `:9090` (hooked into Envoy) | full | `ghcr.io/kagenti/kagenti-extensions/authbridge-envoy` | +| [`authbridge-lite/`](authbridge-lite/) | `proxy-sidecar` | HTTP forward + reverse proxies | lite (jwt-validation + token-exchange only — parsers dropped) | `ghcr.io/kagenti/kagenti-extensions/authbridge-lite` | +| [`abctl/`](abctl/) | n/a | n/a | n/a | not published — local TUI for the Session Events API | + +Each binary directory contains `main.go`, `go.mod`/`go.sum`, +`Dockerfile`, and `entrypoint.sh`. The Dockerfiles produce +combined images that bundle the authbridge binary, the +[`spiffe-helper`](https://github.com/spiffe/spiffe-helper) daemon +(started conditionally on `SPIRE_ENABLED=true`), and — for the envoy +variant — the Envoy proxy itself. + +## Configuration + +All three binaries accept a single flag, `--config `, pointing +at the YAML config file the operator mounts at +`/etc/authbridge/config.yaml`. The config schema and per-plugin +options are documented in +[`../docs/plugin-reference.md`](../docs/plugin-reference.md). +Hot-reload, the session-events API at `:9094`, and the supporting +ConfigMap contracts are documented in +[`../CLAUDE.md`](../CLAUDE.md). + +## Ports + +**Proxy-sidecar (`authbridge-proxy`, `authbridge-lite`):** + +| Port | Purpose | +|---|---| +| 8080 | Reverse proxy (inbound) | +| 8081 | Forward proxy (outbound; HTTP_PROXY target) | +| 9091 | Health | +| 9093 | Stats / config inspection | +| 9094 | Session Events API (consumed by `abctl`) | + +**Envoy-sidecar (`authbridge-envoy`):** + +| Port | Purpose | +|---|---| +| 15123 | Envoy outbound listener (iptables redirects here) | +| 15124 | Envoy inbound listener | +| 9090 | gRPC ext_proc (called by Envoy) | +| 9901 | Envoy admin | + +## Choosing a binary + +- **Default deployment**: use `authbridge-proxy`. No iptables, no + Envoy, observable via abctl. +- **Need ambient/transparent interception via Envoy**: use + `authbridge-envoy`. Requires the [`proxy-init`](../authproxy/) + iptables init container. +- **Size-constrained, no protocol-aware events needed**: use + `authbridge-lite`. Same listener layout as `authbridge-proxy` but + without parsers — abctl will only see denial events and basic + auth-level invocations, not full A2A/MCP/Inference protocol context. diff --git a/authbridge/cmd/abctl/go.mod b/authbridge/cmd/abctl/go.mod index 65188f216..5b07473bd 100644 --- a/authbridge/cmd/abctl/go.mod +++ b/authbridge/cmd/abctl/go.mod @@ -2,8 +2,6 @@ module github.com/kagenti/kagenti-extensions/authbridge/cmd/abctl go 1.24.2 -toolchain go1.24.5 - // Workspace-only: this replace is satisfied by authbridge/go.work during // local development. Standalone `go get` / `go install` outside the // workspace will not resolve this path — if abctl is ever distributed as @@ -15,6 +13,7 @@ require ( github.com/charmbracelet/bubbles v1.0.0 github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/lipgloss v1.1.0 + github.com/charmbracelet/x/ansi v0.11.6 github.com/kagenti/kagenti-extensions/authbridge/authlib v0.0.0-00010101000000-000000000000 ) @@ -22,22 +21,12 @@ require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/colorprofile v0.4.1 // indirect - github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect github.com/clipperhouse/displaywidth v0.9.0 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/uax29/v2 v2.5.0 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/gobwas/glob v0.2.3 // indirect - github.com/goccy/go-json v0.10.3 // indirect - github.com/lestrrat-go/blackmagic v1.0.3 // indirect - github.com/lestrrat-go/httpcc v1.0.1 // indirect - github.com/lestrrat-go/httprc v1.0.6 // indirect - github.com/lestrrat-go/iter v1.0.2 // indirect - github.com/lestrrat-go/jwx/v2 v2.1.6 // indirect - github.com/lestrrat-go/option v1.0.1 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect @@ -46,10 +35,7 @@ require ( github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/segmentio/asm v1.2.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/crypto v0.48.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/authbridge/cmd/abctl/go.sum b/authbridge/cmd/abctl/go.sum index 125d8c8d5..59a5512f1 100644 --- a/authbridge/cmd/abctl/go.sum +++ b/authbridge/cmd/abctl/go.sum @@ -26,29 +26,8 @@ github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfa github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U= github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= -github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= -github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= -github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/lestrrat-go/blackmagic v1.0.3 h1:94HXkVLxkZO9vJI/w2u1T0DAoprShFd13xtnSINtDWs= -github.com/lestrrat-go/blackmagic v1.0.3/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= -github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= -github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= -github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k= -github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= -github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= -github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVfecA= -github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU= -github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= -github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -63,21 +42,10 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= -github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -86,8 +54,3 @@ golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/authbridge/cmd/authbridge-envoy/Dockerfile b/authbridge/cmd/authbridge-envoy/Dockerfile new file mode 100644 index 000000000..da19a141f --- /dev/null +++ b/authbridge/cmd/authbridge-envoy/Dockerfile @@ -0,0 +1,53 @@ +# AuthBridge envoy-sidecar combined image — Envoy + authbridge-envoy +# (ext_proc) + spiffe-helper in a single container. +# +# Spiffe-helper starts conditionally based on the SPIRE_ENABLED env var +# (set by the operator when the workload opts into SPIRE identity). +# +# Build context: ./authbridge (needs access to authlib/ and cmd/authbridge-envoy/) + +# Stage 1: Build authbridge-envoy binary. +# `-ldflags="-s -w"` drops the symbol table and DWARF debug info to +# shave ~30% off the binary. Go's runtime keeps `pclntab` separately, +# so panic stack traces still show function names. +FROM golang:1.24-alpine@sha256:8bee1901f1e530bfb4a7850aa7a479d17ae3a18beb6e09064ed54cfd245b7191 AS builder + +RUN apk add --no-cache git binutils + +WORKDIR /app + +COPY authlib/ authlib/ +COPY cmd/authbridge-envoy/ cmd/authbridge-envoy/ + +ENV GOWORK=off +RUN cd cmd/authbridge-envoy && CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /authbridge-envoy . + +# Stage 2: Get Envoy binary. Already stripped upstream; nothing to do. +FROM docker.io/envoyproxy/envoy:v1.37.1 AS envoy-source + +# Stage 3: Pull spiffe-helper from upstream image +FROM ghcr.io/spiffe/spiffe-helper:0.11.0 AS spiffe-source + +# Stage 4: Strip spiffe-helper. The upstream image ships an unstripped +# binary (~23 MB); strip removes ~7 MB of symbol/debug data. +FROM builder AS spiffe-stripper +COPY --from=spiffe-source /spiffe-helper /spiffe-helper +RUN strip /spiffe-helper + +# Stage 5: Runtime — UBI9-micro (glibc-compatible, ~24 MB, has bash) +FROM registry.access.redhat.com/ubi9/ubi-micro@sha256:2173487b3b72b1a7b11edc908e9bbf1726f9df46a4f78fd6d19a2bab0a701f38 + +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt +COPY --from=envoy-source --chmod=755 /usr/local/bin/envoy /usr/local/bin/envoy +COPY --from=spiffe-stripper --chmod=755 /spiffe-helper /usr/local/bin/spiffe-helper +COPY --from=builder --chmod=755 /authbridge-envoy /usr/local/bin/authbridge-envoy +COPY --chmod=755 cmd/authbridge-envoy/entrypoint.sh /usr/local/bin/entrypoint.sh + +# Envoy writes hot-restart lock files and admin socket here +RUN mkdir -p /tmp/envoy && chmod 775 /tmp/envoy + +USER 1337 + +EXPOSE 15123 15124 9901 9090 + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/authbridge/cmd/authbridge/entrypoint-envoy.sh b/authbridge/cmd/authbridge-envoy/entrypoint.sh old mode 100644 new mode 100755 similarity index 77% rename from authbridge/cmd/authbridge/entrypoint-envoy.sh rename to authbridge/cmd/authbridge-envoy/entrypoint.sh index f86b44b34..981e424b5 --- a/authbridge/cmd/authbridge/entrypoint-envoy.sh +++ b/authbridge/cmd/authbridge-envoy/entrypoint.sh @@ -2,12 +2,12 @@ set -eu # AuthBridge envoy-sidecar combined entrypoint with process supervision. -# Manages: spiffe-helper (optional), authbridge (ext_proc), envoy. +# Manages: spiffe-helper (optional), authbridge-envoy (ext_proc), envoy. # # Startup order: # 1. spiffe-helper (background, only when SPIRE_ENABLED=true) -# 2. authbridge (background) — gRPC ext_proc listener -# 3. envoy (background) — calls authbridge over ext_proc +# 2. authbridge-envoy (background) — gRPC ext_proc listener +# 3. envoy (background) — calls authbridge-envoy over ext_proc # # Process management: PID 1 (this shell) supervises every long-running # critical process. If any critical process exits, the others are killed @@ -32,12 +32,12 @@ if [ "${SPIRE_ENABLED:-}" = "true" ]; then CRITICAL_PIDS="$CRITICAL_PIDS $!" fi -# --- Phase 2: authbridge (ext_proc gRPC server) --- -echo "[entrypoint] Starting authbridge..." -/usr/local/bin/authbridge "$@" & +# --- Phase 2: authbridge-envoy (ext_proc gRPC server) --- +echo "[entrypoint] Starting authbridge-envoy..." +/usr/local/bin/authbridge-envoy "$@" & CRITICAL_PIDS="$CRITICAL_PIDS $!" -# Give authbridge a moment to bind the gRPC listener before Envoy connects +# Give authbridge-envoy a moment to bind the gRPC listener before Envoy connects sleep 2 # --- Phase 3: Envoy --- diff --git a/authbridge/cmd/authbridge-envoy/go.mod b/authbridge/cmd/authbridge-envoy/go.mod index 1cd98502b..690b147eb 100644 --- a/authbridge/cmd/authbridge-envoy/go.mod +++ b/authbridge/cmd/authbridge-envoy/go.mod @@ -7,7 +7,6 @@ toolchain go1.24.5 require ( github.com/envoyproxy/go-control-plane/envoy v1.37.0 github.com/kagenti/kagenti-extensions/authbridge/authlib v0.0.0-00010101000000-000000000000 - github.com/kagenti/kagenti-extensions/authbridge/cmd/authbridge v0.0.0-00010101000000-000000000000 google.golang.org/grpc v1.80.0 ) @@ -36,5 +35,3 @@ require ( ) replace github.com/kagenti/kagenti-extensions/authbridge/authlib => ../../authlib - -replace github.com/kagenti/kagenti-extensions/authbridge/cmd/authbridge => ../authbridge diff --git a/authbridge/cmd/authbridge-envoy/main.go b/authbridge/cmd/authbridge-envoy/main.go index 4aeafcc2b..15879b203 100644 --- a/authbridge/cmd/authbridge-envoy/main.go +++ b/authbridge/cmd/authbridge-envoy/main.go @@ -1,7 +1,14 @@ -// Package main is the envoy-sidecar lite binary: envoy-sidecar mode -// only (ext_proc), with jwt-validation + token-exchange as the only -// plugins. For the full-featured all-modes binary, see cmd/authbridge. -// For no-Envoy / HTTP-proxy-only, see cmd/authbridge-proxy. +// Package main is the envoy-sidecar authbridge binary: an ext_proc +// gRPC server intended to run alongside Envoy in a sidecar (or as a +// shared service hooked into Envoy's external_processor filter), with +// the full plugin set compiled in (jwt-validation, token-exchange, +// a2a-parser, mcp-parser, inference-parser). +// +// Mode is hardcoded to envoy-sidecar; YAML configs that specify a +// different mode are rejected at boot. For proxy-sidecar mode (HTTP +// forward/reverse proxies, no Envoy), use cmd/authbridge-proxy. For a +// size-optimized proxy-sidecar build with parsers dropped, use +// cmd/authbridge-lite. package main import ( @@ -35,10 +42,14 @@ import ( // Only the ext_proc listener is compiled in (no ext_authz, no // HTTP proxies). - "github.com/kagenti/kagenti-extensions/authbridge/cmd/authbridge/listener/extproc" + "github.com/kagenti/kagenti-extensions/authbridge/authlib/listener/extproc" - // Only two plugins: drop the parsers and token-broker. + // Plugins. Auth gates first, then the protocol parsers that + // supply session-event context for abctl. + _ "github.com/kagenti/kagenti-extensions/authbridge/authlib/plugins/a2aparser" + _ "github.com/kagenti/kagenti-extensions/authbridge/authlib/plugins/inferenceparser" _ "github.com/kagenti/kagenti-extensions/authbridge/authlib/plugins/jwtvalidation" + _ "github.com/kagenti/kagenti-extensions/authbridge/authlib/plugins/mcpparser" _ "github.com/kagenti/kagenti-extensions/authbridge/authlib/plugins/tokenexchange" ) diff --git a/authbridge/cmd/authbridge-lite/Dockerfile b/authbridge/cmd/authbridge-lite/Dockerfile new file mode 100644 index 000000000..b194f8b5d --- /dev/null +++ b/authbridge/cmd/authbridge-lite/Dockerfile @@ -0,0 +1,53 @@ +# AuthBridge proxy-sidecar lite combined image — authbridge-lite +# (jwt-validation + token-exchange only, no parsers) + spiffe-helper +# in a single container. +# +# Same listener layout as the full authbridge-proxy image, but with +# the a2a / mcp / inference parser plugins dropped to optimize binary +# size for deployments that don't need protocol-aware session events +# in abctl. Functionally identical for inbound JWT validation + +# outbound token exchange. +# +# Spiffe-helper starts conditionally based on the SPIRE_ENABLED env var. +# +# Build context: ./authbridge (needs access to authlib/ and cmd/authbridge-lite/) + +# Stage 1: Build authbridge-lite binary. +# `-ldflags="-s -w"` drops the symbol table and DWARF debug info to +# shave ~30% off the binary. Go's runtime keeps `pclntab` separately, +# so panic stack traces still show function names. +FROM golang:1.24-alpine@sha256:8bee1901f1e530bfb4a7850aa7a479d17ae3a18beb6e09064ed54cfd245b7191 AS builder + +RUN apk add --no-cache git binutils + +WORKDIR /app + +COPY authlib/ authlib/ +COPY cmd/authbridge-lite/ cmd/authbridge-lite/ + +ENV GOWORK=off +RUN cd cmd/authbridge-lite && CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /authbridge-lite . + +# Stage 2: Pull spiffe-helper from upstream image +FROM ghcr.io/spiffe/spiffe-helper:0.11.0 AS spiffe-source + +# Stage 3: Strip spiffe-helper. The upstream image ships an unstripped +# binary (~23 MB); strip removes ~7 MB of symbol/debug data. +FROM builder AS spiffe-stripper +COPY --from=spiffe-source /spiffe-helper /spiffe-helper +RUN strip /spiffe-helper + +# Stage 4: Runtime — alpine (has bash, ~5 MB base) +FROM alpine:3.20@sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd1367effd16dc0d06d + +RUN apk add --no-cache bash ca-certificates + +COPY --from=spiffe-stripper --chmod=755 /spiffe-helper /usr/local/bin/spiffe-helper +COPY --from=builder --chmod=755 /authbridge-lite /usr/local/bin/authbridge-lite +COPY --chmod=755 cmd/authbridge-lite/entrypoint.sh /usr/local/bin/entrypoint.sh + +USER 1001 + +EXPOSE 8080 8081 9091 9093 9094 + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/authbridge/cmd/authbridge-lite/entrypoint.sh b/authbridge/cmd/authbridge-lite/entrypoint.sh new file mode 100755 index 000000000..e558935d5 --- /dev/null +++ b/authbridge/cmd/authbridge-lite/entrypoint.sh @@ -0,0 +1,48 @@ +#!/bin/bash +set -eu + +# AuthBridge proxy-sidecar lite combined entrypoint with process supervision. +# Manages: spiffe-helper (optional), authbridge-lite. +# +# Startup order: +# 1. spiffe-helper (background, only when SPIRE_ENABLED=true) +# 2. authbridge-lite (background) — HTTP forward + reverse proxies +# +# Process management: PID 1 (this shell) supervises every long-running +# critical process. If any critical process exits, the others are killed +# and the container exits non-zero so Kubernetes restarts it. SIGTERM / +# SIGINT are forwarded for graceful shutdown. + +CRITICAL_PIDS="" + +cleanup() { + echo "[entrypoint] Received signal, shutting down..." + # shellcheck disable=SC2086 + kill $CRITICAL_PIDS 2>/dev/null || true + wait + exit 0 +} +trap cleanup TERM INT + +# --- Phase 1: spiffe-helper (conditional) --- +if [ "${SPIRE_ENABLED:-}" = "true" ]; then + echo "[entrypoint] Starting spiffe-helper..." + /usr/local/bin/spiffe-helper -config=/etc/spiffe-helper/helper.conf run & + CRITICAL_PIDS="$CRITICAL_PIDS $!" +fi + +# --- Phase 2: authbridge-lite (HTTP forward + reverse proxies) --- +echo "[entrypoint] Starting authbridge-lite..." +/usr/local/bin/authbridge-lite "$@" & +CRITICAL_PIDS="$CRITICAL_PIDS $!" + +# Block until any critical process exits, then terminate the container +# so Kubernetes restarts the pod. +# shellcheck disable=SC2086 +wait -n $CRITICAL_PIDS +EXIT_CODE=$? +echo "[entrypoint] A critical process exited unexpectedly (exit code $EXIT_CODE), terminating container" +# shellcheck disable=SC2086 +kill $CRITICAL_PIDS 2>/dev/null || true +wait +exit 1 diff --git a/authbridge/cmd/authbridge/go.mod b/authbridge/cmd/authbridge-lite/go.mod similarity index 58% rename from authbridge/cmd/authbridge/go.mod rename to authbridge/cmd/authbridge-lite/go.mod index f91969cd3..fa983a2ea 100644 --- a/authbridge/cmd/authbridge/go.mod +++ b/authbridge/cmd/authbridge-lite/go.mod @@ -1,20 +1,13 @@ -module github.com/kagenti/kagenti-extensions/authbridge/cmd/authbridge +module github.com/kagenti/kagenti-extensions/authbridge/cmd/authbridge-lite go 1.24.0 toolchain go1.24.5 -require ( - github.com/envoyproxy/go-control-plane/envoy v1.37.0 - github.com/kagenti/kagenti-extensions/authbridge/authlib v0.0.0-00010101000000-000000000000 - google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 - google.golang.org/grpc v1.80.0 -) +require github.com/kagenti/kagenti-extensions/authbridge/authlib v0.0.0-00010101000000-000000000000 require ( - github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect - github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.10.3 // indirect @@ -24,13 +17,9 @@ require ( github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/jwx/v2 v2.1.6 // indirect github.com/lestrrat-go/option v1.0.1 // indirect - github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/segmentio/asm v1.2.0 // indirect golang.org/x/crypto v0.48.0 // indirect - golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.41.0 // indirect - golang.org/x/text v0.34.0 // indirect - google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/authbridge/cmd/authbridge-lite/go.sum b/authbridge/cmd/authbridge-lite/go.sum new file mode 100644 index 000000000..d338f51ea --- /dev/null +++ b/authbridge/cmd/authbridge-lite/go.sum @@ -0,0 +1,41 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/lestrrat-go/blackmagic v1.0.3 h1:94HXkVLxkZO9vJI/w2u1T0DAoprShFd13xtnSINtDWs= +github.com/lestrrat-go/blackmagic v1.0.3/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k= +github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVfecA= +github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/authbridge/cmd/authbridge-lite/main.go b/authbridge/cmd/authbridge-lite/main.go new file mode 100644 index 000000000..93a566cf0 --- /dev/null +++ b/authbridge/cmd/authbridge-lite/main.go @@ -0,0 +1,272 @@ +// Package main is the "lite" authbridge binary: proxy-sidecar mode +// only (no Envoy), with jwt-validation + token-exchange as the only +// plugins compiled in. Optimizes for binary size by dropping the +// gRPC / envoy go-control-plane dependency tree and the +// a2a/mcp/inference parser plugins. +// +// For full-featured proxy-sidecar deployments (with the parsers, so +// abctl can render protocol-aware session events), use +// cmd/authbridge-proxy. For envoy-sidecar mode, use cmd/authbridge-envoy. +package main + +import ( + "context" + "flag" + "fmt" + "log" + "log/slog" + "net/http" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "github.com/kagenti/kagenti-extensions/authbridge/authlib/auth" + "github.com/kagenti/kagenti-extensions/authbridge/authlib/config" + "github.com/kagenti/kagenti-extensions/authbridge/authlib/observe" + "github.com/kagenti/kagenti-extensions/authbridge/authlib/pipeline" + "github.com/kagenti/kagenti-extensions/authbridge/authlib/plugins" + "github.com/kagenti/kagenti-extensions/authbridge/authlib/reloader" + "github.com/kagenti/kagenti-extensions/authbridge/authlib/session" + "github.com/kagenti/kagenti-extensions/authbridge/authlib/sessionapi" + + // Only HTTP listeners are compiled in: no extproc/extauthz + // (no gRPC, no envoy types). + "github.com/kagenti/kagenti-extensions/authbridge/authlib/listener/forwardproxy" + "github.com/kagenti/kagenti-extensions/authbridge/authlib/listener/reverseproxy" + + // Only two plugins: drop the parsers and token-broker. + _ "github.com/kagenti/kagenti-extensions/authbridge/authlib/plugins/jwtvalidation" + _ "github.com/kagenti/kagenti-extensions/authbridge/authlib/plugins/tokenexchange" +) + +var logLevel = new(slog.LevelVar) + +func initLogging() { + switch strings.ToLower(os.Getenv("LOG_LEVEL")) { + case "debug": + logLevel.Set(slog.LevelDebug) + case "warn": + logLevel.Set(slog.LevelWarn) + case "error": + logLevel.Set(slog.LevelError) + default: + logLevel.Set(slog.LevelInfo) + } + slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: logLevel}))) +} + +func startSignalToggle() { + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGUSR1) + go func() { + for range sigCh { + if logLevel.Level() == slog.LevelDebug { + logLevel.Set(slog.LevelInfo) + slog.Info("log level toggled to INFO (send SIGUSR1 to switch back to DEBUG)") + } else { + logLevel.Set(slog.LevelDebug) + slog.Info("log level toggled to DEBUG (send SIGUSR1 to switch back to INFO)") + } + } + }() +} + +func main() { + configPath := flag.String("config", "", "path to config YAML file") + flag.Parse() + + initLogging() + startSignalToggle() + + if *configPath == "" { + log.Fatal("--config is required") + } + + // This binary is hardcoded to proxy-sidecar. Rejecting other modes + // early gives operators a clear boot-time error instead of silently + // misbehaving (e.g., YAML says envoy-sidecar but binary can't + // serve ext_proc). + buildPipelines := func() (*pipeline.Pipeline, *pipeline.Pipeline, *config.Config, error) { + c, err := config.Load(*configPath) + if err != nil { + return nil, nil, nil, err + } + if c.Mode != "" && c.Mode != config.ModeProxySidecar { + return nil, nil, nil, fmt.Errorf( + "authbridge-lite supports only mode=%q (got %q); use cmd/authbridge-envoy for envoy-sidecar mode", + config.ModeProxySidecar, c.Mode) + } + c.Mode = config.ModeProxySidecar + config.ApplyPreset(c) + if err := config.Validate(c); err != nil { + return nil, nil, nil, err + } + in, err := plugins.Build(c.Pipeline.Inbound.Plugins) + if err != nil { + return nil, nil, nil, fmt.Errorf("inbound: %w", err) + } + out, err := plugins.Build(c.Pipeline.Outbound.Plugins) + if err != nil { + return nil, nil, nil, fmt.Errorf("outbound: %w", err) + } + return in, out, c, nil + } + + inboundPipeline, outboundPipeline, cfg, err := buildPipelines() + if err != nil { + log.Fatalf("initial pipeline build: %v", err) + } + + initCtx, initCancel := context.WithTimeout(context.Background(), 60*time.Second) + defer initCancel() + if err := inboundPipeline.Start(initCtx); err != nil { + log.Fatalf("inbound pipeline Start: %v", err) + } + if err := outboundPipeline.Start(initCtx); err != nil { + log.Fatalf("outbound pipeline Start: %v", err) + } + + inboundH := pipeline.NewHolder(inboundPipeline) + outboundH := pipeline.NewHolder(outboundPipeline) + + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + rld := reloader.New(*configPath, inboundH, outboundH, buildPipelines, cfg) + if err := rld.Start(ctx); err != nil { + log.Fatalf("reloader: %v", err) + } + + var sessions *session.Store + if cfg.Session.SessionEnabled() { + ttl := 30 * time.Minute + if cfg.Session.TTL != "" { + if d, err := time.ParseDuration(cfg.Session.TTL); err == nil { + ttl = d + } else { + slog.Warn("invalid session.ttl, using default", "value", cfg.Session.TTL, "error", err) + } + } + maxEvents := 100 + if cfg.Session.MaxEvents > 0 { + maxEvents = cfg.Session.MaxEvents + } + maxSessions := 100 + if cfg.Session.MaxSessions > 0 { + maxSessions = cfg.Session.MaxSessions + } + sessions = session.New(ttl, maxEvents, maxSessions) + slog.Info("session tracking enabled", "ttl", ttl, "maxEvents", maxEvents, "maxSessions", maxSessions) + } else { + slog.Info("session tracking disabled") + } + + var httpServers []*http.Server + + // Proxy-sidecar: reverse proxy on the inbound path + forward proxy + // on the outbound path. + rpSrv, err := reverseproxy.NewServer(inboundH, sessions, cfg.Listener.ReverseProxyBackend) + if err != nil { + log.Fatalf("creating reverse proxy: %v", err) + } + httpServers = append(httpServers, startHTTPServer("reverse-proxy", rpSrv.Handler(), cfg.Listener.ReverseProxyAddr)) + httpServers = append(httpServers, startHTTPServer("forward-proxy", forwardproxy.NewServer(outboundH, sessions).Handler(), cfg.Listener.ForwardProxyAddr)) + + statsProvider := func() *auth.Stats { + sources := plugins.CollectStats(inboundH.Load()) + sources = append(sources, plugins.CollectStats(outboundH.Load())...) + return auth.MergeStats(sources...) + } + statSrv := startStatServer(cfg, rld.ConfigProvider(), statsProvider, rld.Handler()) + + var sessionAPISrv *sessionapi.Server + if cfg.Listener.SessionAPIAddr != "" && sessions != nil { + sessionAPISrv = sessionapi.New( + cfg.Listener.SessionAPIAddr, + sessions, + sessionapi.WithPipelines(inboundH, outboundH), + ) + go func() { + slog.Warn("session API listening — UNAUTHENTICATED; contains raw user content; never expose via ingress", + "addr", cfg.Listener.SessionAPIAddr) + if err := sessionAPISrv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("session API: %v", err) + } + }() + } + + slog.Info("authbridge-lite starting", "mode", cfg.Mode, "logLevel", logLevel.Level().String()) + + go func() { + mux := http.NewServeMux() + mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + mux.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) { + if name := inboundH.NotReadyPlugin(); name != "" { + http.Error(w, "inbound plugin not ready: "+name, http.StatusServiceUnavailable) + return + } + if name := outboundH.NotReadyPlugin(); name != "" { + http.Error(w, "outbound plugin not ready: "+name, http.StatusServiceUnavailable) + return + } + w.WriteHeader(http.StatusOK) + }) + slog.Info("health server listening", "addr", ":9091") + if err := http.ListenAndServe(":9091", mux); err != nil { + slog.Warn("health server failed", "error", err) + } + }() + + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT) + sig := <-sigCh + slog.Info("shutting down", "signal", sig) + + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 15*time.Second) + defer shutdownCancel() + + for _, srv := range httpServers { + srv.Shutdown(shutdownCtx) + } + statSrv.Shutdown(shutdownCtx) + if sessionAPISrv != nil { + sessionAPISrv.Shutdown(shutdownCtx) + } + + outboundPipeline.Stop(shutdownCtx) + inboundPipeline.Stop(shutdownCtx) + + if sessions != nil { + sessions.Close() + } +} + +func startHTTPServer(name string, handler http.Handler, addr string) *http.Server { + srv := &http.Server{ + Addr: addr, + Handler: handler, + ReadHeaderTimeout: 10 * time.Second, + } + go func() { + slog.Info("HTTP server listening", "name", name, "addr", addr) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("%s serve: %v", name, err) + } + }() + return srv +} + +func startStatServer(cfg *config.Config, cfgProvider observe.ConfigProvider, statsProvider observe.StatsProvider, reloadStatus http.Handler) *observe.StatServer { + srv := observe.NewStatServer(cfg.Stats.StatsAddress, cfgProvider, statsProvider, + observe.WithReloadStatus(reloadStatus)) + go func() { + slog.Info("stat server listening", "addr", cfg.Stats.StatsAddress) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("stat server: %v", err) + } + }() + return srv +} diff --git a/authbridge/cmd/authbridge/Dockerfile.proxy b/authbridge/cmd/authbridge-proxy/Dockerfile similarity index 57% rename from authbridge/cmd/authbridge/Dockerfile.proxy rename to authbridge/cmd/authbridge-proxy/Dockerfile index f171d6561..64884c80a 100644 --- a/authbridge/cmd/authbridge/Dockerfile.proxy +++ b/authbridge/cmd/authbridge-proxy/Dockerfile @@ -1,26 +1,27 @@ -# AuthBridge proxy-sidecar combined image — authbridge-proxy (no Envoy, -# no gRPC) + spiffe-helper in a single container. +# AuthBridge proxy-sidecar combined image — authbridge-proxy (HTTP +# forward + reverse proxies, no Envoy / no gRPC, full plugin set) + +# spiffe-helper in a single container. # # The agent uses HTTP_PROXY env var to route outbound traffic through # the forward proxy; inbound traffic flows through the reverse proxy # that takes over the agent's original port. Spiffe-helper starts # conditionally based on the SPIRE_ENABLED env var. # -# Image lineage: replaces the older `authbridge-light` image and is -# the new home for the default proxy-sidecar deployment shape. -# # Uses alpine base (~5 MB) — needs bash for the supervisor entrypoint # and glibc-compatible libc (musl is fine for both authbridge-proxy # (CGO_ENABLED=0 static) and spiffe-helper (statically linked Go)). # No shell would also work via a Go supervisor, but bash matches the -# Dockerfile.envoy pattern and keeps both images consistent. +# envoy-sidecar Dockerfile pattern and keeps both images consistent. # # Build context: ./authbridge (needs access to authlib/ and cmd/authbridge-proxy/) -# Stage 1: Build authbridge-proxy binary +# Stage 1: Build authbridge-proxy binary. +# `-ldflags="-s -w"` drops the symbol table and DWARF debug info to +# shave ~30% off the binary. Go's runtime keeps `pclntab` separately, +# so panic stack traces still show function names. FROM golang:1.24-alpine@sha256:8bee1901f1e530bfb4a7850aa7a479d17ae3a18beb6e09064ed54cfd245b7191 AS builder -RUN apk add --no-cache git +RUN apk add --no-cache git binutils WORKDIR /app @@ -28,19 +29,25 @@ COPY authlib/ authlib/ COPY cmd/authbridge-proxy/ cmd/authbridge-proxy/ ENV GOWORK=off -RUN cd cmd/authbridge-proxy && CGO_ENABLED=0 GOOS=linux go build -o /authbridge-proxy . +RUN cd cmd/authbridge-proxy && CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /authbridge-proxy . -# Stage 2: Get spiffe-helper binary +# Stage 2: Pull spiffe-helper from upstream image FROM ghcr.io/spiffe/spiffe-helper:0.11.0 AS spiffe-source -# Stage 3: Runtime — alpine (has bash, ~5 MB base) +# Stage 3: Strip spiffe-helper. The upstream image ships an unstripped +# binary (~23 MB); strip removes ~7 MB of symbol/debug data. +FROM builder AS spiffe-stripper +COPY --from=spiffe-source /spiffe-helper /spiffe-helper +RUN strip /spiffe-helper + +# Stage 4: Runtime — alpine (has bash, ~5 MB base) FROM alpine:3.20@sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd1367effd16dc0d06d RUN apk add --no-cache bash ca-certificates -COPY --from=spiffe-source --chmod=755 /spiffe-helper /usr/local/bin/spiffe-helper +COPY --from=spiffe-stripper --chmod=755 /spiffe-helper /usr/local/bin/spiffe-helper COPY --from=builder --chmod=755 /authbridge-proxy /usr/local/bin/authbridge-proxy -COPY --chmod=755 cmd/authbridge/entrypoint-proxy.sh /usr/local/bin/entrypoint.sh +COPY --chmod=755 cmd/authbridge-proxy/entrypoint.sh /usr/local/bin/entrypoint.sh USER 1001 diff --git a/authbridge/cmd/authbridge/entrypoint-proxy.sh b/authbridge/cmd/authbridge-proxy/entrypoint.sh old mode 100644 new mode 100755 similarity index 100% rename from authbridge/cmd/authbridge/entrypoint-proxy.sh rename to authbridge/cmd/authbridge-proxy/entrypoint.sh diff --git a/authbridge/cmd/authbridge-proxy/main.go b/authbridge/cmd/authbridge-proxy/main.go index b294c0200..23ace6e5e 100644 --- a/authbridge/cmd/authbridge-proxy/main.go +++ b/authbridge/cmd/authbridge-proxy/main.go @@ -1,10 +1,12 @@ -// Package main is the "lite" authbridge binary: proxy-sidecar mode -// only (no Envoy), with jwt-validation + token-exchange as the only -// plugins compiled in. Optimizes for binary size by dropping the -// gRPC / envoy go-control-plane dependency tree and the -// a2a/mcp/inference parser plugins. +// Package main is the proxy-sidecar authbridge binary: HTTP forward +// proxy + reverse proxy, no Envoy / gRPC dependencies, full plugin set +// (jwt-validation, token-exchange, a2a-parser, mcp-parser, +// inference-parser). // -// For the full-featured batteries-included binary, see cmd/authbridge. +// Mode is hardcoded to proxy-sidecar; YAML configs that specify a +// different mode are rejected at boot. For envoy-sidecar mode, use +// cmd/authbridge-envoy. For a size-optimized build with parsers +// dropped, use cmd/authbridge-lite. package main import ( @@ -34,8 +36,12 @@ import ( "github.com/kagenti/kagenti-extensions/authbridge/authlib/listener/forwardproxy" "github.com/kagenti/kagenti-extensions/authbridge/authlib/listener/reverseproxy" - // Only two plugins: drop the parsers and token-broker. + // Plugins. Auth gates first, then the protocol parsers that + // supply session-event context for abctl. + _ "github.com/kagenti/kagenti-extensions/authbridge/authlib/plugins/a2aparser" + _ "github.com/kagenti/kagenti-extensions/authbridge/authlib/plugins/inferenceparser" _ "github.com/kagenti/kagenti-extensions/authbridge/authlib/plugins/jwtvalidation" + _ "github.com/kagenti/kagenti-extensions/authbridge/authlib/plugins/mcpparser" _ "github.com/kagenti/kagenti-extensions/authbridge/authlib/plugins/tokenexchange" ) @@ -93,7 +99,7 @@ func main() { } if c.Mode != "" && c.Mode != config.ModeProxySidecar { return nil, nil, nil, fmt.Errorf( - "authbridge-proxy supports only mode=%q (got %q); use cmd/authbridge for other modes", + "authbridge-proxy supports only mode=%q (got %q); use cmd/authbridge-envoy for envoy-sidecar mode", config.ModeProxySidecar, c.Mode) } c.Mode = config.ModeProxySidecar diff --git a/authbridge/cmd/authbridge/Dockerfile.envoy b/authbridge/cmd/authbridge/Dockerfile.envoy deleted file mode 100644 index 8828e8022..000000000 --- a/authbridge/cmd/authbridge/Dockerfile.envoy +++ /dev/null @@ -1,47 +0,0 @@ -# AuthBridge envoy-sidecar combined image — Envoy + authbridge (ext_proc) -# + spiffe-helper in a single container. -# -# Spiffe-helper starts conditionally based on the SPIRE_ENABLED env var -# (set by the operator when the workload opts into SPIRE identity). -# -# Image lineage: replaces the older `authbridge` (envoy + everything, -# UBI9-micro base ~140 MB combined) and `authbridge-unified` images. -# -# Build context: ./authbridge (needs access to authlib/ and cmd/authbridge/) - -# Stage 1: Build authbridge binary -FROM golang:1.24-alpine@sha256:8bee1901f1e530bfb4a7850aa7a479d17ae3a18beb6e09064ed54cfd245b7191 AS builder - -RUN apk add --no-cache git - -WORKDIR /app - -COPY authlib/ authlib/ -COPY cmd/authbridge/ cmd/authbridge/ - -ENV GOWORK=off -RUN cd cmd/authbridge && CGO_ENABLED=0 GOOS=linux go build -o /authbridge . - -# Stage 2: Get Envoy binary -FROM docker.io/envoyproxy/envoy:v1.37.1 AS envoy-source - -# Stage 3: Get spiffe-helper binary -FROM ghcr.io/spiffe/spiffe-helper:0.11.0 AS spiffe-source - -# Stage 4: Runtime — UBI9-micro (glibc-compatible, ~24 MB, has bash) -FROM registry.access.redhat.com/ubi9/ubi-micro@sha256:2173487b3b72b1a7b11edc908e9bbf1726f9df46a4f78fd6d19a2bab0a701f38 - -COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt -COPY --from=envoy-source --chmod=755 /usr/local/bin/envoy /usr/local/bin/envoy -COPY --from=spiffe-source --chmod=755 /spiffe-helper /usr/local/bin/spiffe-helper -COPY --from=builder --chmod=755 /authbridge /usr/local/bin/authbridge -COPY --chmod=755 cmd/authbridge/entrypoint-envoy.sh /usr/local/bin/entrypoint.sh - -# Envoy writes hot-restart lock files and admin socket here -RUN mkdir -p /tmp/envoy && chmod 775 /tmp/envoy - -USER 1337 - -EXPOSE 15123 15124 9901 9090 - -ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/authbridge/cmd/authbridge/README.md b/authbridge/cmd/authbridge/README.md deleted file mode 100644 index db8a63580..000000000 --- a/authbridge/cmd/authbridge/README.md +++ /dev/null @@ -1,295 +0,0 @@ -# AuthBridge Binary - -A single binary that replaces three separate codebases (go-processor, waypoint, klaviger) with a unified auth proxy supporting three deployment modes. - -## Images - -| Image | Dockerfile | Size | Contents | -|-------|-----------|------|----------| -| `authbridge-envoy` | `Dockerfile` | 140 MB | Envoy + authbridge (UBI9-micro) | -| `authbridge-light` | `Dockerfile.light` | 29 MB | authbridge only (distroless) | -| `authbridge-unified` | `Dockerfile` | 140 MB | Deprecated alias (same image as `authbridge-envoy`) | - -## Modes - -| Mode | Image | Interception | Listeners | -|------|-------|-------------|-----------| -| `envoy-sidecar` | `authbridge-envoy` | Envoy iptables + ext_proc | gRPC ext_proc on :9090 | -| `proxy-sidecar` | `authbridge-light` | HTTP_PROXY env + port-stealing | HTTP reverse proxy + forward proxy | -| `waypoint` | `authbridge-light` | Istio ambient + ext_authz | gRPC ext_authz + HTTP forward proxy | - -### proxy-sidecar port reassignment - -In proxy-sidecar mode, the kagenti-operator webhook transparently reassigns the agent's port to interpose the reverse proxy: - -1. The reverse proxy takes over the agent's original port (e.g., `:8000`) -2. The agent is moved to a free port (e.g., `:8001`) via `PORT` env var -3. `HTTP_PROXY`/`HTTPS_PROXY` env vars are injected into the agent container -4. The Service targetPort remains unchanged — traffic flows through the reverse proxy - -The operator passes the dynamically assigned ports via env vars (`REVERSE_PROXY_ADDR`, `REVERSE_PROXY_BACKEND`, `FORWARD_PROXY_ADDR`) which are expanded via `${...}` in the config YAML. - -## Selecting a Mode - -The operator selects the mode via annotation on the workload's pod template: - -```yaml -# Default (envoy-sidecar) — no annotation needed -metadata: - labels: - kagenti.io/type: agent - -# Proxy-sidecar mode -metadata: - labels: - kagenti.io/type: agent - annotations: - kagenti.io/authbridge-mode: "proxy-sidecar" -``` - -## Building - -All builds run from the **repo root** with `authbridge/` as the build context: - -```bash -# Envoy variant (envoy-sidecar mode) -podman build -f authbridge/cmd/authbridge/Dockerfile -t authbridge-envoy:local authbridge/ - -# Lightweight variant (proxy-sidecar / waypoint modes) -podman build -f authbridge/cmd/authbridge/Dockerfile.light -t authbridge-light:local authbridge/ - -# Load into Kind -kind load docker-image authbridge-envoy:local --name kagenti -kind load docker-image authbridge-light:local --name kagenti -``` - -The Envoy image contains both Envoy and the authbridge binary. The entrypoint starts both processes with `wait -n` supervision (if either dies, the container restarts). The light image runs the authbridge binary directly as the entrypoint. - -## Running - -```bash -authbridge --mode envoy-sidecar --config /etc/authbridge/config.yaml -``` - -The `--mode` flag can also be set in the YAML config. The flag overrides the config file value. - -## Configuration - -YAML with `${ENV_VAR}` expansion. Undefined env vars are preserved as-is (not expanded to empty). - -The runtime config is intentionally thin — it covers the mode, the listener addresses, session tracking, and the plugin pipeline. Everything a plugin needs (issuer, token URL, credentials, routes, bypass paths) lives under its own `config:` block inside the pipeline entry. See [`docs/plugin-reference.md`](../../docs/plugin-reference.md) for the per-plugin decode / defaults / validate convention. - -### envoy-sidecar mode - -Drop-in replacement for `envoy-with-processor`. Used as a sidecar alongside Envoy in each agent pod. - -Minimum viable config — every Kagenti-convention default applies: - -```yaml -mode: envoy-sidecar - -pipeline: - inbound: - plugins: - - name: jwt-validation - config: - issuer: "${ISSUER}" - - outbound: - plugins: - - name: token-exchange - config: - keycloak_url: "${KEYCLOAK_URL}" - keycloak_realm: "${KEYCLOAK_REALM}" - identity: - type: spiffe # spiffe or client-secret -``` - -Defaults filled in by the plugins (override any by setting them explicitly): - -| Plugin | Default | Value | -|---|---|---| -| jwt-validation | `jwks_url` | `/protocol/openid-connect/certs` | -| jwt-validation | `audience_file` | `/shared/client-id.txt` (client-registration convention) | -| jwt-validation | `bypass_paths` | `/.well-known/*`, `/healthz`, `/readyz`, `/livez` | -| jwt-validation | `audience_mode` | `static` | -| token-exchange | `token_url` | `/realms//protocol/openid-connect/token` | -| token-exchange | `default_policy` | `passthrough` | -| token-exchange | `no_token_policy` | `deny` | -| token-exchange | `routes.file` | `/etc/authproxy/routes.yaml` | -| token-exchange | `identity.client_id_file` | `/shared/client-id.txt` (both types) | -| token-exchange | `identity.client_secret_file` | `/shared/client-secret.txt` (client-secret type) | -| token-exchange | `identity.jwt_svid_path` | `/opt/jwt_svid.token` (spiffe type) | - -Full form — every field spelled out, for deployments that diverge from defaults: - -```yaml -mode: envoy-sidecar - -pipeline: - inbound: - plugins: - - name: jwt-validation - config: - issuer: "${ISSUER}" - jwks_url: "${JWKS_URL}" # optional; derived from issuer - audience_file: "/shared/client-id.txt" # audience written by client-registration - bypass_paths: - - "/.well-known/*" - - "/healthz" - - "/readyz" - - "/livez" - - outbound: - plugins: - - name: token-exchange - config: - token_url: "${TOKEN_URL}" # or derived from keycloak_url + keycloak_realm - keycloak_url: "${KEYCLOAK_URL}" - keycloak_realm: "${KEYCLOAK_REALM}" - default_policy: "passthrough" # passthrough (default) or exchange - no_token_policy: "deny" # deny (default), allow, or client-credentials - identity: - type: spiffe # spiffe or client-secret - client_id_file: "/shared/client-id.txt" - client_secret_file: "/shared/client-secret.txt" - jwt_svid_path: "/opt/jwt_svid.token" - routes: - file: "/etc/authproxy/routes.yaml" # loaded when present, ignored when absent - rules: # inline; merged with file - - host: "target-service.**" - target_audience: "target" - token_scopes: "openid target-aud" -``` - -### waypoint mode - -Shared service for Istio ambient mesh. `jwt-validation` derives audience from destination hostname when `audience_mode: per-host` is set. - -```yaml -mode: waypoint - -pipeline: - inbound: - plugins: - - name: jwt-validation - config: - issuer: "${ISSUER}" - audience_mode: per-host # derive from pctx.Host per request - - outbound: - plugins: - - name: token-exchange - config: - keycloak_url: "${KEYCLOAK_URL}" - keycloak_realm: "${KEYCLOAK_REALM}" - default_policy: "exchange" - identity: - type: client-secret - client_id: "token-exchange-service" - client_secret: "${CLIENT_SECRET}" -``` - -### proxy-sidecar mode - -Sidecar without Envoy. Reverse proxy validates inbound, forward proxy exchanges outbound. - -```yaml -mode: proxy-sidecar - -listener: - reverse_proxy_backend: "http://localhost:8081" - -pipeline: - inbound: - plugins: - - name: jwt-validation - config: - issuer: "${ISSUER}" - audience_file: "/shared/client-id.txt" - - outbound: - plugins: - - name: token-exchange - config: - keycloak_url: "${KEYCLOAK_URL}" - keycloak_realm: "${KEYCLOAK_REALM}" - identity: - type: spiffe - client_id_file: "/shared/client-id.txt" - jwt_svid_path: "/opt/jwt_svid.token" -``` - -### Bare-name plugin entries - -Parsers and any other plugin that doesn't take configuration can appear as a bare name: - -```yaml -pipeline: - outbound: - plugins: - - name: token-exchange - config: { ... } - - mcp-parser # equivalent to { name: mcp-parser } - - inference-parser -``` - -### URL Derivation - -Each plugin derives missing URLs from what you supply: - -| Missing field | Derived from | Example | -|---|---|---| -| `jwt-validation.jwks_url` | `jwt-validation.issuer` | `/protocol/openid-connect/certs` | -| `token-exchange.token_url` | `token-exchange.keycloak_url` + `keycloak_realm` | `http://keycloak:8080/realms/kagenti/protocol/openid-connect/token` | - -Explicit values always take precedence over derived values. - -### Credential File Waiting - -When `audience_file` (jwt-validation) or `client_id_file` / `client_secret_file` / `jwt_svid_path` (token-exchange) are configured, the plugin first attempts a synchronous read at Configure time. If the file isn't readable yet (the common case during pod boot while client-registration is still provisioning), the plugin's `Init` spawns a background poll; once the file appears, `auth.UpdateIdentity` swaps the credentials into the live handler atomically. OnRequest returns 503 for traffic that arrives before credentials land. - -## Logging - -AuthBridge uses Go's `slog` structured logger. The log level is configurable at startup and at runtime. - -### Set level at startup - -Set the `LOG_LEVEL` env var (`debug`, `info`, `warn`, `error`). Default: `info`. - -```bash -# In a deployment -kubectl set env deployment/weather-service -n team1 -c authbridge-proxy LOG_LEVEL=debug - -# Standalone -LOG_LEVEL=debug authbridge --config /etc/authbridge/config.yaml -``` - -### Toggle at runtime (no restart) - -Send `SIGUSR1` to toggle between `info` and `debug`: - -```bash -# The container has no standalone kill/grep binaries — use bash builtins only. -# Match on the full binary path to skip the entrypoint script (also at PID 1). -kubectl exec deploy/weather-service -n team1 -c envoy-proxy -- \ - bash -c 'for f in /proc/[0-9]*/cmdline; do [ -r "$f" ] || continue; c=$(<"$f"); [[ "$c" == /usr/local/bin/authbridge* ]] && kill -USR1 "${f//[!0-9]/}" && break; done' -``` - -Send again to toggle back. The current level is logged on each toggle. - -## Architecture - -``` -cmd/authbridge/ -├── main.go # --mode + --config, starts listeners, graceful shutdown -├── entrypoint.sh # Envoy + authbridge process supervision (wait -n) -├── Dockerfile # Combined Envoy + authbridge image (ubi-minimal) -└── listener/ - ├── extproc/ # Envoy ext_proc gRPC streaming (envoy-sidecar mode) - ├── extauthz/ # Envoy ext_authz gRPC unary (waypoint mode) - ├── forwardproxy/ # HTTP forward proxy (waypoint + proxy-sidecar) - └── reverseproxy/ # HTTP reverse proxy (proxy-sidecar mode) -``` - -Listeners are thin protocol translators (~50-175 lines each). All auth logic lives in `authlib/`. diff --git a/authbridge/cmd/authbridge/go.sum b/authbridge/cmd/authbridge/go.sum deleted file mode 100644 index f63e27c28..000000000 --- a/authbridge/cmd/authbridge/go.sum +++ /dev/null @@ -1,85 +0,0 @@ -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= -github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= -github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= -github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= -github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= -github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= -github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= -github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= -github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= -github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/lestrrat-go/blackmagic v1.0.3 h1:94HXkVLxkZO9vJI/w2u1T0DAoprShFd13xtnSINtDWs= -github.com/lestrrat-go/blackmagic v1.0.3/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= -github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= -github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= -github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k= -github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= -github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= -github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVfecA= -github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU= -github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= -github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= -github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= -github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= -github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= -go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= -go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= -go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= -go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= -go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= -go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= -go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= -go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= -go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= -go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= -gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= -google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/authbridge/cmd/authbridge/main.go b/authbridge/cmd/authbridge/main.go deleted file mode 100644 index 2603321d5..000000000 --- a/authbridge/cmd/authbridge/main.go +++ /dev/null @@ -1,408 +0,0 @@ -// Command authbridge is a unified auth proxy supporting three deployment modes: -// envoy-sidecar (ext_proc), waypoint (ext_authz + forward proxy), and -// proxy-sidecar (reverse proxy + forward proxy). -package main - -import ( - "context" - "errors" - "flag" - "fmt" - "log" - "log/slog" - "net" - "net/http" - "os" - "os/signal" - "strings" - "syscall" - "time" - - authv3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" - extprocv3 "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3" - "google.golang.org/grpc" - "google.golang.org/grpc/health" - healthpb "google.golang.org/grpc/health/grpc_health_v1" - "google.golang.org/grpc/reflection" - - "github.com/kagenti/kagenti-extensions/authbridge/authlib/auth" - "github.com/kagenti/kagenti-extensions/authbridge/authlib/config" - "github.com/kagenti/kagenti-extensions/authbridge/authlib/observe" - "github.com/kagenti/kagenti-extensions/authbridge/authlib/pipeline" - "github.com/kagenti/kagenti-extensions/authbridge/authlib/plugins" - // Bundled plugins register themselves via init() on import. Every - // in-tree plugin now lives in its own subpackage; this main.go is - // the batteries-included "reference" binary that opts into all of - // them. External plugins follow the same pattern — add one - // side-effect import here (or in a plugins_extra.go) to include - // them in this binary's build. Slimmer binaries (e.g. - // cmd/authbridge-proxy) import only the subset they need. - "github.com/kagenti/kagenti-extensions/authbridge/authlib/listener/forwardproxy" - "github.com/kagenti/kagenti-extensions/authbridge/authlib/listener/reverseproxy" - _ "github.com/kagenti/kagenti-extensions/authbridge/authlib/plugins/a2aparser" - _ "github.com/kagenti/kagenti-extensions/authbridge/authlib/plugins/inferenceparser" - _ "github.com/kagenti/kagenti-extensions/authbridge/authlib/plugins/jwtvalidation" - _ "github.com/kagenti/kagenti-extensions/authbridge/authlib/plugins/mcpparser" - _ "github.com/kagenti/kagenti-extensions/authbridge/authlib/plugins/tokenexchange" - "github.com/kagenti/kagenti-extensions/authbridge/authlib/reloader" - "github.com/kagenti/kagenti-extensions/authbridge/authlib/session" - "github.com/kagenti/kagenti-extensions/authbridge/authlib/sessionapi" - "github.com/kagenti/kagenti-extensions/authbridge/cmd/authbridge/listener/extauthz" - "github.com/kagenti/kagenti-extensions/authbridge/cmd/authbridge/listener/extproc" -) - -// logLevel is the dynamic log level, togglable at runtime via SIGUSR1. -var logLevel = new(slog.LevelVar) - -func initLogging() { - // LOG_LEVEL env var sets the initial level: debug, info, warn, error. - // Default: info. Override at runtime with SIGUSR1 (toggles debug/info). - switch strings.ToLower(os.Getenv("LOG_LEVEL")) { - case "debug": - logLevel.Set(slog.LevelDebug) - case "warn": - logLevel.Set(slog.LevelWarn) - case "error": - logLevel.Set(slog.LevelError) - default: - logLevel.Set(slog.LevelInfo) - } - slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: logLevel}))) -} - -func startSignalToggle() { - // SIGUSR1 toggles between info and debug at runtime, regardless of - // the initial LOG_LEVEL (warn/error are treated as "not debug"). - // Usage: kubectl exec -c authbridge-proxy -- kill -USR1 1 - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGUSR1) - go func() { - for range sigCh { - if logLevel.Level() == slog.LevelDebug { - logLevel.Set(slog.LevelInfo) - slog.Info("log level toggled to INFO (send SIGUSR1 to switch back to DEBUG)") - } else { - logLevel.Set(slog.LevelDebug) - slog.Info("log level toggled to DEBUG (send SIGUSR1 to switch back to INFO)") - } - } - }() -} - -func main() { - mode := flag.String("mode", "", "deployment mode: envoy-sidecar, waypoint, proxy-sidecar") - configPath := flag.String("config", "", "path to config YAML file") - flag.Parse() - - initLogging() - startSignalToggle() - - if *configPath == "" { - log.Fatal("--config is required") - } - - // buildPipelines loads the config from *configPath, applies mode - // override + presets, validates, and builds the plugin pipelines. - // Runs once at startup and again on every reload — the reloader - // holds this closure so both paths share exactly the same sequence. - // Returns a descriptive error on any step's failure so /reload/status - // surfaces an operator-readable message. - buildPipelines := func() (*pipeline.Pipeline, *pipeline.Pipeline, *config.Config, error) { - c, err := config.Load(*configPath) - if err != nil { - return nil, nil, nil, err - } - if *mode != "" { - c.Mode = *mode // flag overrides YAML - } - config.ApplyPreset(c) - if err := config.Validate(c); err != nil { - return nil, nil, nil, err - } - in, err := plugins.Build(c.Pipeline.Inbound.Plugins) - if err != nil { - return nil, nil, nil, fmt.Errorf("inbound: %w", err) - } - out, err := plugins.Build(c.Pipeline.Outbound.Plugins) - if err != nil { - return nil, nil, nil, fmt.Errorf("outbound: %w", err) - } - if c.Mode == config.ModeWaypoint && (in.NeedsBody() || out.NeedsBody()) { - return nil, nil, nil, errors.New("waypoint mode does not support plugins that require body access (ext_authz limitation)") - } - return in, out, c, nil - } - - inboundPipeline, outboundPipeline, cfg, err := buildPipelines() - if err != nil { - log.Fatalf("initial pipeline build: %v", err) - } - - // Invoke Init on any plugin implementing pipeline.Initializer. Done - // before listeners start so a plugin that fails to initialize takes - // down the process cleanly rather than serving traffic in a broken - // state. Init may do network I/O (register metrics, load a model, - // fetch a remote config); we give it the process context so an - // abort during init (Ctrl-C) cancels cooperatively. - initCtx, initCancel := context.WithTimeout(context.Background(), 60*time.Second) - defer initCancel() - if err := inboundPipeline.Start(initCtx); err != nil { - log.Fatalf("inbound pipeline Start: %v", err) - } - if err := outboundPipeline.Start(initCtx); err != nil { - log.Fatalf("outbound pipeline Start: %v", err) - } - - // Wrap pipelines in Holders. Listeners reference the Holder rather - // than the *Pipeline directly so the reloader can swap the bound - // pipeline under a running listener without pod restart. - inboundH := pipeline.NewHolder(inboundPipeline) - outboundH := pipeline.NewHolder(outboundPipeline) - - // Arm the config file watcher. ctx is cancelled on SIGTERM/SIGINT - // so the reloader goroutine exits cleanly at shutdown. - ctx, cancelCtx := context.WithCancel(context.Background()) - defer cancelCtx() - rld := reloader.New(*configPath, inboundH, outboundH, buildPipelines, cfg) - if err := rld.Start(ctx); err != nil { - log.Fatalf("reloader: %v", err) - } - - // Build session store if enabled (nil when disabled — zero overhead). - // Defaults to on; set session.enabled: false in runtime config to opt out. - var sessions *session.Store - if cfg.Session.SessionEnabled() { - ttl := 30 * time.Minute - if cfg.Session.TTL != "" { - if d, err := time.ParseDuration(cfg.Session.TTL); err == nil { - ttl = d - } else { - slog.Warn("invalid session.ttl, using default", "value", cfg.Session.TTL, "error", err) - } - } - maxEvents := 100 - if cfg.Session.MaxEvents > 0 { - maxEvents = cfg.Session.MaxEvents - } - maxSessions := 100 - if cfg.Session.MaxSessions > 0 { - maxSessions = cfg.Session.MaxSessions - } - sessions = session.New(ttl, maxEvents, maxSessions) - slog.Info("session tracking enabled", "ttl", ttl, "maxEvents", maxEvents, "maxSessions", maxSessions) - } else { - slog.Info("session tracking disabled") - } - - // Track servers for graceful shutdown - var grpcServers []*grpc.Server - var httpServers []*http.Server - - // Start listeners FIRST — before credential resolution - switch cfg.Mode { - case config.ModeEnvoySidecar: - grpcServers = append(grpcServers, startGRPCExtProc(inboundH, outboundH, sessions, cfg.Listener.ExtProcAddr)) - - case config.ModeWaypoint: - grpcServers = append(grpcServers, startGRPCExtAuthz(inboundH, outboundH, cfg.Listener.ExtAuthzAddr)) - httpServers = append(httpServers, startHTTPServer("forward-proxy", forwardproxy.NewServer(outboundH, sessions).Handler(), cfg.Listener.ForwardProxyAddr)) - - case config.ModeProxySidecar: - rpSrv, err := reverseproxy.NewServer(inboundH, sessions, cfg.Listener.ReverseProxyBackend) - if err != nil { - log.Fatalf("creating reverse proxy: %v", err) - } - httpServers = append(httpServers, startHTTPServer("reverse-proxy", rpSrv.Handler(), cfg.Listener.ReverseProxyAddr)) - httpServers = append(httpServers, startHTTPServer("forward-proxy", forwardproxy.NewServer(outboundH, sessions).Handler(), cfg.Listener.ForwardProxyAddr)) - - default: - log.Fatalf("unhandled mode %q", cfg.Mode) - } - - // /stats aggregates per-plugin counters at request time. Each - // plugin that implements plugins.StatsSource contributes its own - // *auth.Stats; the provider merges them into a single response - // per HTTP request. Freshly-computed every call, so the numbers - // reflect traffic up to the moment of the curl. - // Freshly computed per /stats request. Load through the Holder so a - // pipeline swap is reflected without restarting the stats handler. - statsProvider := func() *auth.Stats { - sources := plugins.CollectStats(inboundH.Load()) - sources = append(sources, plugins.CollectStats(outboundH.Load())...) - return auth.MergeStats(sources...) - } - statSrv := startStatServer(cfg, rld.ConfigProvider(), statsProvider, rld.Handler()) - - // Session events API (optional; only when session tracking is on). - // The API has no authentication — bind only on in-cluster addresses and - // never expose via ingress. Payloads contain raw user messages, LLM - // completions, and tool results verbatim. Set session.enabled: false to - // disable. - var sessionAPISrv *sessionapi.Server - if cfg.Listener.SessionAPIAddr != "" && sessions != nil { - sessionAPISrv = sessionapi.New( - cfg.Listener.SessionAPIAddr, - sessions, - sessionapi.WithPipelines(inboundH, outboundH), - ) - go func() { - slog.Warn("session API listening — UNAUTHENTICATED; contains raw user content; never expose via ingress", - "addr", cfg.Listener.SessionAPIAddr) - if err := sessionAPISrv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Fatalf("session API: %v", err) - } - }() - } - - slog.Info("authbridge starting", "mode", cfg.Mode, "logLevel", logLevel.Level().String()) - - // Health/readiness endpoints. /healthz is liveness (always OK); - // /readyz ANDs pipeline.Ready() across inbound and outbound. A - // plugin implementing pipeline.Readier — currently jwt-validation - // and token-exchange — can return false while its deferred Init - // goroutine is still waiting on credential files. The kubelet - // holds traffic off the pod until all plugins are ready, so the - // 503-from-OnRequest window at pod boot is closed. - go func() { - mux := http.NewServeMux() - mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - }) - mux.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) { - // Report the first not-ready plugin by name so operators - // can diagnose from `kubectl describe pod` without - // tailing container logs. - // Holder-delegated — a hot-reloaded pipeline's readiness is - // reflected in the next /readyz probe. - if name := inboundH.NotReadyPlugin(); name != "" { - http.Error(w, "inbound plugin not ready: "+name, http.StatusServiceUnavailable) - return - } - if name := outboundH.NotReadyPlugin(); name != "" { - http.Error(w, "outbound plugin not ready: "+name, http.StatusServiceUnavailable) - return - } - w.WriteHeader(http.StatusOK) - }) - slog.Info("health server listening", "addr", ":9091") - if err := http.ListenAndServe(":9091", mux); err != nil { - slog.Warn("health server failed", "error", err) - } - }() - - // Wait for shutdown signal - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT) - sig := <-sigCh - slog.Info("shutting down", "signal", sig) - - // Graceful shutdown - shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 15*time.Second) - defer shutdownCancel() - - for _, srv := range grpcServers { - // GracefulStop blocks until all RPCs complete. If streams are long-lived - // (e.g., ext_proc), fall back to hard Stop after the shutdown timeout. - go func(s *grpc.Server) { - <-shutdownCtx.Done() - s.Stop() - }(srv) - srv.GracefulStop() - } - for _, srv := range httpServers { - srv.Shutdown(shutdownCtx) - } - statSrv.Shutdown(shutdownCtx) - if sessionAPISrv != nil { - sessionAPISrv.Shutdown(shutdownCtx) - } - - // Invoke plugin Shutdown hooks after listeners have stopped accepting - // traffic so in-flight work is allowed to complete before plugins - // flush state. Best-effort — individual errors are logged but do - // not abort the sequence. Bounded by shutdownCtx's deadline. - outboundPipeline.Stop(shutdownCtx) - inboundPipeline.Stop(shutdownCtx) - - if sessions != nil { - sessions.Close() - } -} - -func startGRPCExtProc(inbound, outbound *pipeline.Holder, sessions *session.Store, addr string) *grpc.Server { - srv := grpc.NewServer() - extprocv3.RegisterExternalProcessorServer(srv, &extproc.Server{ - InboundPipeline: inbound, - OutboundPipeline: outbound, - Sessions: sessions, - }) - registerHealth(srv) - reflection.Register(srv) - - go func() { - lis, err := net.Listen("tcp", addr) - if err != nil { - log.Fatalf("ext_proc listen %s: %v", addr, err) - } - slog.Info("ext_proc gRPC listening", "addr", addr) - if err := srv.Serve(lis); err != nil { - log.Fatalf("ext_proc serve: %v", err) - } - }() - return srv -} - -func startGRPCExtAuthz(inbound, outbound *pipeline.Holder, addr string) *grpc.Server { - srv := grpc.NewServer() - authv3.RegisterAuthorizationServer(srv, &extauthz.Server{ - InboundPipeline: inbound, - OutboundPipeline: outbound, - }) - registerHealth(srv) - reflection.Register(srv) - - go func() { - lis, err := net.Listen("tcp", addr) - if err != nil { - log.Fatalf("ext_authz listen %s: %v", addr, err) - } - slog.Info("ext_authz gRPC listening", "addr", addr) - if err := srv.Serve(lis); err != nil { - log.Fatalf("ext_authz serve: %v", err) - } - }() - return srv -} - -func startHTTPServer(name string, handler http.Handler, addr string) *http.Server { - srv := &http.Server{ - Addr: addr, - Handler: handler, - ReadHeaderTimeout: 10 * time.Second, - } - go func() { - slog.Info("HTTP server listening", "name", name, "addr", addr) - if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Fatalf("%s serve: %v", name, err) - } - }() - return srv -} - -func startStatServer(cfg *config.Config, cfgProvider observe.ConfigProvider, statsProvider observe.StatsProvider, reloadStatus http.Handler) *observe.StatServer { - srv := observe.NewStatServer(cfg.Stats.StatsAddress, cfgProvider, statsProvider, - observe.WithReloadStatus(reloadStatus)) - go func() { - slog.Info("stat server listening", "addr", cfg.Stats.StatsAddress) - if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Fatalf("stat server: %v", err) - } - }() - return srv -} - -func registerHealth(srv *grpc.Server) { - healthSrv := health.NewServer() - healthpb.RegisterHealthServer(srv, healthSrv) - healthSrv.SetServingStatus("", healthpb.HealthCheckResponse_SERVING) -} diff --git a/authbridge/go.work b/authbridge/go.work index b16b03132..9a1fd46ca 100644 --- a/authbridge/go.work +++ b/authbridge/go.work @@ -4,7 +4,7 @@ use ( ./authlib ./authproxy ./cmd/abctl - ./cmd/authbridge ./cmd/authbridge-envoy + ./cmd/authbridge-lite ./cmd/authbridge-proxy )