kubectl OIDC Login via Dex

June 1, 2026 · View on GitHub

This guide explains how to use kubelogin to authenticate kubectl against the Kubernetes API via Dex and GitHub.

OIDC is the default, day-to-day way to reach the cluster and is intentionally read-only (see RBAC). Write/admin access is break-glass only — the root client-certificate kubeconfig kept in the vault (see Break-glass admin access).

Prerequisites

  • Access to the cluster (kubeconfig with server address)
  • A GitHub account that is a member of the devantler-tech organization
  • Your OIDC identity must be bound to the read-only roles (see RBAC)

1 — Install kubelogin

# Homebrew (macOS / Linux)
brew install int128/kubelogin/kubelogin

# Or with krew
kubectl krew install oidc-login

Verify: kubectl oidc-login --help

2 — Add an OIDC user to kubeconfig

Local cluster (platform.lan)

kubectl config set-credentials oidc-local \
  --exec-api-version=client.authentication.k8s.io/v1 \
  --exec-command=kubectl \
  --exec-arg=oidc-login \
  --exec-arg=get-token \
  --exec-arg=--oidc-issuer-url=https://dex.platform.lan \
  --exec-arg=--oidc-client-id=kubectl \
  --exec-arg=--oidc-extra-scope=email \
  --exec-arg=--oidc-extra-scope=profile \
  --exec-arg=--oidc-extra-scope=groups \
  --exec-arg=--certificate-authority-data=$(cat "$(mkcert -CAROOT)/rootCA.pem" | base64 | tr -d '\n')

Note: The --certificate-authority-data flag is only needed for local development where TLS is signed by mkcert. You can find the CA at $(mkcert -CAROOT)/rootCA.pem.

Production cluster (platform.devantler.tech)

kubectl config set-credentials oidc-prod \
  --exec-api-version=client.authentication.k8s.io/v1 \
  --exec-command=kubectl \
  --exec-arg=oidc-login \
  --exec-arg=get-token \
  --exec-arg=--oidc-issuer-url=https://dex.platform.devantler.tech \
  --exec-arg=--oidc-client-id=kubectl \
  --exec-arg=--oidc-extra-scope=email \
  --exec-arg=--oidc-extra-scope=profile \
  --exec-arg=--oidc-extra-scope=groups

3 — Create a context that uses the OIDC user

# Local
kubectl config set-context oidc@local \
  --cluster=local \
  --user=oidc-local

# Production
kubectl config set-context oidc@prod \
  --cluster=prod \
  --user=oidc-prod

4 — Test

kubectl config use-context oidc@local   # or oidc@prod
kubectl get nodes

# Confirm the identity and that access is read-only:
kubectl auth whoami                      # Username: oidc:ned@devantler.tech
kubectl auth can-i list pods             # yes
kubectl auth can-i get secrets           # no  (Secrets are excluded by design)
kubectl auth can-i create deployments    # no  (writes are break-glass only)

On the first run, kubelogin opens a browser window. Log in with GitHub through Dex. Once authenticated, the token is cached locally and refreshed automatically.

Authentication flow

kubectl get pods


kubelogin (exec credential plugin)
      │  opens browser → https://dex.{domain}/auth
      │                        │
      │                        ▼
      │                  GitHub OAuth login
      │                        │
      │                        ▼
      │                  Dex issues ID token
      │  ◄─── http://localhost:8000 callback


kubectl sends ID token as Bearer header


kube-apiserver validates token
  • oidc-issuer-url matches token issuer ✓
  • oidc-client-id matches token audience ✓
  • signature verified against Dex JWKS ✓
  • email claim → Kubernetes username
  • groups claim → Kubernetes groups


RBAC: ClusterRoleBindings oidc-view + oidc-cluster-reader
  grant READ-ONLY (view + cluster-reader) to oidc:ned@devantler.tech
  (admin is break-glass via the root cert — not OIDC)

RBAC

OIDC access is read-only. Two ClusterRoleBindings in k8s/bases/infrastructure/cluster-role-bindings/oidc-readonly.yaml bind the user whose email matches the Dex email claim (oidc:${admin_email}) to:

BindingRoleGrants
oidc-viewbuilt-in viewread all namespaced resources (pods, deployments, configmaps, pod logs, events, …) — excluding Secrets
oidc-cluster-readercluster-readerread what view omits — mostly cluster-scoped infra (nodes, PVs, storage classes, CRDs, API services, priority/runtime/ingress classes, CSRs) plus RBAC objects and node/pod metrics
subjects:
  - apiGroup: rbac.authorization.k8s.io
    kind: User
    name: "oidc:${admin_email}"   # → oidc:ned@devantler.tech

What is deliberately not granted via OIDC: Secrets (kept in the vault on purpose) and any write/exec verb (create/update/delete, pods/exec, pods/portforward). The cluster-reader ClusterRole is defined in k8s/bases/infrastructure/cluster-roles/cluster-reader.yaml.

To grant read-only access to additional users, add more subjects to both bindings (or switch the subject to a Dex group such as oidc:devantler-tech:platform).

Break-glass admin access

There is no admin path via OIDC — by design. When a write or an otherwise-forbidden operation is genuinely required, use the root client-certificate kubeconfig stored in the vault:

  1. Retrieve the root kubeconfig from the vault and point KUBECONFIG at it (or merge its admin@prod context), e.g.:

    export KUBECONFIG=/path/to/root-kubeconfig.yaml
    kubectl config use-context admin@prod
    
  2. Do the minimal change, then switch back to the OIDC context:

    unset KUBECONFIG                       # back to ~/.kube/config
    kubectl config use-context oidc@prod
    

The root cert authenticates directly against the cluster CA and bypasses OIDC/RBAC role limits (it is cluster-admin), so treat it accordingly: pull it only when needed and never persist it in your day-to-day kubeconfig.

Last-resort regeneration (if the vault copy is lost): a fresh admin kubeconfig can be minted from the Talos control plane with talosctl --talosconfig <admin-talosconfig> kubeconfig.

Dex client configuration

kubelogin uses the kubectl static client defined in the Dex HelmRelease. This is a public client (no secret required) that uses Dex's cross-client trust (trustedPeers) so the issued token has aud: public-client, matching the kube-apiserver's --oidc-client-id flag.

Troubleshooting

SymptomCauseFix
error: You must be logged in to the server (Unauthorized)Token expired or wrong audienceRun kubectl oidc-login setup --oidc-issuer-url=... --oidc-client-id=kubectl to verify the flow
Browser doesn't openkubelogin not installed or not in $PATHVerify kubectl oidc-login --help works
x509: certificate signed by unknown authority (local)mkcert CA not trustedPass --certificate-authority-data or install the mkcert root CA in your system trust store
Forbidden on a readEmail not bound, or resource outside the read-only rolesCheck kubectl auth whoami; confirm the email matches the oidc-readonly.yaml subjects
Forbidden on a writeExpected — OIDC is read-onlyUse the break-glass admin cert for writes

References