README
¶
Dex Token Broker
Trading OAuth2 tokens from Dex for Envoy Gateway.
DexTokenBroker is a lightweight external authorization service for Envoy Gateway. It performs OAuth2 client_credentials requests against Dex, caches access tokens in memory, and returns Authorization: Bearer ... headers that Envoy can forward to backend services.
The project is built for the ext_authz pattern: Envoy calls DexTokenBroker first, DexTokenBroker fetches or reuses a token, and the backend receives an already-authorized request.
Why this exists
Envoy Gateway is good at routing, policy enforcement, and header forwarding, but it does not natively execute OAuth2 client flows. If your backend trusts Dex-issued access tokens and Dex is only reachable inside the cluster, you need a small broker between Envoy and Dex.
DexTokenBroker fills that gap.
Features
- Small Go service with no third-party runtime dependencies
- Designed for Envoy Gateway
ext_authz - OAuth2
client_credentialssupport against Dex - In-memory token cache with periodic cleanup
- Bounded cache size with a simple eviction policy
- Cache key includes a hash of the client secret, so rotated or incorrect secrets do not reuse another token
- In-flight request deduplication to avoid token refresh storms
- TLS-to-Dex by default, with an explicit insecure opt-out for local or trusted-network testing
- Strict request validation and bounded upstream response parsing
- Configurable inbound and outbound header names
- Optional static credentials mode for secret injection from the runtime environment
- Stateless per pod
- Ready for Docker and Kubernetes
- GitHub Actions CI
- GitHub Container Registry publishing
- Release Please for SemVer + Conventional Commits
- Dependabot for Go modules, Docker, and GitHub Actions
- Security workflow with
govulncheckand Trivy
Request flow
sequenceDiagram
participant C as Client
participant E as Envoy Gateway
participant B as DexTokenBroker
participant D as Dex
participant S as Backend Service
C->>E: Request with x-client-id / x-client-secret
E->>B: ext_authz /check
B->>B: Check in-memory cache
alt cache miss
B->>D: POST /token (client_credentials)
D-->>B: access_token
B->>B: Cache token until shortly before expiry
end
B-->>E: 200 OK + Authorization header
E->>S: Forward request with Authorization: Bearer <token>
S-->>C: Response
Project layout
.
├── .github/
│ ├── dependabot.yml
│ └── workflows/
├── cmd/dextokenbroker/
├── internal/tokenbroker/
├── CHANGELOG.md
├── Dockerfile
├── Makefile
└── README.md
Configuration
DexTokenBroker is configured with environment variables:
| Variable | Default | Description |
|---|---|---|
LISTEN_ADDR |
:8080 |
HTTP listen address |
DEX_TOKEN_URL |
https://dex.dex.svc.cluster.local/token |
Dex OAuth2 token endpoint |
HTTP_TIMEOUT |
5s |
Timeout for outbound token requests |
CACHE_CLEANUP_INTERVAL |
5m |
How often expired tokens are removed |
EXPIRY_SAFETY_MARGIN |
30s |
Buffer subtracted from expires_in before a token is treated as expired |
CACHE_MAX_ENTRIES |
1024 |
Maximum number of cached token entries; 0 disables caching |
ALLOW_INSECURE_DEX_URL |
false |
Allow plain http:// Dex token endpoints for local development or trusted internal networks |
LOG_LEVEL |
INFO |
Log level for the service logger |
SHUTDOWN_TIMEOUT |
10s |
Graceful shutdown timeout |
UPSTREAM_AUTH_HEADER |
Authorization |
Header returned to Envoy for the backend request |
CLIENT_ID_HEADER |
x-client-id |
Header name used to read the OAuth client ID |
CLIENT_SECRET_HEADER |
x-client-secret |
Header name used to read the OAuth client secret |
SCOPE_HEADER |
x-scope |
Header name used to read the OAuth scope |
STATIC_CLIENT_ID |
empty | Fixed OAuth client ID; when set together with STATIC_CLIENT_SECRET, incoming credential headers are ignored |
STATIC_CLIENT_SECRET |
empty | Fixed OAuth client secret for static credential mode |
STATIC_SCOPE |
empty | Fixed OAuth scope for static credential mode |
API
POST /check
Expected request headers by default:
x-client-idx-client-secretx-scope(optional)
Those names can be changed with CLIENT_ID_HEADER, CLIENT_SECRET_HEADER, and SCOPE_HEADER.
Success response:
HTTP/1.1 200 OK
Authorization: Bearer <access_token>
Failure responses:
401 Unauthorizedfor missing or rejected credentials502 Bad Gatewayfor invalid responses from Dex503 Service Unavailableif Dex cannot be reached
GET /healthz
Returns 200 OK with body ok.
Local development
Run the broker locally:
go run ./cmd/dextokenbroker
Run tests:
go test ./...
Format code:
make fmt
Build the binary:
make build
Print version information:
go run ./cmd/dextokenbroker --version
Docker
Build the container locally:
docker build -t dextokenbroker:dev .
Run it:
docker run \
-p 8080:8080 \
-e DEX_TOKEN_URL=https://dex.dex.svc.cluster.local/token \
dextokenbroker:dev
Published images are intended for GitHub Container Registry:
ghcr.io/matzegebbe/dextokenbroker
Release tags publish at least these image tags:
v1.2.31.2.31.2latest
Example configuration files:
Envoy Gateway integration
The simplest pattern is to have DexTokenBroker return the final Authorization header and let Envoy forward that header upstream.
Conceptually the setup looks like this:
- The client calls an
HTTPRouteon Envoy Gateway. - Envoy sends an
ext_authzrequest to DexTokenBroker at/check. - Envoy forwards
x-client-id,x-client-secret, and optionallyx-scopeto DexTokenBroker. - DexTokenBroker returns
Authorization: Bearer <token>. - Envoy forwards that
Authorizationheader to the backend service.
If you change UPSTREAM_AUTH_HEADER, Envoy must forward that header name instead.
Example SecurityPolicy shape:
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
name: dex-token-broker
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: my-api
extAuth:
headersToExtAuth:
- x-client-id
- x-client-secret
- x-scope
http:
backendRefs:
- name: dex-token-broker
port: 8080
path: /check
headersToBackend:
- Authorization
Field names and placement have shifted across some Envoy Gateway releases, so treat the YAML above as the target pattern and align it with the exact version of Envoy Gateway you deploy.
Kubernetes example
Minimal Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: dex-token-broker
spec:
replicas: 2
selector:
matchLabels:
app: dex-token-broker
template:
metadata:
labels:
app: dex-token-broker
spec:
containers:
- name: dex-token-broker
image: ghcr.io/matzegebbe/dextokenbroker:latest
ports:
- containerPort: 8080
env:
- name: DEX_TOKEN_URL
value: https://dex.dex.svc.cluster.local/token
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
readinessProbe:
httpGet:
path: /healthz
port: 8080
livenessProbe:
httpGet:
path: /healthz
port: 8080
Cache behavior
The cache stores tokens, not raw credentials.
The cache key is derived from:
client_idscope- SHA-256 hash of
client_secret
That keeps the service stateless while preventing a token minted for one secret from being reused by a different secret for the same client ID.
The cache is bounded by CACHE_MAX_ENTRIES. When the cache reaches capacity, DexTokenBroker first removes expired entries and then evicts the entry that expires soonest.
Expired tokens are removed in two ways:
- lazily on read when an expired entry is accessed
- periodically by a background cleanup goroutine
The broker also deduplicates concurrent cache misses per cache key, which helps avoid a burst of identical /token requests when a token expires under load.
Security notes
- Always use TLS between clients, Envoy Gateway, DexTokenBroker, and Dex.
- DexTokenBroker rejects insecure
http://Dex endpoints by default. If you intentionally run Dex over plain HTTP, setALLOW_INSECURE_DEX_URL=true. - Do not log client secrets.
x-client-id,x-client-secret, andx-scopeare length-limited and rejected if they contain control characters.- Dex token responses are size-limited and the broker rejects non-Bearer token types.
- If all traffic should use one fixed machine client, prefer storing the credentials in Kubernetes Secrets and letting DexTokenBroker own them instead of forwarding credentials from external clients.
STATIC_CLIENT_IDandSTATIC_CLIENT_SECRETare intended for that fixed machine-client mode.- The in-memory cache is pod-local by design. That keeps the service simple, but each replica has its own cache.
- The published container image is non-root, distroless, emits SBOM/provenance on release, and is scanned in CI.
CI, releases, and automation
This repository is set up for GitHub from day one.
CI
.github/workflows/ci.yml runs on pushes to main and on pull requests. It:
- checks
gofmt - runs
go test ./... - runs
govulncheck ./... - builds the binary
- builds the Docker image
Conventional Commits
Commit messages should follow Conventional Commits so Release Please can determine the next semantic version.
Examples:
feat: add optional static credentials mode
fix: return 503 when dex is unavailable
docs: expand envoy gateway integration guide
chore(ci): update build-push-action
Semantic Versioning
Releases follow SemVer:
fix:-> patch releasefeat:-> minor releasefeat!:orBREAKING CHANGE:-> major release
Release Please
.github/workflows/release-please.yml and the two .release-please-* files manage releases.
If you want the release tag and GitHub release created by Release Please to trigger downstream workflows such as the container publish job, create a repository secret named RELEASE_PLEASE_PAT. The workflow uses that secret when present and falls back to GITHUB_TOKEN otherwise.
Flow:
- Merge conventional commits into
main. - Release Please opens or updates a release PR.
- Merge that PR.
- Release Please creates the Git tag and GitHub release.
- The container workflow publishes the matching Docker image to GHCR.
Container publishing
.github/workflows/container.yml publishes multi-architecture images for:
linux/amd64linux/arm64
It also publishes SBOM and provenance attestations with the release image.
Dependabot
.github/dependabot.yml keeps these dependencies current:
- Go modules
- GitHub Actions
- Docker base images
Security workflow
.github/workflows/security.yml runs additional security checks:
govulncheckagainst the Go module graph- Trivy filesystem scanning with SARIF upload to GitHub security results
License
Apache License 2.0. See LICENSE.