Development and testing
Building the proxy, running it against a local cluster, the end-to-end suite, and a local multi-issuer test against a real GitHub Actions token. For running the proxy in production see operations.
- Build and test
- Local multi-issuer test: kind and GitHub Actions
- Chart checks
- Log contract
- Architecture diagrams
- See also
Build and test
Section titled “Build and test”Building kube-oidc-proxy requires Go 1.26 or higher.
Local dev cluster
Section titled “Local dev cluster”A few make targets spin up a kind cluster for quick testing:
-
make dev_cluster_create— create a kind cluster and build/load the proxy and tooling images onto each node. -
make dev_cluster_deploy— build, load, and deploy the proxy alongside a fake OIDC issuer, reachable via a NodePort service. It prints a signed OIDC token valid for the proxy, which you can use directly:Terminal window curl -k https://<node-ip>:<nodeport> -H 'Authorization: bearer <token>'Set
KUBE_OIDC_PROXY_FAKE_APISERVER=true make dev_cluster_deployto also deploy a fake API server that logs the headers and request body the proxy sends — useful for inspecting impersonation output. -
make dev_cluster_destroy— delete the test cluster.
End-to-end tests
Section titled “End-to-end tests”make e2e runs the Go end-to-end suite (test/e2e/suite) against a real
Kubernetes cluster. It’s hermetic: it builds the proxy and test-tool images from
source, creates its own kind cluster, loads the
images, runs the suite, and tears the cluster down on exit (including on failure
or interrupt). No pre-existing cluster is required.
Prerequisites (all on PATH): go, docker (daemon running), kind,
kubectl, helm. Images are built for the host architecture, so the suite runs
on both amd64 and arm64 (e.g. Apple Silicon). The suite deploys the proxy
with the ClusterRole rendered from the Helm chart, not a copy of its own, so a
permission the proxy needs but the chart does not grant fails the suite; a unit
test in the framework checks the same thing against the list of extra keys the
proxy code emits.
make e2e # build images, spin up kind, run the suite, tear downmake e2e-clean # delete a leftover e2e kind cluster (safe if none exists)Useful overrides: E2E_TIMEOUT (Go test timeout, default 30m) and
KUBE_OIDC_PROXY_K8S_VERSION, which selects among the versions declared in
test/e2e/versions/kubernetes-versions.json (default: the newest). Versions
outside the manifest are refused — the manifest is the definition of what
this commit supports. CI tests the newest declared version on every pull
request and the full declared window on the twice-daily scheduled run.
The suite runs in CI on every pull request and on pushes to main
(.github/workflows/e2e.yaml). A companion workflow
(.github/workflows/e2e-oidc-gha.yaml) proves the multi-issuer union
authenticator against the real GitHub Actions OIDC issuer alongside a local
Dex issuer. For a scripted multi-issuer demo, see ../demo/README.md.
Local multi-issuer test: kind and GitHub Actions
Section titled “Local multi-issuer test: kind and GitHub Actions”This runs the proxy in a local kind cluster and authenticates with a real GitHub Actions OIDC token. GitHub only mints these tokens inside a workflow run, so the flow is:
Prerequisites
Section titled “Prerequisites”docker,kind,helm,kubectl,gh(authenticated:gh auth status).- Set these once per shell — every
ghcall needs an explicit--repo:
export GH_SLUG="rafpe/kube-oidc-proxy" # <owner>/<repo> hosting the mint workflowexport BRANCH="master"1. Token-minting workflow (one-time)
Section titled “1. Token-minting workflow (one-time)”You need a workflow_dispatch workflow in the repository that requests an ID
token (permissions: id-token: write, then core.getIDToken(<audience>))
and uploads it as a short-lived artifact.
2. Build the image and create the cluster
Section titled “2. Build the image and create the cluster”docker build -t kube-oidc-proxy:test .kind create cluster --name oidc-testkind load docker-image kube-oidc-proxy:test --name oidc-test3. Deploy the chart
Section titled “3. Deploy the chart”This uses the simple sub-as-username mapping, which is enough to prove the
flow. The mapping to run in production, with a stable username, trust tiers,
audit extras and numeric-ID pinning, is the
GitHub Actions recipe.
cat > /tmp/values-test.yaml <<'EOF'image: repository: kube-oidc-proxy tag: test pullPolicy: Never
authenticationConfig: content: | apiVersion: apiserver.config.k8s.io/v1 kind: AuthenticationConfiguration jwt: - issuer: url: https://token.actions.githubusercontent.com audiences: ["kube-oidc-proxy-kind-test"] claimMappings: # GitHub's newer sub is ID-embedded: repo:Owner@<owner_id>/repo@<repo_id>:ref:... username: claim: sub prefix: "gha:" # GitHub tokens carry no groups claim — CEL synthesizes one from # repository_owner (case-sensitive: "RafPe" != "rafpe"): groups: expression: '["github:" + claims.repository_owner]'EOF
helm install kube-oidc-proxy oci://ghcr.io/rafpe/charts/kube-oidc-proxy \ -n kube-oidc-proxy --create-namespace -f /tmp/values-test.yamlkubectl -n kube-oidc-proxy rollout status deploy/kube-oidc-proxy --timeout=180s
# The proxy lists its issuers at startup, then reports each one initialized:kubectl -n kube-oidc-proxy logs deploy/kube-oidc-proxy \ | jq -r 'select((.event_type // "") | startswith("oidc.issuer.")) | [.event_type, .issuer_name] | @tsv'To change config later, edit the values file and helm upgrade kube-oidc-proxy ./chart/kube-oidc-proxy -n kube-oidc-proxy -f /tmp/values-test.yaml — the
checksum annotation rolls the pods automatically.
4. RBAC
Section titled “4. RBAC”Decode a token first (step 5) if you’re unsure of your exact claims. With the
default sub mapping and GitHub’s ID-embedded subject format:
# Group binding — matches the CEL-synthesized group (mind the case!):kubectl create clusterrolebinding gha-owner-view \ --clusterrole=view --group="github:RafPe"
# Exact-subject binding — GitHub's sub embeds owner/repo IDs:kubectl create clusterrolebinding gha-branch-view \ --clusterrole=view \ --user="gha:repo:RafPe@9809655/kube-oidc-proxy@1308696420:ref:refs/heads/master"5. Mint a token and fetch it (TTL ~5 min — move fast)
Section titled “5. Mint a token and fetch it (TTL ~5 min — move fast)”gh workflow run oidc.yaml --repo "$GH_SLUG" --ref "$BRANCH"sleep 5RUN_ID=$(gh run list --repo "$GH_SLUG" --workflow=oidc.yaml -L1 \ --json databaseId -q '.[0].databaseId')gh run watch "$RUN_ID" --repo "$GH_SLUG"rm -rf /tmp/oidc && gh run download "$RUN_ID" --repo "$GH_SLUG" \ -n oidc-token -D /tmp/oidcTOKEN=$(cat /tmp/oidc/token.jwt)
# Inspect the claims you are about to authenticate with:echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null \ | jq '{iss,aud,sub,repository,repository_owner,ref}'6. Test through the proxy
Section titled “6. Test through the proxy”kubectl -n kube-oidc-proxy port-forward svc/kube-oidc-proxy 8443:443 &sleep 3Who does the cluster think you are?
kubectl --server=https://127.0.0.1:8443 --insecure-skip-tls-verify=true \ --token="$TOKEN" auth whoamiExpected: your prefixed username plus groups [github:RafPe, system:authenticated].
Then prove authorization scoping:
PROXY="--server=https://127.0.0.1:8443 --insecure-skip-tls-verify=true --token=$TOKEN"kubectl $PROXY get pods -A # allowed (view)kubectl $PROXY create ns nope # forbidden (view cannot write)If you get 401, the token expired — rerun step 5. Steps 2–4 stay up.
--insecure-skip-tls-verifyis acceptable only against this throwaway kind cluster (the chart generated an ephemeral self-signed cert). Real deployments settls.secretName/cert-manager and ship the CA in kubeconfig.
7. Access logs and cleanup
Section titled “7. Access logs and cleanup”kubectl -n kube-oidc-proxy logs deploy/kube-oidc-proxy --tail=20 # request.access.decided: AuSuccess / AuFailkill %1kind delete cluster --name oidc-testAdding the next system
Section titled “Adding the next system”A pure config change — no new deployments:
- Append another
jwt:entry toauthenticationConfig.content(its issuer URL, its own audience, and a distinct prefix, e.g.sys-a:— never reuse or omit prefixes across issuers). - Add RBAC bindings for the new prefixed identities.
helm upgrade— pods roll automatically; with the default readiness mode the rollout completes even if the new issuer is temporarily unreachable (it’s logged as pending and starts working the moment its JWKS loads).
Chart checks
Section titled “Chart checks”Committed fixtures under chart/kube-oidc-proxy/ci/ cover both authentication
configurations and are used by the helm-chart GitHub Actions workflow,
together with two scripts that pin down what the chart renders:
helm lint chart/kube-oidc-proxy -f chart/kube-oidc-proxy/ci/single-issuer-values.yamlhelm template t chart/kube-oidc-proxy -f chart/kube-oidc-proxy/ci/multi-issuer-values.yamlbash hack/verify-chart-logging.sh # logging values render as flagsbash hack/verify-chart-rbac.sh # ClusterRole grants every declared userextras keybash hack/verify-chart-metrics.sh # metrics values render the listener, Service and monitorsLog contract
Section titled “Log contract”The event registry in pkg/logging/events.go is
the source for the event table in the logging
reference; make eventdoc regenerates it and CI fails on a diff. Never
hand-edit between the events:begin and events:end markers. The rules that
keep saved queries working across releases are in
logging: compatibility.
Architecture diagrams
Section titled “Architecture diagrams”The C4 diagrams under docs/c4/ are generated from
workspace.dsl with Structurizr; edit the DSL, not the
PNGs. Architecture shows them.
docs/diagrams/*.svg and demo/diagrams/*.svg are checked by
hack/verify-diagrams.sh in CI. Two kinds exist:
- Hand-authored SVGs (
metrics-data-flow.svg): literal colours, a<title>, no CSS variables. - Archify exports (
gha-token-flow.svg,demo/diagrams/multi-issuer-demo.svg): built from the*.archify.jsonnext to each SVG with Archify 3.0.1, a local tool that is not a dependency of this repository (it ships as thearchifyagent skill; its CLI isnode <skill-dir>/bin/archify.mjs). To change one, edit the JSON, runarchify finalize architecture <name>.archify.json <name>.html, thennode website/scripts/export-diagram-svg.mjs <name>.html <name>.svgfromwebsite/afternpm ci. Use the same Archify version so the output only changes where the JSON did. The export follows the reader’s colour scheme and embeds its fonts.
Metrics demo
Section titled “Metrics demo”hack/metrics-demo/ builds a kind cluster running kube-prometheus-stack and
this chart with metrics, a ServiceMonitor and the three Grafana dashboards, and
proves the dashboards are honest: it drives nineteen kinds of traffic —
allowed and forbidden requests, a 5xx the API server itself answers, invalid,
expired and username-less tokens, allowed, denied and simultaneous
impersonation, the two anomaly denials, both token-passthrough outcomes, a
watch, an exec, a log stream, non-resource paths and nine non-Kubernetes verbs
— fails if any dashboard panel’s query returns an empty result, and only then
captures the screenshots in docs/dashboards/. It fakes no failure of the hop
to the API server, so the overview’s “Upstream failures/s” panel reads zero, as
it should against a healthy upstream. Four steps, each one command:
make metrics_demo_up # cluster, Prometheus, Grafana, the chartmake metrics_demo_load METRICS_DEMO_LOAD_ARGS=--once # one call of every traffic kindmake metrics_demo_load METRICS_DEMO_LOAD_ARGS="--duration 10m"make metrics_demo_verify # every panel has data; write the PNGsmake metrics_demo_down # delete the cluster and its stateup leaves hack/metrics-demo/.state/ holding the kubeconfig, the mock
issuer’s CA, key and URL and the proxy’s serving certificate; the load
generator and verify read it, and down removes it. It needs Docker, Go,
kubectl, helm, jq and curl — but no kind CLI: the scripts run the kind pinned
in go.mod, which is the one the e2e suite uses. Prerequisites, the pinned
versions and what each file does are in
hack/metrics-demo/README.md.
See also
Section titled “See also”- Operations — running the proxy in production.
- Releases — how a merged change becomes a release.
- Multi-issuer authentication — the configuration the local test exercises.