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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 12 additions & 39 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,53 +22,26 @@
strategy:
matrix:
image_config:
# Client Registration - registers workloads with Keycloak using SPIFFE ID
- name: client-registration
context: ./authbridge/client-registration
dockerfile: Dockerfile

# AuthProxy components - token exchange and traffic interception
- name: auth-proxy
context: ./authbridge/authproxy
dockerfile: Dockerfile

# iptables init container — used by envoy-sidecar mode to
# set up traffic redirection. Proxy-sidecar mode does not
# need this (HTTP_PROXY env var routing replaces iptables).
- name: proxy-init
context: ./authbridge/authproxy
dockerfile: Dockerfile.init

# SPIFFE Helper - fetches SPIFFE credentials from SPIRE (UBI-based fork)
- name: spiffe-helper
context: ./authbridge/spiffe-helper
dockerfile: Dockerfile

# Combined AuthBridge sidecar (envoy + authbridge + spiffe-helper + client-registration)
- name: authbridge
context: ./authbridge
dockerfile: authproxy/Dockerfile.authbridge

# Unified AuthBridge binary (Envoy + authbridge in one container)
# Drop-in replacement for envoy-with-processor
# DEPRECATED: use authbridge-envoy instead
- name: authbridge-unified
context: ./authbridge
dockerfile: cmd/authbridge/Dockerfile

# AuthBridge with Envoy (envoy-sidecar mode)
# New canonical name for authbridge-unified
# AuthBridge envoy-sidecar combined image —
# Envoy + authbridge (ext_proc) + spiffe-helper. Spiffe-helper
# starts conditionally based on SPIRE_ENABLED.
- name: authbridge-envoy
context: ./authbridge
dockerfile: cmd/authbridge/Dockerfile
dockerfile: cmd/authbridge/Dockerfile.envoy

# Lightweight AuthBridge (Go binary only, no Envoy)
# Used for waypoint and proxy-sidecar modes
- name: authbridge-light
# AuthBridge proxy-sidecar combined image (default mode) —
# authbridge-proxy + spiffe-helper. No Envoy, no gRPC.
# Spiffe-helper starts conditionally based on SPIRE_ENABLED.
- name: authbridge
context: ./authbridge
dockerfile: cmd/authbridge/Dockerfile.light

# Demo application for testing
- name: demo-app
context: ./authbridge/authproxy/quickstart/demo-app
dockerfile: Dockerfile
dockerfile: cmd/authbridge/Dockerfile.proxy

