Kubernetes SSO with Keycloak using OIDC and kubelogin
Configure Keycloak as the OIDC provider for the Kubernetes API server using kube-apiserver flags and kubelogin as a kubectl exec credential plugin. Covers public PKCE client setup, groups mapper, RBAC bindings, and kubeconfig.
KeycloakPro Team
KeycloakPro Team
Introduction
Kubernetes has built-in OIDC support in kube-apiserver. You pass a handful of flags pointing at your Keycloak realm, and the API server validates JWTs directly — no sidecar, no admission webhook. On the developer side, kubelogin (a kubectl credential plugin) handles the browser-based login flow and caches the token in kubeconfig automatically.
This approach puts authentication in Keycloak and authorization in Kubernetes RBAC. Groups from Keycloak map to Kubernetes subjects via a configurable prefix, which keeps your RBAC bindings readable and prevents namespace collisions with system users.
This guide covers kubeadm-provisioned clusters and managed clusters that expose kube-apiserver flags (EKS uses a different mechanism). Keycloak 24+ is required.
Prerequisites
- Keycloak 24+ with HTTPS
- Kubernetes cluster with control plane access to edit
kube-apiserverflags kubectlinstalled locally- Keycloak admin access
- HTTPS between the API server and Keycloak (required — Kubernetes rejects HTTP OIDC issuers)
Step 1 — Create the Keycloak public client
Developer machines log into Keycloak via a browser. Distributing a client secret to every developer creates a rotation headache, so use a public PKCE client instead. The PKCE code challenge provides the security guarantee without a shared secret.
In the Keycloak admin console:
- Clients → Create client
- Client type:
OpenID Connect - Client ID:
kubernetes - Click Next
Capability config:
- Client authentication: OFF (public client)
- Standard flow: ON
- Direct access grants: OFF
- Click Next
Login settings:
- Valid redirect URIs:
http://localhost:8000andhttp://localhost:18000 - Web origins:
http://localhost:8000andhttp://localhost:18000
kubelogin starts a local HTTP server on port 8000 (or 18000 as fallback) to receive the authorization code. These are loopback addresses — Keycloak allows them for public clients.

1.1 — Enforce PKCE
Clients → kubernetes → Advanced → Proof Key for Code Exchange Code Challenge Method → S256.

1.2 — Add the groups mapper
Create a Group Membership mapper so group names appear in the token:
- Clients → kubernetes → Client scopes → kubernetes-dedicated → Add mapper → By configuration
- Select Group Membership
- Name:
groups - Token Claim Name:
groups - Full group path: OFF
- Add to ID token: ON
- Add to access token: ON
- Save
Full group path OFF sends k8s-admins instead of /k8s-admins. The --oidc-groups-prefix flag on the API server adds a prefix, so the final subject name is oidc:k8s-admins. Your RBAC bindings use this prefixed form.

1.3 — Create groups and assign users
- Groups → Create group →
k8s-admins - Create group →
k8s-devs - Assign users: Users → [user] → Groups → Join group

Step 2 — Configure kube-apiserver
On a kubeadm cluster, kube-apiserver is a static pod. Edit the manifest:
sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml
Add these flags to the command array in the kube-apiserver container spec:
- --oidc-issuer-url=https://keycloak.example.com/realms/YOUR_REALM
- --oidc-client-id=kubernetes
- --oidc-username-claim=preferred_username
- --oidc-username-prefix=oidc:
- --oidc-groups-claim=groups
- --oidc-groups-prefix=oidc:
Replace YOUR_REALM and keycloak.example.com with your values.
The prefixes (oidc:) prevent collisions between Keycloak users and built-in Kubernetes users like system:masters. A developer named alice becomes oidc:alice in audit logs and RBAC policies.

After saving, kubelet detects the manifest change and restarts the API server pod automatically. You don't need to run any restart command. Check the pod status:
kubectl get pods -n kube-system -l component=kube-apiserver
Wait until the pod returns to Running before continuing.
2.1 — Managed cluster equivalents
If you're on a managed cluster:
- EKS: OIDC configuration goes in the EKS OIDC provider ARN, not kube-apiserver flags. This guide doesn't cover EKS — see the EKS OIDC documentation.
- GKE: Add OIDC flags via the
--extra-api-server-argsparameter in the cluster config. - AKS: Use Azure AD as the OIDC provider instead — AKS doesn't expose kube-apiserver flag access for third-party OIDC.
Step 3 — Install kubelogin
kubelogin is a kubectl credential plugin that handles the browser login flow. Its binary is named kubectl-oidc_login — kubectl discovers it via the kubectl- naming convention.
macOS (Homebrew):
brew install int128/kubelogin/kubelogin
Linux (binary download):
curl -Lo kubectl-oidc_login \
https://github.com/int128/kubelogin/releases/latest/download/kubelogin_linux_amd64.zip
unzip kubectl-oidc_login
chmod +x kubectl-oidc_login
sudo mv kubectl-oidc_login /usr/local/bin/
Windows (winget):
winget install int128.kubelogin
Verify the install:
kubectl oidc-login version

