SSO Integrationadvanced18 min readJune 12, 2026

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.

KT

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-apiserver flags
  • kubectl installed 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:

  1. Clients → Create client
  2. Client type: OpenID Connect
  3. Client ID: kubernetes
  4. 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:8000 and http://localhost:18000
  • Web origins: http://localhost:8000 and http://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.

Keycloak admin console showing the kubernetes client with Client authentication OFF and Valid redirect URIs set to http://localhost:8000 and http://localhost:18000
Public client with loopback redirect URIs — kubelogin listens on these ports during the login flow

1.1 — Enforce PKCE

Clients → kubernetes → Advanced → Proof Key for Code Exchange Code Challenge MethodS256.

Keycloak admin console Advanced tab for the kubernetes client showing Proof Key for Code Exchange Code Challenge Method set to S256
S256 enforcement means Keycloak rejects authorization requests without a valid PKCE challenge — required for public clients

1.2 — Add the groups mapper

Create a Group Membership mapper so group names appear in the token:

  1. Clients → kubernetes → Client scopes → kubernetes-dedicated → Add mapper → By configuration
  2. Select Group Membership
  3. Name: groups
  4. Token Claim Name: groups
  5. Full group path: OFF
  6. Add to ID token: ON
  7. Add to access token: ON
  8. 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.

Keycloak admin console showing the Group Membership mapper for the kubernetes client with Token Claim Name set to groups and Full group path set to OFF
Full group path OFF — without the leading slash, the prefixed group name becomes oidc:k8s-admins, not oidc:/k8s-admins

1.3 — Create groups and assign users

  1. Groups → Create group → k8s-admins
  2. Create group → k8s-devs
  3. Assign users: Users → [user] → Groups → Join group
Keycloak Groups page listing k8s-admins and k8s-devs groups
Group names will appear in RBAC subjects with the oidc: prefix — exactly as written here

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.

Terminal showing a text editor with kube-apiserver.yaml open and the OIDC flags added to the command array in the container spec
Save the file and kubelet automatically restarts the API server pod — wait 30-60 seconds before testing

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-args parameter 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
Terminal showing kubectl oidc-login version output with the installed kubelogin version number
kubectl discovers kubelogin via the kubectl-oidc_login binary name — verify this command works before configuring kubeconfig

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.

Terminal showing kubectl config view output with the keycloak-user credentials entry and keycloak-context context using the exec credential plugin configuration
The exec block tells kubectl to call kubelogin when it needs a token — kubelogin caches the token and refreshes it automatically

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.

Browser showing the Keycloak login page that kubelogin opened for Kubernetes authentication
kubelogin opens the system browser — log in with your Keycloak credentials and the browser redirects back to localhost:8000

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

Terminal showing the kubectl get nodes output listing cluster nodes after successful Keycloak SSO login
Subsequent kubectl commands use the cached token until it expires — kubelogin refreshes automatically using the refresh token

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-url uses 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-prefix and --oidc-groups-prefix set 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.

See integration packages