A reverse proxy that exposes Kubernetes OIDC discovery and JWKS endpoints with lightweight in-memory caching so external systems can configure workload identity federation without requiring anonymous authentication on the Kubernetes API server.
Kubernetes can act as an OIDC identity provider, allowing workloads running in the cluster to authenticate to external systems using ServiceAccount tokens. This is commonly known as workload identity federation.
For workload identity federation to work, the external system (such as a cloud provider, HashiCorp Vault, or any OIDC-compliant service) needs to:
- Discover the OIDC configuration - by fetching
/.well-known/openid-configurationfrom the Kubernetes API server - Validate JWT signatures - by fetching the public keys from
/openid/v1/jwks
The challenge: Even if your Kubernetes cluster is entirely private (no public API server endpoint), these OIDC endpoints must be publicly accessible for external systems to validate tokens. External systems cannot reach into your private network to fetch these documents.
Common scenarios requiring public OIDC endpoints:
- Cloud Provider Workload Identity: AWS IAM Roles for Service Accounts (IRSA), GCP Workload Identity, Azure Workload Identity
- HashiCorp Vault: JWT/OIDC authentication method
- CI/CD Systems: GitHub Actions OIDC, GitLab CI OIDC
- Any OIDC-relying party: Services that accept Kubernetes ServiceAccount tokens for authentication
When running a Kubernetes cluster with --anonymous-auth=false (a common security hardening practice), external systems cannot access the OIDC discovery and JWKS endpoints because:
- The Kubernetes API server requires authentication for all requests
- External systems don't have credentials to authenticate to your cluster
- You don't want to enable anonymous authentication just to serve these public OIDC documents
This gateway solves the problem by:
- Running inside the cluster with a ServiceAccount
- Authenticating to the API server using the ServiceAccount token
- Fetching and re-serving the OIDC discovery and JWKS documents
- Providing lightweight in-memory caching with configurable TTL
- Pretty-printing JSON responses by default
Important: The OIDC discovery document and JWKS contain only public information (public keys and metadata). They do not expose any secrets or sensitive cluster information. Making these endpoints publicly accessible is safe and necessary for workload identity federation to function.
The gateway exposes exactly two OIDC endpoints:
GET /.well-known/openid-configuration- OIDC discovery documentGET /openid/v1/jwks- JSON Web Key Set
Additionally, health check endpoints are available:
GET /healthz- Liveness check (fetches and caches both OIDC endpoints)GET /readyz- Readiness check (fetches and caches both OIDC endpoints)
All other paths return 404 Not Found.
curl http://kube-oidc-gateway/.well-known/openid-configurationcurl http://kube-oidc-gateway/openid/v1/jwkscurl http://kube-oidc-gateway/healthzAll configuration is done via environment variables with safe defaults for in-cluster operation.
| Variable | Type | Default | Description |
|---|---|---|---|
LISTEN_ADDR |
string | 0.0.0.0 |
Bind address |
LISTEN_PORT |
string | 8080 |
HTTP listen port |
UPSTREAM_HOST |
string | https://kubernetes.default.svc |
Kubernetes API server base URL |
UPSTREAM_TIMEOUT_SECONDS |
int | 5 |
Timeout for upstream HTTP calls |
CACHE_TTL_SECONDS |
int | 60 |
In-memory cache TTL in seconds |
CLIENT_CACHE_TTL_SECONDS |
int | 3600 |
Cache-Control/Expires TTL advertised to clients in seconds |
PRETTY_PRINT_JSON |
bool | true |
Pretty-print JSON responses |
SA_TOKEN_PATH |
string | /var/run/secrets/kubernetes.io/serviceaccount/token |
ServiceAccount token path |
SA_CA_CERT_PATH |
string | /var/run/secrets/kubernetes.io/serviceaccount/ca.crt |
ServiceAccount CA certificate path |
The ServiceAccount used by the gateway requires minimal permissions to access only the two non-resource OIDC endpoints:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kube-oidc-gateway
rules:
- nonResourceURLs:
- "/openid/v1/jwks"
- "/.well-known/openid-configuration"
verbs: ["get"]Deploy with the following manifest that includes all necessary resources:
apiVersion: v1
kind: Namespace
metadata:
name: kube-oidc-gateway
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: kube-oidc-gateway
namespace: kube-oidc-gateway
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kube-oidc-gateway
rules:
- nonResourceURLs:
- "/openid/v1/jwks"
- "/.well-known/openid-configuration"
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: kube-oidc-gateway
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: kube-oidc-gateway
subjects:
- kind: ServiceAccount
name: kube-oidc-gateway
namespace: kube-oidc-gateway
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: kube-oidc-gateway
namespace: kube-oidc-gateway
spec:
replicas: 2
selector:
matchLabels:
app: kube-oidc-gateway
template:
metadata:
labels:
app: kube-oidc-gateway
spec:
serviceAccountName: kube-oidc-gateway
containers:
- name: kube-oidc-gateway
image: ghcr.io/unitvectory-labs/kube-oidc-gateway:latest
ports:
- containerPort: 8080
env:
- name: CACHE_TTL_SECONDS
value: "60"
- name: CLIENT_CACHE_TTL_SECONDS
value: "3600"
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 30
readinessProbe:
httpGet:
path: /readyz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
resources:
requests:
memory: "32Mi"
cpu: "10m"
limits:
memory: "64Mi"
cpu: "100m"
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 65534
capabilities:
drop:
- ALL
---
apiVersion: v1
kind: Service
metadata:
name: kube-oidc-gateway
namespace: kube-oidc-gateway
spec:
selector:
app: kube-oidc-gateway
ports:
- port: 80
targetPort: 8080
type: ClusterIPSave this to a file and deploy with:
kubectl apply -f kube-oidc-gateway.yamlThe default deployment creates a ClusterIP service. You can expose it externally using:
Ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: kube-oidc-gateway
namespace: kube-oidc-gateway
spec:
rules:
- host: oidc.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: kube-oidc-gateway
port:
number: 80LoadBalancer Service:
kubectl patch svc kube-oidc-gateway -n kube-oidc-gateway -p '{"spec":{"type":"LoadBalancer"}}'- No Built-in Authentication: This service does not implement authentication or authorization. It serves public OIDC discovery data but access control is your responsibility.
- Network Exposure: Control who can access the service using Kubernetes NetworkPolicies, Ingress authentication, or firewall rules.
- Minimal RBAC: The ServiceAccount has minimal permissions (only read access to two non-resource URLs).
- Works with --anonymous-auth=false: Designed specifically to work when the API server disables anonymous authentication.
The gateway follows a simple request flow:
External Client → kube-oidc-gateway → Kubernetes API Server
- Request Handling: The gateway exposes only two OIDC endpoints plus health checks
- Authentication: Uses the mounted ServiceAccount token to authenticate to the Kubernetes API server
- Caching: Maintains an in-memory cache with configurable TTL to reduce load on the API server
- Response Processing: Optionally pretty-prints JSON responses for easier debugging
Key design principles:
- Minimal attack surface: Only two read-only endpoints are exposed
- No secrets in responses: OIDC documents contain only public keys and metadata
- Resilient: Serves stale cache entries on upstream failures
- Lightweight: Single binary with minimal resource requirements
The gateway logs all requests with the following information:
- Request path
- HTTP status code
- Cache hit/miss
- Request duration
Example log output:
path=/.well-known/openid-configuration status=200 cache_hit=true duration=1.234ms
503 Service Unavailable on /healthz or /readyz
- The gateway cannot reach the Kubernetes API server
- Check ServiceAccount token is mounted correctly
- Verify ClusterRole permissions are applied
502 Bad Gateway on OIDC endpoints
- Upstream request to Kubernetes API server failed
- Check network connectivity to
kubernetes.default.svc - Verify the API server is healthy
- Default upstream cache TTL is 60 seconds
- Default client cache TTL is 3600 seconds
- Responses include
Cache-Control: public, max-age=...andExpiresheaders based onCLIENT_CACHE_TTL_SECONDS - On cache miss, fetches from upstream and caches the result
- On upstream failure with cached data, serves stale cache (stale-on-error)
- ETags are generated for cache validation
go build -o kube-oidc-gateway .docker build -t kube-oidc-gateway:latest .