Step 4 — Configure kubeconfig
Add a new user entry to kubeconfig that uses kubelogin as an exec credential plugin:
kubectl config set-credentials keycloak-user \
--exec-api-version=client.authentication.k8s.io/v1beta1 \
--exec-command=kubectl \
--exec-arg=oidc-login \
--exec-arg=get-token \
--exec-arg=--oidc-issuer-url=https://keycloak.example.com/realms/YOUR_REALM \
--exec-arg=--oidc-client-id=kubernetes \
--exec-arg=--oidc-extra-scope=groups
Then set up a context pointing to this user:
kubectl config set-context keycloak-context \
--cluster=YOUR_CLUSTER_NAME \
--user=keycloak-user
kubectl config use-context keycloak-context
No --oidc-client-secret argument — this is a public PKCE client.

Step 5 — Create RBAC bindings
Kubernetes RBAC binds roles to subjects. Subjects for OIDC users and groups use the prefixed names from the --oidc-username-prefix and --oidc-groups-prefix flags set in Step 2.
A user alice from Keycloak has the subject oidc:alice.
A group k8s-admins from Keycloak has the subject oidc:k8s-admins.
ClusterRoleBinding for admins:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: keycloak-admins
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: Group
name: oidc:k8s-admins
apiGroup: rbac.authorization.k8s.io
ClusterRoleBinding for developers (read-only cluster access, write access to specific namespaces):
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: keycloak-devs-view
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: view
subjects:
- kind: Group
name: oidc:k8s-devs
apiGroup: rbac.authorization.k8s.io
Apply both:
kubectl apply -f admins-binding.yaml
kubectl apply -f devs-binding.yaml
To give k8s-devs write access to a specific namespace, create a RoleBinding (not ClusterRoleBinding) in that namespace:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: keycloak-devs-edit
namespace: staging
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: edit
subjects:
- kind: Group
name: oidc:k8s-devs
apiGroup: rbac.authorization.k8s.io
Step 6 — Test the login
Run any kubectl command. kubelogin detects there's no valid cached token and opens a browser:
kubectl get nodes
A browser window opens at https://keycloak.example.com/realms/YOUR_REALM. Log in with a Keycloak account that's a member of k8s-admins or k8s-devs.

After login, kubelogin stores the token and kubectl completes the original command:

Verify who the API server sees:
kubectl auth whoami
The output shows oidc:alice as the username and the groups array with oidc:k8s-admins or oidc:k8s-devs.
Troubleshooting common issues
kube-apiserver fails to start after adding OIDC flags
The API server fetches the Keycloak discovery document on startup to validate the configuration. If it can't reach Keycloak (network issue, DNS failure, TLS error), the pod enters CrashLoopBackOff.
Check the pod logs:
kubectl logs -n kube-system -l component=kube-apiserver --previous
Common causes:
- The
--oidc-issuer-urluses HTTP instead of HTTPS - Keycloak uses a private CA not trusted by the API server's OS
- The realm name in the URL is wrong
To add a private CA, mount it into the API server pod and add --oidc-ca-file=/path/to/ca.crt to the flags.
"Unauthorized" when running kubectl after successful kubelogin
The API server accepted the token (authentication passed) but no RBAC binding matches the user or group. Decode the token kubelogin received:
kubectl oidc-login get-token \
--oidc-issuer-url=https://keycloak.example.com/realms/YOUR_REALM \
--oidc-client-id=kubernetes \
--oidc-extra-scope=groups 2>/dev/null | jq -r '.status.token' | \
cut -d. -f2 | base64 -d | jq .
Check preferred_username and groups in the payload. Then check the RBAC bindings match those values with the oidc: prefix.
Groups claim is absent from the token
The mapper must be on the kubernetes-dedicated client scope, not a realm-level scope. Check Clients → kubernetes → Client scopes → kubernetes-dedicated. If the mapper is under Realm Scopes instead, OIDC tokens for this client won't include the groups claim.
kubelogin opens the browser but the callback fails
kubelogin listens on http://localhost:8000. If that port is taken by another process, it falls back to http://localhost:18000. Make sure both are in the Keycloak client's Valid redirect URIs. Firewalls that block loopback ports can also cause this — unusual but possible on some enterprise machines.
Token expiry forces repeated logins
kubelogin caches the refresh token. If the Keycloak session idle timeout is shorter than typical kubectl usage patterns, the refresh token expires and the user has to log in again. Adjust the realm's SSO Session Idle timeout under Realm Settings → Sessions to match your team's working patterns — a few hours is typical.
Username prefix causes RBAC lookups to fail
If RBAC bindings were created with bare usernames (no oidc: prefix) before adding the --oidc-username-prefix flag, those bindings won't match anymore. Update existing bindings to use the oidc: prefix, or set --oidc-username-prefix=- to disable the prefix entirely (not recommended in multi-tenant clusters).
Production checklist
- HTTPS between kube-apiserver and Keycloak — HTTP is rejected
-
--oidc-username-prefixand--oidc-groups-prefixset to the same value —oidc:is conventional - RBAC bindings use prefixed subject names matching the flags above
- Full group path OFF in the Keycloak mapper — leading slashes break the group name match
- PKCE S256 enforced on the Keycloak client — public clients without PKCE are vulnerable to code interception
- API server restart tested on a non-production cluster first — misconfigured flags take the API server down
- kubelogin binary in PATH on every developer machine — kubectl silently falls back to no auth if it's missing
- Cluster admin binding kept for at least one break-glass user before removing existing auth methods
- kube-apiserver manifest stored in version control — rebuilding a control plane node requires it
Need help integrating Kubernetes with Keycloak?
We deliver production-ready Kubernetes + Keycloak integrations in 1–3 weeks.
Fixed-price, zero vendor lock-in, full source code ownership.