oidc-gateway is an Envoy-based authentication and authorization gateway for
services that need to accept human, machine, CI/CD, and SPIFFE workload
identities through one consistent backend contract.
The gateway validates credentials at the edge, normalizes the caller into a canonical principal, evaluates route-level authorization with Casbin, and forwards only a minimal identity header to the upstream service. This lets backends avoid auth mechanism-specific code and rely on a single principal format.
- Accepts OIDC bearer JWTs, SPIFFE JWT-SVIDs, and SPIFFE X.509-SVID client certificates.
- Prefers verified SPIFFE X.509-SVID identity when present and ignores bearer tokens in that case.
- Falls back to bearer JWT authentication when no X.509-SVID identity is available.
- Allows configured public paths such as
/healthzand gRPC reflection without credentials. - Denies non-public requests when no supported credential is present.
- Extracts canonical principals such as
oidc:dex:alice,oidc:github:repo:org/repo:workflow:deploy.yml:ref:refs/heads/main, andspiffe:spiffe://example.org/ns/default/sa/backend. - Authorizes those principals against YAML-defined roles and allowed HTTP or gRPC paths.
- Sends upstream services exactly one configured identity header, defaulting to
x-auth-principal.
client
|
| bearer JWT or SPIFFE X.509-SVID client certificate
v
Envoy
| validates bearer JWTs with jwt_authn
| validates downstream mTLS when SPIFFE X.509-SVID is enabled
| strips client-supplied x-jwt-payload and configured principal header
v
oidc-gateway ext_authz server
| prefers X.509 identity, otherwise reads verified x-jwt-payload
| normalizes identity to <auth-family>:<canonical-principal>
| evaluates Casbin RBAC
v
backend service
|
| receives configured principal header only when authorization succeeds
For non-public paths, the precedence is:
- SPIFFE X.509-SVID from verified downstream mTLS.
- OIDC JWT or SPIFFE JWT-SVID from Envoy
jwt_authn. - Deny when neither identity source is available.
cmd/oidc-gateway: runnable ext_authz server, Dockerfile, local Docker Compose test environment, and local test configs.authzserver: Envoy ext_authz implementation, payload extraction, config validation, deny list handling, and Casbin RBAC integration.identity: lightweight public Go module for parsing and formatting canonical identity principals.install/charts/oidc-gateway: Helm chart that deploys Envoy and the authorization server.Taskfile.yml: build, lint, test, license, dependency, Helm, and release automation.
Every authenticated caller is represented as:
<auth-family>:<canonical-principal>
Supported auth families:
oidc: OIDC-style identity from a configured issuer alias.spiffe: SPIFFE identity from either JWT-SVID or X.509-SVID.
Examples:
oidc:dex:alice
oidc:dex:sync-service
oidc:github:repo:org/repo:workflow:deploy.yml:ref:refs/heads/main
spiffe:spiffe://example.org/ns/default/sa/backend
OIDC issuers use a stable configured providerKey, so policy rules do not need
to embed long issuer URLs. SPIFFE principals keep the full SPIFFE ID.
GitHub workflow wildcard rules are intentionally strict. A GitHub wildcard may
contain exactly one *, it must be the final character, and it is only supported
inside the branch ref segment:
oidc:github:repo:org/repo:workflow:deploy.yml:ref:refs/heads/release-*
Backends should consume only the configured principal header. By default this is:
x-auth-principal: <auth-family>:<canonical-principal>Clients must not set this header themselves. Envoy strips client-supplied principal headers, and the ext_authz server sets the configured header only after successful authentication and authorization.
x-jwt-payload is an internal Envoy-to-ext_authz header. In production, Envoy
sets it after jwt_authn validates a bearer JWT. It is not a client API.
The ext_authz server loads YAML from CONFIG_PATH, defaulting to
/etc/oidc-gateway/config.yaml.
claims:
principalClaim: "sub"
emailClaimPath: "email"
headers:
authPrincipal: "x-auth-principal"
issuers:
- providerKey: "dex"
provider: "https://dex.example.com"
authFamily: "oidc"
- providerKey: "github"
provider: "https://token.actions.githubusercontent.com"
authFamily: "oidc"
- provider: "https://spire-oidc.example.org"
authFamily: "spiffe"
denyList:
- "oidc:dex:blocked@example.com"
publicPaths:
- "/healthz"
- "/grpc.reflection"
roles:
admin:
allowedMethods: ["*"]
principals:
- "oidc:dex:admin@example.com"
workloads:
allowedMethods:
- "/example.service.v1.ExampleService/Read"
principals:
- "spiffe:*"
- "oidc:github:repo:org/repo:workflow:deploy.yml:ref:refs/heads/*"Important fields:
claims.principalClaim: JWT claim used as the default OIDC principal value.claims.emailClaimPath: optional claim path used for email deny-list matching.headers.authPrincipal: upstream identity header emitted by ext_authz. Defaults tox-auth-principal; configure the same value in Envoy stripping rules so clients cannot spoof it.issuers[].provider: issuer URL found in the JWTissclaim.issuers[].providerKey: stable alias used inoidc:<providerKey>:...principals. It is required for OIDC issuers.issuers[].authFamily:oidcorspiffe. SPIFFE JWT-SVID issuers producespiffe:<spiffe-id>principals.denyList: principals or email claim values that are always denied.publicPaths: defense-in-depth public path list for requests that reach the authz server. Envoy also disables authz for known public routes.roles: role definitions. Each role hasallowedMethodsand canonicalprincipals.
The Helm chart deploys two main workloads:
- Envoy, exposed through one or more configured
envoy.endpoints, and configured withjwt_authn,ext_authz, optional SPIFFE downstream mTLS, and optional SPIFFE upstream mTLS. - The
oidc-gatewayauthorization server, exposed internally onauthServer.service.port.
Install from the published OCI chart:
helm install oidc-gateway oci://ghcr.io/agntcy/oidc-gateway/helm-charts/oidc-gateway \
--version <version> \
-f values.yamlThe most important values to set are:
envoy.backend.addressandenvoy.backend.port: upstream backend service.envoy.endpoints.oidc: OIDC/JWT listener and service port.envoy.endpoints.mtls: optional SPIFFE X.509-SVID listener and service port.envoy.oidc.issuers: generic OIDC or SPIFFE JWT-SVID issuers for Envoyjwt_authn.envoy.oidc.github: optional GitHub Actions OIDC provider shortcut.envoy.spiffe.enabled: enables SPIFFE SDS and upstream mTLS to the backend.ingress.oidc: optional OIDC/JWT ingress, normally with gRPC TLS termination.ingress.mtls: optional SPIFFE X.509-SVID ingress, normally with TLS passthrough.authServer.oidc: renders the authorization config consumed by the ext_authz server, including the principal header name Envoy strips from client requests.
Minimal OIDC/JWT endpoint example:
envoy:
endpoints:
oidc:
enabled: true
port: 8080
servicePort: 8080
mtls:
enabled: false
backend:
address: "directory.default.svc.cluster.local"
port: 8888
oidc:
issuers:
- name: dex
enabled: true
issuer: "https://dex.example.com"
jwksUri: "https://dex.example.com/.well-known/jwks.json"
jwksHost: "dex.example.com"
spiffe:
enabled: true
trustDomain: example.org
authServer:
oidc:
headers:
authPrincipal: "x-auth-principal"
issuers:
- providerKey: "dex"
provider: "https://dex.example.com"
authFamily: "oidc"
roles:
admin:
allowedMethods: ["*"]
principals:
- "oidc:dex:admin@example.com"Dual endpoint example:
envoy:
endpoints:
oidc:
enabled: true
port: 8080
servicePort: 8080
downstreamTls:
enabled: false
mtls:
enabled: true
port: 8443
servicePort: 8443
downstreamTls:
enabled: true
requireClientCertificate: true
spiffe:
enabled: true
trustDomain: example.org
ingress:
oidc:
enabled: true
host: gateway.example.com
annotations:
nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
mtls:
enabled: true
host: mtls-gateway.example.com
annotations:
nginx.ingress.kubernetes.io/ssl-passthrough: "true"
nginx.ingress.kubernetes.io/backend-protocol: "GRPCS"
authServer:
oidc:
roles:
workloads:
allowedMethods: ["*"]
principals:
- "spiffe:spiffe://example.org/ns/default/sa/workload"Prerequisites:
- Go
1.26.2 - Docker with Buildx
- Docker Compose
taskfrom Taskjqfor the local integration test script
Run the ext_authz server directly:
cd cmd/oidc-gateway
CONFIG_PATH=./config.yaml \
LISTEN_ADDRESS=:9002 \
LOG_LEVEL=debug \
go run .Run unit tests:
task test:unitRun all linters:
task lintBuild the container image:
task buildUseful task commands:
task deps: install pinned local tooling into.bin.task test:unit: run unit tests for all Go modules.task test:unit:coverage: write unit coverage profiles under.coverage.task lint:go: rungolangci-lint.task lint:helm: run Helm chart linting.task check: run lint and license checks.task build: build theoidc-gatewayimage with Docker Buildx.
The repository includes a Docker Compose test environment under
cmd/oidc-gateway:
oidc-gatewayon port9002: Envoy ext_authz gRPC server.envoyon port8080: gateway with header mutation, JWT payload forwarding, and ext_authz configured.mock-backendon port8888: echoes the identity header received from the gateway.- Envoy admin on port
9901.
Start the stack:
cd cmd/oidc-gateway
docker compose up --buildRun the test script in another terminal:
cd cmd/oidc-gateway
./test/test.shThe script checks:
- public health check access without credentials
- rejection when credentials are missing
- rejection of invalid JWT payloads
- successful OIDC principal extraction
- successful SPIFFE JWT-SVID principal extraction
- canonical principal header forwarding to the mock backend
- best-effort XFCC simulation for local X.509-SVID behavior
The local test setup passes x-jwt-payload directly for convenience. Production
deployments should rely on Envoy jwt_authn to validate bearer JWTs and set that
header. The XFCC test is a simulation; real X.509-SVID validation requires
downstream mTLS and trusted SPIFFE certificate handling.
Inspect logs:
docker compose logs oidc-gateway
docker compose logs envoy
docker compose logs mock-backend
curl localhost:9901/stats | grep ext_authzStop the stack:
docker compose downThe authorization server reads:
CONFIG_PATH: path to the YAML authorization config. Defaults to/etc/oidc-gateway/config.yaml.LISTEN_ADDRESS: gRPC listen address. Defaults to:9002.LOG_LEVEL:debug,info,warn, orerror. Defaults toinfo.
- Do not trust client-supplied principal headers or
x-jwt-payloadheaders. Envoy configuration should strip both before authentication processing. The default principal header isx-auth-principal. - Keep
failure_mode_allowdisabled forext_authzso authorization service failures deny protected traffic. - Prefer workload identity and short-lived tokens over long-lived credentials.
- Configure issuer audiences in Envoy when your IdP supports stable audience values.
- For SPIFFE X.509-SVID, ensure downstream client certificates are validated by Envoy through SPIFFE/SPIRE trust bundles before relying on XFCC-derived identity.
- Keep public routes explicit and narrow.
See the open issues for a list of proposed features and known issues.
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are appreciated. For detailed contributing guidelines, please see CONTRIBUTING.md.
Distributed under the Apache-2.0 License. See LICENSE for more information.