steps:
# 1. Checkout code
Expand Down Expand Up @@ -115,7 +88,7 @@
# Always use the computed tag
type=raw,value=${{ steps.tag.outputs.tag }}
# Add 'latest' tag for version tags, workflow_dispatch, and pushes to main
type=raw,value=latest,enable=${{ (github.ref_type == 'tag' && startsWith(github.ref_name, 'v')) || github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' }}

Check warning on line 91 in .github/workflows/build.yaml

View workflow job for this annotation

GitHub Actions / YAML Lint

91:151 [line-length] line too long (189 > 150 characters)

# 7. Build and push image
- name: Build and push ${{ matrix.image_config.name }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/security-scans.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ jobs:
echo "=== Bandit Python Security Scan ==="
# Scan AuthBridge Python code and tests
PYTHON_DIRS=""
for dir in authbridge/client-registration authbridge tests; do
for dir in authbridge tests; do
if [ -d "$dir" ]; then
PYTHON_DIRS="$PYTHON_DIRS $dir"
fi
Expand Down
85 changes: 49 additions & 36 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,20 @@ kagenti-extensions/
│ │ ├── routing/ # Host-to-audience router
│ │ ├── auth/ # HandleInbound + HandleOutbound composition
│ │ └── config/ # Mode presets, YAML config, validation
│ ├── cmd/authbridge/ # Unified binary — 3 modes, 1 codebase
│ │ ├── listener/ # Protocol adapters (ext_proc, ext_authz, forward/reverse proxy)
│ │ ├── entrypoint.sh # Envoy + authbridge process supervision
│ │ └── Dockerfile # Combined Envoy + authbridge image
│ ├── authproxy/ # Auth proxy support files and demos
│ ├── 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
│ ├── authproxy/ # iptables init container + standalone quickstart
│ │ ├── quickstart/ # Standalone demo (no SPIFFE)
│ │ └── k8s/ # Standalone K8s manifests
│ ├── client-registration/ # Keycloak auto-registration (Python)
│ ├── spiffe-helper/ # SPIFFE helper Dockerfile (fetches JWT-SVIDs from SPIRE)
│ ├── demos/ # Demo scenarios (weather-agent, github-issue, webhook, single-target, multi-target)
│ └── keycloak_sync.py # Declarative Keycloak sync tool
├── tests/ # Python tests (client-registration, keycloak_sync)
├── tests/ # Python tests (keycloak_sync)
├── .github/
│ ├── workflows/ # CI/CD (ci.yaml, build.yaml, security-scans, scorecard, spellcheck)
│ └── ISSUE_TEMPLATE/ # Bug report, feature request, epic templates
Expand All @@ -68,38 +70,42 @@ A **single binary** providing transparent traffic interception for both inbound

**Ports:** 15123 (outbound), 15124 (inbound), 9090 (ext-proc/ext-authz), 9901 (admin), 9093 (stats and config)

### 2. Client Registration (Python)
### 2. Client Registration

A Python script that **automatically registers Kubernetes workloads as Keycloak OAuth2 clients** using their SPIFFE identity.

**Location:** `authbridge/client-registration/`
**Language:** Python 3.12
**Detailed guide:** [`authbridge/CLAUDE.md`](authbridge/CLAUDE.md)

**Flow:** Reads SPIFFE ID from JWT, registers client in Keycloak, writes secret to `/shared/client-secret.txt`
Keycloak client registration for workloads is handled by the
**kagenti-operator** (separate repo) — see `kagenti-operator/docs/operator-managed-client-registration.md`.
The operator creates a Secret with `client-id.txt` + `client-secret.txt`
and the webhook mounts it at `/shared/` in the workload pod. The
in-pod `client-registration` sidecar that previously lived in this
repo has been removed.

## How the Components Work Together

The kagenti-operator (in a separate repo) injects AuthBridge sidecars into workload pods. Once injected, the sidecars work together:
The kagenti-operator (in a separate repo) injects AuthBridge sidecars
into workload pods. Default deployment shape (proxy-sidecar mode):

```
┌────────────────────────────────────┐
│ WORKLOAD POD │
│ │
│ proxy-init (init) ─► iptables │
│ │
│ spiffe-helper ──► SPIRE Agent │
│ │ writes JWT SVID │
│ ▼ │
│ client-registration ──► Keycloak │
│ │ writes client secret │
│ ▼ │
│ envoy-proxy (+ authbridge) │
│ - Inbound: JWT validation │
│ - Outbound: token exchange │
│ spiffe-helper ──► SPIRE Agent │ (in-container,
│ │ writes JWT SVID │ conditional on
│ ▼ │ SPIRE_ENABLED)
│ authbridge-proxy │
│ - Reverse proxy: inbound JWT │
│ - Forward proxy: outbound │
│ token exchange │
│ │ │
│ Your Application │
│ (HTTP_PROXY → forward proxy) │
└────────────────────────────────────┘

The operator also creates a Secret with client-id +
client-secret and mounts it at /shared/.

For envoy-sidecar mode, replace authbridge-proxy with
the authbridge-envoy image (Envoy + ext_proc + spiffe-helper)
and add a proxy-init container for iptables.
```

## Unified AuthBridge Binary
Expand Down Expand Up @@ -146,17 +152,24 @@ Types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`

## Container Images

All images are pushed to `ghcr.io/kagenti/kagenti-extensions/`:
All images are pushed to `ghcr.io/kagenti/kagenti-extensions/` from
`.github/workflows/build.yaml`:

| Image | Source | Description |
|-------|--------|-------------|
| **`authbridge-unified`** | **`authbridge/cmd/authbridge/Dockerfile`** | **Unified Envoy + authbridge binary (recommended)** |
| `authbridge` | `authbridge/authproxy/Dockerfile.authbridge` | Combined sidecar (Envoy + authbridge + spiffe-helper + client-registration) |
| `proxy-init` | `authbridge/authproxy/Dockerfile.init` | Alpine + iptables init container |
| `client-registration` | `authbridge/client-registration/Dockerfile` | Python Keycloak client registrar |
| `spiffe-helper` | `authbridge/spiffe-helper/Dockerfile` | Fetches SPIFFE credentials from SPIRE |
| `auth-proxy` | `authbridge/authproxy/Dockerfile` | Example pass-through proxy (for demos) |
| `demo-app` | `authbridge/authproxy/quickstart/demo-app/Dockerfile` | Demo target service |
| **`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 |
| `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
identity is enabled for the workload).

The legacy `authbridge-unified`, `authbridge-light`, `client-registration`,
`spiffe-helper`, `auth-proxy`, and `demo-app` standalone images have
been removed from CI (the auth-proxy / demo-app source is still in-tree
for the standalone quickstart). Older release tags continue to publish
the old images.

## Pre-commit Hooks

Expand Down
47 changes: 26 additions & 21 deletions authbridge/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,27 +38,26 @@ authbridge/
│ ├── auth/ # HandleInbound + HandleOutbound composition
│ └── config/ # Mode presets, YAML config, validation
├── cmd/authbridge/ # Unified binary -- 3 modes, 1 codebase
│ ├── listener/ # Protocol adapters (ext_proc, ext_authz, forward/reverse proxy)
│ ├── entrypoint.sh # Envoy + authbridge process supervision
│ └── Dockerfile # Combined Envoy + authbridge image
├── 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
├── authproxy/ # Legacy sidecar support (iptables, quickstart)
│ ├── init-iptables.sh # iptables setup (outbound + inbound, Istio ambient compatible)
├── cmd/authbridge-proxy/ # Lite binary — proxy-sidecar mode only, no gRPC
├── cmd/authbridge-envoy/ # Lite binary — envoy-sidecar mode only
├── authproxy/ # iptables init container + standalone quickstart
│ ├── init-iptables.sh # iptables setup for envoy-sidecar mode
│ ├── Dockerfile.init # proxy-init container image
│ ├── Dockerfile.authbridge # Combined sidecar image
│ ├── k8s/ # Standalone K8s manifests
│ └── quickstart/ # Standalone demo (no SPIFFE)
│ ├── setup_keycloak.py
│ └── demo-app/main.go # Test target: JWT validation (:8081), TLS echo (:8443)
├── client-registration/ # Keycloak auto-registration (Python)
│ ├── client_registration.py # Main script: register client, write secret
│ └── Dockerfile # Python 3.12-slim, UID/GID 1000
├── spiffe-helper/ # SPIFFE helper (Dockerfile only)
│ └── Dockerfile # Fetches JWT-SVIDs from SPIRE agent
├── demos/ # Demo scenarios with full setup
│ ├── README.md # Demo index (recommended starting order)
│ ├── weather-agent/ # Getting-started demo (inbound validation only)
Expand Down Expand Up @@ -116,7 +115,7 @@ with protocol-specific listeners in `cmd/authbridge/listener/`:
- 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).
- Credential files: `client-registration` writes `/shared/client-id.txt` and `/shared/client-secret.txt`; `spiffe-helper` writes `/opt/jwt_svid.token`. `jwt-validation` reads the audience from `/shared/client-id.txt` via `audience_file`; `token-exchange` reads client credentials via `client_id_file` / `client_secret_file` / `jwt_svid_path`. Each plugin attempts a synchronous read at Configure time and falls back to a background poll from its `Init` goroutine if the file isn't yet readable.
- Credential files: the **kagenti-operator** registers each workload with Keycloak and creates a Secret containing `client-id.txt` + `client-secret.txt`; the operator's webhook mounts that Secret at `/shared/client-id.txt` and `/shared/client-secret.txt` in containers that share the `shared-data` volume. `spiffe-helper` (bundled in both combined images, started conditionally on `SPIRE_ENABLED=true`) writes `/opt/jwt_svid.token`. `jwt-validation` reads the audience from `/shared/client-id.txt` via `audience_file`; `token-exchange` reads client credentials via `client_id_file` / `client_secret_file` / `jwt_svid_path`. Each plugin attempts a synchronous read at Configure time and falls back to a background poll from its `Init` goroutine if the file isn't yet readable. The legacy in-pod `client-registration` sidecar has been removed; workloads opting back into that path use `kagenti.io/client-registration-inject: "true"` and the operator's bundled client-registration image (separate repo).
- Outbound route config: `token-exchange` reads `/etc/authproxy/routes.yaml` by default (path is per-plugin, configured via `routes.file` in its config block); inline rules can be declared under `routes.rules`.
- Outbound `default_policy`: `passthrough` (default) or `exchange`, configured per-plugin (no top-level `DEFAULT_OUTBOUND_POLICY` field anymore; the env var is still expanded into the plugin config by `authbridge-combined.yaml`).

Expand Down Expand Up @@ -236,9 +235,9 @@ Sidecars communicate through files on shared volumes:

| Path | Writer | Reader | Content |
|------|--------|--------|---------|
| `/opt/jwt_svid.token` | spiffe-helper | client-registration | JWT SVID from SPIRE |
| `/shared/client-id.txt` | client-registration | authbridge | SPIFFE ID or CLIENT_NAME |
| `/shared/client-secret.txt` | client-registration | authbridge | Keycloak client secret |
| `/opt/jwt_svid.token` | spiffe-helper | authbridge (token-exchange) | JWT SVID from SPIRE |
| `/shared/client-id.txt` | operator (Secret mount) | authbridge | SPIFFE ID or workload name |
| `/shared/client-secret.txt` | operator (Secret mount) | authbridge | Keycloak client secret |

## Build and Deploy

Expand All @@ -253,9 +252,15 @@ make build-images
# Load into Kind cluster
make load-images # Uses KIND_CLUSTER_NAME env var (default: kagenti)

# Build the authbridge-unified sidecar image separately (from authbridge/ context)
cd .. && podman build -f cmd/authbridge/Dockerfile -t authbridge-unified:latest .
kind load docker-image authbridge-unified:latest --name kagenti
# 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 .
kind load docker-image authbridge-envoy:latest --name kagenti
kind load docker-image authbridge:latest --name kagenti

# Deploy auth-proxy + demo-app
make deploy
Expand Down
11 changes: 6 additions & 5 deletions authbridge/authlib/config/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ import (

// ReadCredentialFile performs a one-shot read of a credential file,
// returning its whitespace-trimmed contents. Used by plugins from
// Configure to opportunistically pick up values that client-registration
// has already written; when it returns an error, the plugin should fall
// back to WaitForCredentialFile from Init to wait for the file.
// Configure to opportunistically pick up values the operator has
// already mounted from a Secret; when it returns an error, the
// plugin should fall back to WaitForCredentialFile from Init to wait
// for the file.
func ReadCredentialFile(path string) (string, error) {
info, err := os.Stat(path)
if err != nil {
Expand All @@ -45,8 +46,8 @@ var heartbeatInterval = 60 * time.Second

// WaitForCredentialFile blocks until the file is readable with non-zero
// length, or until ctx is cancelled. Plugins call this from Init (via a
// goroutine) to wait out the race with client-registration's secret
// provisioning.
// goroutine) to wait out the race with the operator's Secret-mount
// propagation.
//
// Polls at 2s intervals — fast enough for human-observable boot times,
// slow enough that a pod full of plugins isn't hammering the kubelet.
Expand Down
4 changes: 2 additions & 2 deletions authbridge/authlib/pipeline/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,8 @@ type Finisher interface {
// ANDs Ready() across all implementers to decide whether the pipeline
// is ready to serve traffic. A plugin whose Configure succeeded but
// whose Init is still waiting (e.g. for a credential file to be
// written by client-registration) returns false — the kubelet keeps
// traffic off the pod until Init completes.
// mounted by the operator from a Secret) returns false — the kubelet
// keeps traffic off the pod until Init completes.
//
// Plugins without deferred state don't implement this interface and
// are treated as always-ready. Pipeline.Ready() returns true when
Expand Down
Loading
Loading