AuthBridge provides secure, transparent token management for Kubernetes workloads. The shared library is at authlib/; the mode-specific binaries (proxy-sidecar default, envoy-sidecar, lite) live under cmd/. Keycloak client registration is handled by the operator's ClientRegistrationReconciler (no in-pod registration sidecar). Together with SPIFFE/SPIRE, this enables zero-trust authentication flows.
📘 Looking to run the demo? See the Weather Agent or GitHub Issue Agent demos for step-by-step instructions, and Token-Exchange Routes for route configuration.
Prefer not to compile from source? Every v* release attaches prebuilt abctl (the
session-inspector TUI) and authbridge-proxy binaries for linux and macOS (amd64 +
arm64) on the Releases page.
VER=v0.1.0 # a released tag
OS=darwin ARCH=arm64 # one of: linux/darwin × amd64/arm64
base="https://github.com/rossoctl/cortex/releases/download/${VER}"
curl -fsSLO "${base}/abctl_${VER}_${OS}_${ARCH}.tar.gz"
curl -fsSLO "${base}/checksums.txt"
sha256sum -c checksums.txt --ignore-missing # macOS: shasum -a 256 -c ... --ignore-missing
tar xzf "abctl_${VER}_${OS}_${ARCH}.tar.gz"
sudo mv abctl /usr/local/bin/ # onto PATH
abctl --versionauthbridge-proxy ships the same way (authbridge-proxy_${VER}_${OS}_${ARCH}.tar.gz).
- Linux binaries are fully static (
CGO_ENABLED=0) — no libc dependency, run anywhere. - macOS binaries are portable but unsigned; after extracting, clear the Gatekeeper
quarantine once:
xattr -dr com.apple.quarantine ./abctl(orcodesign --sign - ./abctl).
Building from source: make abctl or make authbridge-proxy from the repo
root. authbridge-proxy defaults to the full plugin profile; pass
PROFILE=lite or local for smaller sets. See
Build-tag plugin selection for the underlying
go build invocations.
Two container images are published:
| Image | Contents |
|---|---|
authbridge |
proxy-sidecar combined: authbridge-proxy binary + bundled spiffe-helper |
authbridge-envoy |
envoy-sidecar combined: Envoy + ext_proc + bundled spiffe-helper |
authbridge-lite |
authbridge-proxy built with the lite profile (see authbridge/scripts/profile-tags), a sidecar minimum. A build variant, not a separate binary |
| Mode | Image | Use Case | How It Works |
|---|---|---|---|
proxy-sidecar (default) |
authbridge |
HTTP_PROXY-based forward + reverse proxies | Agent routes outbound traffic through forward proxy; reverse proxy validates inbound JWTs |
envoy-sidecar |
authbridge-envoy |
Transparent interception via iptables | Envoy intercepts all traffic, delegates auth to authbridge via ext_proc gRPC |
lite |
authbridge-lite |
The authbridge-proxy binary built with the lite profile (see authbridge/scripts/profile-tags) |
For size-constrained deployments that don't need protocol-aware session events |
The operator resolves the mode per workload from AgentRuntime.Spec.AuthBridgeMode → namespace ConfigMap → deprecated rossoctl.io/authbridge-mode annotation → cluster default (proxy-sidecar). See operator#361.
The shared auth library at authlib/ contains the building blocks (JWT validation, token exchange, caching, routing) with no protocol dependencies. See authlib/README.md for package reference.
See docs/plugin-catalog.md for the full list of implemented pipeline plugins, what each does, and its configuration options.
The following describes the operator-injected sidecar deployment. After cortex#411 each mode is served by its own combined image (one container per pod, with spiffe-helper bundled inside and gated by SPIRE_ENABLED). The legacy authbridge-unified, authbridge-light, envoy-with-processor, and standalone client-registration / spiffe-helper sidecars are gone.
AuthBridge solves the challenge of secure service-to-service authentication in Kubernetes:
-
Automatic Identity - Workloads automatically obtain their identity from SPIFFE/SPIRE and register as Keycloak clients using their SPIFFE ID (e.g.,
spiffe://example.com/ns/default/sa/myapp) -
Token-Based Authorization - Callers obtain JWT tokens from Keycloak with the workload's identity as the audience, authorizing them to invoke specific services
-
Transparent Token Exchange - A sidecar intercepts outgoing requests, validates incoming tokens, and exchanges them for tokens with the appropriate target audience—all without application code changes
-
Target Service Validation - Target services validate the exchanged token, ensuring it has the correct audience before authorizing requests
Incoming request (with JWT)
│
▼
┌───────────────────────────────────────────────────────────────────────┐
│ WORKLOAD POD │
│ (with AuthBridge sidecars) │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Init Container: proxy-init (iptables intercepts pod traffic, │ │
│ │ excluding Keycloak port) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ AuthBridge Sidecar (combined image) │ │
│ │ Container name = mode-dependent: │ │
│ │ proxy-sidecar (default): authbridge-proxy │ │
│ │ envoy-sidecar: envoy-proxy │ │
│ │ │ │
│ │ INBOUND: Validates JWT (signature + issuer via JWKS) │ │
│ │ Returns 401 Unauthorized if invalid │ │
│ │ OUTBOUND: Exchanges token → target-service audience │ │
│ │ (using Workload's credentials) │ │
│ └──────────────────────┬──────────────────────────────────────────┘ │
│ ▲ outbound │ inbound │
│ │ request │ (validated) │
│ │ ▼ │
│ ┌─────────┴───────────────────────────────────────────────────────┐ │
│ │ Your App │ │
│ │ (spiffe-helper bundled inside the AuthBridge sidecar above, │ │
│ │ gated per-workload by SPIRE_ENABLED) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────────┘
▲
│ Out-of-band: operator's ClientRegistrationReconciler
│ creates the Keycloak client + a rossoctl-keycloak-client-credentials
│ Secret. The webhook mounts that Secret into the AuthBridge sidecar
│ at /shared/client-{id,secret}.txt — no in-pod registration sidecar.
│
│ Exchanged token (aud: target-service)
▼
┌─────────────────────┐
│ TARGET SERVICE POD │
│ │
│ Validates token │
│ with audience │
│ "target-service" │
└─────────────────────┘
📊 Mermaid Architecture Diagram (click to expand)
flowchart TB
subgraph WorkloadPod["WORKLOAD POD (with AuthBridge sidecar)"]
subgraph Init["Init Container (envoy-sidecar mode only)"]
ProxyInit["proxy-init<br/>(iptables setup)"]
end
subgraph Containers["Containers"]
App["Your Application"]
Sidecar["AuthBridge sidecar (combined image)<br/>name = mode-dependent:<br/>proxy-sidecar: authbridge-proxy<br/>envoy-sidecar: envoy-proxy<br/><br/>(spiffe-helper bundled inside,<br/>gated by SPIRE_ENABLED)"]
end
end
subgraph Operator["operator (rossoctl-system)"]
ClientReg["ClientRegistration<br/>Reconciler"]
end
subgraph TargetPod["TARGET SERVICE POD"]
Target["Target Service<br/>(validates tokens)"]
end
subgraph External["External Services"]
SPIRE["SPIRE Agent"]
Keycloak["Keycloak"]
end
Caller["Caller<br/>(external)"]
SPIRE --> Sidecar
ClientReg -->|"creates client + Secret"| Keycloak
ClientReg -.->|"Secret mounted at /shared/"| Sidecar
Caller -->|"1. Get token"| Keycloak
Caller -->|"2. Pass token"| Sidecar
Sidecar -->|"3. Validate JWT (JWKS, returns 401 if invalid)"| Caller
Sidecar -->|"4. Forward if valid"| App
App -->|"5. Request + Token"| Sidecar
Sidecar -->|"6. Exchange via Keycloak"| Keycloak
Sidecar -->|"7. Request + Exchanged Token"| Target
Target -->|"8. Response"| App
App -->|"9. Response"| Caller
style WorkloadPod fill:#e1f5fe
style TargetPod fill:#e8f5e9
style Sidecar fill:#fff3e0
style External fill:#fce4ec
style Caller fill:#fff9c4
After cortex#411 a workload pod has the application
container plus a single combined AuthBridge sidecar. In
envoy-sidecar mode it also has a one-shot proxy-init init
container; in proxy-sidecar mode (the cluster default) it does
not. spiffe-helper is bundled inside the sidecar image; client
registration runs in the operator, not the pod.
| Component | Type | Mode | Purpose |
|---|---|---|---|
proxy-init |
init | envoy-sidecar only | Sets up iptables to intercept inbound and outbound traffic (excludes Keycloak port to avoid token-exchange loops) |
Your App |
container | both | Your application |
authbridge-proxy |
container | proxy-sidecar (default) | Combined sidecar from the authbridge image: HTTP forward + reverse proxies, full plugin set (jwt-validation + token-exchange + a2a/mcp/inference parsers), bundled spiffe-helper gated by SPIRE_ENABLED. |
envoy-proxy |
container | envoy-sidecar | Combined sidecar from the authbridge-envoy image: Envoy + ext_proc + bundled spiffe-helper. Validates inbound JWTs (signature + issuer via JWKS) and exchanges outbound tokens; HTTPS is TLS-passthrough. |
Any downstream service that validates incoming tokens have the expected audience.
Initialization (Workload Pod Startup):
SPIRE Agent Workload Pod Keycloak
│ │ │
│ 0. SVID │ │
│───────────────────────►│ SPIFFE Helper │
│ (SPIFFE ID) │ │
│ │ │
│ │ 1. Register client │
│ │ (client_id = SPIFFE ID) │
│ │───────────────────────────────►│
│ │ Client Registration │
│ │ │
│ │◄───────────────────────────────│
│ │ client_secret │
│ │ (saved to /shared/) │
Runtime Flow:
Caller Workload Pod Keycloak Target Service
│ │ │ │
│ 2. Get token │ │ │
│ (aud: Workload's SPIFFE ID) │ │
│─────────────────────────────────────────────►│ │
│◄─────────────────────────────────────────────│ │
│ Token (aud: Workload) │ │
│ │ │ │
│ 3. Pass token │ │ │
│ to Workload │ │ │
│────────────────────►│ │ │
│ │──────────┐ │ │
│ │ Envoy intercepts │ │
│ │ inbound request │ │
│ │ │ │ │
│ │ Ext Proc validates │ │
│ │ JWT (signature + │ │
│ │ issuer via JWKS) │ │
│ │ │ │ │
│ │ 401 if invalid ──────►│ (rejected) │
│ │ │ │ │
│ │ 4. Forward to App │ │
│ │ if valid │ │
│ │◄─────────┘ │ │
│ │ │ │
│ │ 5. Workload calls │ │
│ │ Target Service with │ │
│ │ Caller's token │ │
│ │──────────┐ │ │
│ │ │ │ │
│ │ Envoy intercepts │ │
│ │ outbound request │ │
│ │ │ │ │
│ │ 6. Token Exchange │ │
│ │ (using Workload creds)│ │
│ │───────────────────────►│ │
│ │◄───────────────────────│ │
│ │ New token (aud: target-service) │
│ │ │ │ │
│ │ 7. Forward request │ │
│ │ with exchanged token │ │
│ │───────────────────────────────────────►│
│ │ │ │
│ │◄───────────────────────────────────────│
│ │ "authorized" │ │
│◄────────────────────│ │ │
│ Response │ │ │
| Step | Component | Verification |
|---|---|---|
| 0 | SPIFFE Helper | SVID obtained from SPIRE Agent |
| 1 | Client Registration | Workload registered with Keycloak (client_id = SPIFFE ID) |
| 2 | Caller | Token obtained with aud: Workload's SPIFFE ID |
| 3 | Envoy + Ext Proc (inbound) | Inbound JWT validated: signature verified via JWKS, issuer checked, optional audience check. Returns 401 if invalid. |
| 4 | Workload | Validated request forwarded to application |
| 5 | Envoy + Ext Proc (outbound) | Outbound request intercepted; token exchanged using Workload's credentials → aud: target-service |
| 6 | Target Service | Token validated (aud: target-service), returns "authorized" |
📊 Mermaid Diagram (click to expand)
sequenceDiagram
autonumber
participant SPIRE as SPIRE Agent
participant Helper as SPIFFE Helper
participant Reg as Client Registration
participant Caller as Caller
participant App as Workload
participant Envoy as AuthProxy (Envoy + Ext Proc)
participant KC as Keycloak
participant Target as Target Service
Note over Helper,SPIRE: Workload Pod Initialization
SPIRE->>Helper: SVID (SPIFFE credentials)
Helper->>Reg: JWT with SPIFFE ID
Reg->>KC: Register client (client_id = SPIFFE ID)
KC-->>Reg: Client credentials (saved to /shared/)
Note over Caller,Target: Runtime Flow
Caller->>KC: Get token (aud: Workload's SPIFFE ID)
KC-->>Caller: Token with workload-aud scope
Note over Caller,Envoy: Inbound Path (JWT Validation)
Caller->>Envoy: Request with Bearer token
Note over Envoy: Ext Proc validates JWT:<br/>signature (JWKS), issuer,<br/>optional audience check
alt Invalid token
Envoy-->>Caller: 401 Unauthorized
end
Envoy->>App: Forward validated request
Note over App,Envoy: Outbound Path (Token Exchange)
App->>Envoy: Call Target Service with Caller's token
Note over Envoy: Ext Proc intercepts outbound<br/>Uses Workload's credentials
Envoy->>KC: Token Exchange (Workload's creds)
KC-->>Envoy: New Token (aud: target-service)
Envoy->>Target: Request + Exchanged Token
Target->>Target: Validate token (aud: target-service)
Target-->>App: "authorized"
App-->>Caller: Response
| Step | From → To | Action |
|---|---|---|
| Initialization Phase | ||
| 1 | SPIRE → SPIFFE Helper | Issue SVID (SPIFFE credentials) |
| 2 | SPIFFE Helper → Client Registration | Pass JWT with SPIFFE ID |
| 3 | Client Registration → Keycloak | Register client (client_id = SPIFFE ID) |
| 4 | Keycloak → Client Registration | Return client credentials (saved to /shared/) |
| Runtime Phase — Inbound (JWT Validation) | ||
| 5 | Caller → Keycloak | Request token (aud: Workload's SPIFFE ID) |
| 6 | Keycloak → Caller | Return token with workload-aud scope |
| 7 | Caller → Envoy (inbound) | Request intercepted by iptables, routed to Envoy inbound listener |
| 8 | Envoy → Ext Proc | Validate JWT: signature (JWKS), issuer, optional audience. Returns 401 if invalid. |
| 9 | Envoy → Workload | Forward validated request to application |
| Runtime Phase — Outbound (Token Exchange) | ||
| 10 | Workload → Envoy (outbound) | Outbound request intercepted by iptables, routed to Envoy outbound listener |
| 11 | Envoy → Ext Proc → Keycloak | Token Exchange (using Workload's credentials) |
| 12 | Keycloak → Envoy | Return new token (aud: target-service) |
| 13 | Envoy → Target Service | Forward request with exchanged token |
| 14 | Target Service | Validate token (aud: target-service) |
| 15 | Target Service → Workload | Return "authorized" |
| 16 | Workload → Caller | Return response |
- No Static Secrets - Credentials are dynamically generated during registration
- Short-Lived Tokens - JWT tokens expire and must be refreshed
- Inbound JWT Validation - Incoming requests are validated at the sidecar (signature via JWKS, issuer, optional audience) before reaching the application
- Self-Audience Scoping - Tokens include the Workload's own identity as audience, enabling token exchange
- Same Identity for Exchange - AuthProxy uses the Workload's credentials (same SPIFFE ID), matching the token's audience
- Transparent to Application - Both inbound validation and outbound token exchange are handled by the sidecar; applications don't need to implement either
- Configurable Targets - Route-based configuration maps destination hosts to target audiences
- Kubernetes cluster (Kind recommended for local development)
- SPIRE installed and running (server + agent) - for SPIFFE version
- Keycloak deployed
- Docker/Podman for building images
The easiest way to get all prerequisites is to use the Rossoctl Quickstart.
- Weather Agent Demo - Recommended starting demo: shows how the operator webhook automatically injects the combined AuthBridge sidecar, with inbound JWT validation and outbound passthrough
- GitHub Issue Agent Demo - End-to-end demo with the real GitHub Issue Agent and GitHub MCP Tool, showing transparent token exchange via AuthBridge
- Manual deployment — deploy everything via
kubectland YAML manifests - UI deployment — import agent and tool via the Rossoctl dashboard
- Manual deployment — deploy everything via
- Token-Exchange Routes - Configuration reference for the
authproxy-routesConfigMap; covers single-target (one route) and multi-target (one agent → many tools) patterns - Lineage Demo - Per-request data lineage from the sidecar on the Weather Agent pair, attached to the running Deployments with the lineage attach kit; shows the same turn fragmented and then as one trace (no Keycloak involved)
All demos except the Lineage Demo cover configuring Keycloak, deploying, and testing.
AuthBridge supports per-host token exchange configuration via routes.yaml:
# Exchange tokens for target-alpha audience when calling this host
- host: "target-alpha-service.authbridge.svc.cluster.local"
target_audience: "target-alpha"
token_scopes: "openid target-alpha-aud"
# Glob patterns supported
- host: "*.internal.svc.cluster.local"
passthrough: true # Skip token exchangeUse keycloak_sync.py to reconcile routes.yaml with Keycloak configuration:
python keycloak_sync.py --config routes.yaml --agent-client "spiffe://..." --yesThis creates target clients, audience scopes, and assigns scopes to the agent.
Every plugin is opt-in. A binary links exactly the plugins its build tags name,
so an artifact's contents are its tag list and nothing more. A build with no
tags registers no plugins, and will reject any config that names one with
unknown plugin "..." (registered: []).
Tag sets are not written by hand. authbridge/scripts/profile-tags holds one
declarative profile per shipped artifact and emits its tags:
| Profile | Artifact | Plugins |
|---|---|---|
local |
desktop authbridge-proxy |
the three parsers + tool-prune |
full |
authbridge image, Kubernetes proxy-sidecar |
all thirteen |
lite |
authbridge-lite image |
sidecar minimum: jwt-validation, token-exchange, litellm-budget-track, static-inject |
envoy |
authbridge-envoy image |
envoy-sidecar set |
cpex |
authbridge-cpex image |
cpex set |
context-guru and session-budget belong to no profile — both are heavy
(roughly 16 MiB and 6 MiB respectively) and are linked only on request.
Go build:
# Desktop set
go build -tags "$(go -C scripts/profile-tags run . local)" ./cmd/authbridge-proxy
# Everything a Kubernetes sidecar ships
go build -tags "$(go -C scripts/profile-tags run . full)" ./cmd/authbridge-proxy
# A profile plus one optional plugin
go build -tags "$(go -C scripts/profile-tags run . full),include_plugin_sessionbudget" \
./cmd/authbridge-proxyDocker build:
docker build --build-arg GO_BUILD_TAGS="$(go -C scripts/profile-tags run . full)" \
-f cmd/authbridge-proxy/Dockerfile .Tags combine with commas. Go does not error on a tag that matches nothing, so a typo silently drops a plugin — prefer a profile name over a hand-written list.
To make a plugin excludable:
- Create a
plugins_<name>.gofile in eachcmd/binary that imports the plugin:
//go:build include_plugin_<name>
package main
import _ "github.com/rossoctl/cortex/authbridge/authlib/plugins/<name>"-
Never import a plugin package from
main.go. An unconditional import cannot be excluded by any tag, and nothing reports that. -
Name the plugin in every profile that should carry it, in
scripts/profile-tags/profiles.go— or add it tooptionalif it deliberately ships in none. A plugin in neither fails the accounting guard. -
Avoid a filename whose suffix is a GOOS or GOARCH token.
plugins_sparc.gocarries an implicitGOARCH=sparcconstraint and compiles on no normal machine, silently dropping the plugin — which is why the sparc plugin's file isplugins_sparcplugin.go. A guard test enforces this.
The build constraint include_plugin_<name> means the file is compiled in only
when -tags include_plugin_<name> is passed.
- authlib — Shared auth building blocks (Go library)
- cmd/authbridge-proxy — proxy-sidecar binary (default mode, full plugin set)
- cmd/authbridge-envoy — envoy-sidecar binary (Envoy + ext_proc, full plugin set)
authbridge-liteimage —cmd/authbridge-proxybuilt with theliteprofile (seeauthbridge/scripts/profile-tags); a build variant, not a separate binary- proxy-init — iptables init container (envoy-sidecar mode only)
- docs/ — framework architecture and plugin author references
Keycloak client registration is handled by the operator's ClientRegistrationReconciler, not by an in-pod sidecar.