kube-oidc-proxy multi-issuer demo
A local, one-command demo of kube-oidc-proxy’s multi-issuer
authentication. A single proxy is configured with a Kubernetes
AuthenticationConfiguration that trusts two independent OIDC issuers, and
tokens from either issuer authenticate through it and are impersonated to the
Kubernetes API server as distinct users.
Everything runs locally in a kind cluster. No cloud accounts, no DNS, no browser.
What it shows
Section titled “What it shows”- Two Dex issuers, each with its own TLS-served OIDC discovery endpoint and one
static user (
alice@example.comon A,bob@example.comon B). - One kube-oidc-proxy, deployed with the Helm chart in
chart/kube-oidc-proxy, running in multi-issuer mode viaauthenticationConfig.content(apiserver.config.k8s.io/v1), withreadinessRequireAllIssuers: trueso the pod is only Ready once both issuers have initialized (JWKS fetched). - A token minted from issuer A authenticates as
oidc-a:alice@example.com; a token from issuer B authenticates asoidc-b:bob@example.com— through the same proxy. That is the multi-issuer proof.
Prerequisites
Section titled “Prerequisites”Install and have on your PATH: kind, docker (daemon running), kubectl,
helm (verified with v4; v3.8+ should also work), openssl, jq, curl, and
the Go toolchain (go, to build the proxy binary). The demo builds the proxy
image from your checkout rather than pulling a published release, so it
exercises the code you have, including uncommitted changes.
Both scripts locate their own files, so they work from any directory. The commands on this page are written for the repository root:
demo/run.sh # build, deploy, and verify (leaves the cluster running)demo/cleanup.sh # delete the kind cluster and generated artifactsrun.sh is idempotent: if a kube-oidc-proxy-demo cluster already exists it is
deleted and recreated. On success it prints, for each issuer, a successful
kubectl get pods -A and a denied kubectl get secrets -A whose error names the
impersonated user — unambiguous proof of which identity the proxy asserted.
How it works
Section titled “How it works”run.sh performs these steps:
- Build the image. Compiles the proxy (
go buildfor linux amd64+arm64, the same outputs asmake buildminus the test-only code generation), buildskube-oidc-proxy:demofrom the repoDockerfile, andkind loads it. The chart is deployed withimage.pullPolicy=Neverso the cluster never reaches for a registry. - Create the cluster.
kind create cluster --name kube-oidc-proxy-demo. - Generate certificates. One CA plus a serving certificate per Dex, whose
SANs cover the in-cluster Service DNS (
dex-a.dex.svc.cluster.local,dex-a.dex,dex-a). Generated material lives in the gitignored.generated/directory. - Deploy the two Dex issuers (namespace
dex) frommanifests/dex.yaml.tpl. Each Dex’sissuer:is set to exactly its Service DNS URL so its discovery document’sissmatches what the proxy dials. The password grant (oauth2.passwordConnector: localenablePasswordDB: true) lets a token be minted with a singlecurl— no browser redirect.
- Install the proxy via Helm. The CA of each issuer is inlined into
manifests/authentication-config.yaml.tplunderissuer.certificateAuthority; the rendered file is passed to the chart with--set-file authenticationConfig.content=.... The username is the token’semailclaim, prefixedoidc-a:/oidc-b:per issuer. - Apply demo RBAC (
manifests/rbac.yaml): aClusterRoleBindinggranting each impersonated username the built-inviewClusterRole. The proxy’s own ServiceAccount already holds the impersonation RBAC granted by the chart. - Mint and verify. For each issuer it port-forwards Dex, mints an ID token
via the password grant (
--resolvekeeps SNI/cert/issuer consistent with the Service DNS), then port-forwards the proxy and runskubectlwith a kubeconfig whose bearer token is the mintedid_token.
Credentials (demo only)
Section titled “Credentials (demo only)”| Issuer | Username | Password | Impersonated as |
|---|---|---|---|
| dex-a | alice@example.com | password | oidc-a:alice@example.com |
| dex-b | bob@example.com | password | oidc-b:bob@example.com |
OAuth client: demo / demo-client-secret. These are throwaway demo values.
A note on TLS verification
Section titled “A note on TLS verification”The kubeconfig used to call the proxy sets insecure-skip-tls-verify: true. The
proxy’s own serving certificate is a self-signed cert generated by the chart,
and this is a local demo — so client-to-proxy TLS is not verified. This does
not weaken the OIDC path: the proxy verifies each Dex’s certificate against
the CA inlined in the AuthenticationConfiguration, and verifies every token’s
signature against the issuer’s JWKS. In production, front the proxy with a
properly issued serving certificate and drop insecure-skip-tls-verify.
Poking at it after a run
Section titled “Poking at it after a run”The cluster is left running. To explore manually:
# Mint a token from issuer A (alice):kubectl --context kind-kube-oidc-proxy-demo -n dex port-forward svc/dex-a 5556:5556 &TOKEN=$(curl -s --resolve dex-a.dex.svc.cluster.local:5556:127.0.0.1 \ --cacert demo/.generated/certs/ca.crt \ https://dex-a.dex.svc.cluster.local:5556/dex/token \ -d grant_type=password -d scope="openid email groups" \ -d client_id=demo -d client_secret=demo-client-secret \ -d username=alice@example.com -d password=password | jq -r .id_token)
# Call the API server through the proxy as that user:kubectl --context kind-kube-oidc-proxy-demo -n kube-oidc-proxy port-forward svc/kube-oidc-proxy 8443:443 &kubectl --server https://127.0.0.1:8443 --insecure-skip-tls-verify \ --token "$TOKEN" get pods -ATroubleshooting
Section titled “Troubleshooting”- Proxy pod never becomes Ready. With
readinessRequireAllIssuers: truethe proxy waits until it has fetched JWKS from both Dex issuers. Check it reached them:kubectl --context kind-kube-oidc-proxy-demo -n kube-oidc-proxy logs deploy/kube-oidc-proxy. A discovery/issmismatch means a Dexissuer:value does not equal its Service DNS URL. get podsis Forbidden. RBAC did not match the impersonated username. The name must be exactlyoidc-a:alice@example.com/oidc-b:bob@example.com(theemailclaim plus the per-issuer prefix). Confirm with the (expected)get secretserror, which prints the username the proxy asserted.- Token mint returns null. Dex was not ready, or the CA/
--resolvehost does not match the serving cert SAN. The mint uses the Service DNS name via--resolveon purpose so SNI, certificate, andissall line up. - Port-forward errors. Re-run
./run.sh; it recreates the cluster cleanly. Stray port-forwards from a crashed run are killed on the script’s exit trap.
Layout
Section titled “Layout”demo/├── run.sh # orchestrates build, deploy, verify├── cleanup.sh # kind delete cluster + rm .generated├── manifests/│ ├── dex.yaml.tpl # Dex ConfigMap/Deployment/Service (rendered per issuer)│ ├── authentication-config.yaml.tpl # multi-issuer AuthenticationConfiguration (CA inlined at runtime)│ ├── proxy-values.yaml # Helm values for the proxy│ └── rbac.yaml # view ClusterRoleBindings for the two identities├── lab-log-delivery/ # optional: ship the audit log + structured log to Kinesis and S3 (local emulator)└── .generated/ # gitignored: certs, rendered config, kubeconfigsThe log-delivery lab builds on this cluster:
run it after ./run.sh to see the proxy’s audit events and structured log
flow through a node agent into Kinesis and S3, all emulated locally.