Certificate-based Kubernetes authentication has three audit problems: no central revocation mechanism, no MFA enforcement, and no connection to your identity governance program. When someone leaves, their cert keeps working until it expires. There’s no way to tie cluster access to your offboarding process, and no way to require a second factor before kubectl runs. In a NIST-based assessment those are AC-2 and IA-2(1) findings.
OIDC moves authentication to Okta. Disable a user there and cluster access is gone immediately — no cert hunting, no waiting for expiry.
This post picks up where Kubernetes RBAC — Creating Users, Groups, and Securing Credentials with Vault left off. The RBAC configuration stays exactly the same — ClusterRoles, ClusterRoleBindings, group-based permissions — but authentication moves to Okta via OIDC. Users log in with their existing Okta credentials, group membership in Okta drives what they can do in the cluster, and disabling a user in Okta immediately revokes their access.
| Certificate Auth | OIDC / Okta | |
|---|---|---|
| User identity | CN field in cert | Token claims from Okta |
| Group membership | O field in cert | Groups claim in token |
| User management | Manual cert generation | Managed in Okta |
| Revoking access | Cert expiry or CRL | Disable in Okta — immediate |
| MFA enforcement | Not possible | Okta authentication policy |
| RBAC | ClusterRole/Binding | Same — no changes needed |
How It Works
Kubernetes doesn’t manage users itself — it delegates authentication to external systems. When a user runs kubectl, a plugin called kubelogin handles the browser-based login flow with Okta and returns a signed JWT. kubectl passes that token to the API server, which validates it against Okta’s public keys and reads the claims to determine who the user is and what groups they belong to.
↓
kubelogin (exec credential in kubeconfig)
↓
Browser opens → Okta login page
↓
User authenticates (password + MFA)
↓
Okta issues JWT: sub: unique user ID, groups: [“naxslabs-admins”]
↓
kubectl sends request to API server with token
↓
API server validates token → reads groups claim → matches ClusterRoleBinding → allowed or denied
Adding OIDC doesn’t replace certificate-based auth. Your existing kubernetes-admin cert keeps working alongside it. Keep it as a fallback — if Okta goes down or something breaks in the OIDC config, you can still get into your cluster with the admin kubeconfig.
Okta App Setup
Create a new app in Okta for the cluster: Applications → Create App Integration, sign-in method OIDC, application type Native Application.
kubelogin is a CLI tool running locally. Native Application type enables the PKCE flow — the correct auth method for CLI tools that can’t safely store a client secret. No client secret is needed or used.
Grant types: Authorization Code only. Enable Require PKCE. Set Client Authentication to None.
Sign-in redirect URI:
http://localhost:8000
This is where kubelogin spins up a local server to catch the token after Okta redirects back. It only ever listens on loopback — the token never travels over an unencrypted network connection.
Under Assignments, restrict access to your admin group. Don’t leave this open to everyone in the org.
Groups Claim
Okta doesn’t include group membership in tokens by default. Go to Security → API → Authorization Servers → default → Claims → Add Claim:
| Field | Value |
|---|---|
| Name | groups |
| Include in token type | ID Token (Always) |
| Value type | Groups |
| Filter | Matches regex .* |
| Include in | Any scope |
Authorization Server Access Policy
The groups claim and app assignment aren’t enough on their own. The Okta authorization server has its own access policy that must also allow the request. Without this you’ll get a no_matching_policy error in the Okta system log even when everything else looks correct.
Go to Security → API → Authorization Servers → default → Access Policies → Add Policy. Assign it to your K8S app and add a rule allowing your group with the Authorization Code grant type.
Installing kubelogin
# Via krew
kubectl krew install oidc-login
# Or direct binary
curl -LO https://github.com/int128/kubelogin/releases/latest/download/kubelogin_linux_amd64.zip
unzip kubelogin_linux_amd64.zip
mv kubelogin /usr/local/bin/kubectl-oidc_login
Before changing anything in the cluster, verify the Okta flow works and inspect the token:
If you have a custom domain in Okta, use it — e.g. https://auth.yourdomain.com/oauth2/default. If not, use your Okta integrator address directly. The custom domain must also be set as the issuer in your authorization server settings (Security → API → Authorization Servers → default), otherwise the iss claim in the token won’t match and Kubernetes will reject it.
kubectl oidc-login setup \
--oidc-issuer-url=https://your-okta-domain/oauth2/default \
--oidc-client-id=your-client-id \
--listen-address=localhost:8000
This opens a browser, completes the login, and prints the decoded token. Confirm the groups claim is present and contains the right values before touching the API server.
Configuring the API Server
Edit the static pod manifest on each control plane node — kube-apiserver is a static pod managed locally, not a cluster-wide resource, so this can’t be done with kubectl.
sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml
Add these four flags to the command: section:
- --oidc-issuer-url=https://auth.yourdomain.com/oauth2/default
- --oidc-client-id=your-client-id
- --oidc-username-claim=sub
- --oidc-groups-claim=groups
The API server restarts automatically when the manifest is saved. Give it a minute, then verify:
ps aux | grep kube-apiserver | grep oidc
Okta’s ID token doesn’t include email by default — you’d need to add it as a separate claim. Using sub (the unique Okta user ID) is simpler and works fine because Kubernetes permissions are group-based, not username-based. If you want human-readable values in your audit logs, add an email or preferred_username claim to the authorization server and update the flag — readable identities in audit logs are worth the extra step for any environment under compliance scrutiny.
Configuring kubeconfig
kubectl config set-credentials oidc \
--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://auth.yourdomain.com/oauth2/default" \
--exec-arg="--oidc-client-id=your-client-id" \
--exec-arg="--listen-address=localhost:8000" \
--exec-interactive-mode=IfAvailable
kubectl config set-context oidc \
--cluster=kubernetes \
--user=oidc \
--namespace=default
kubectl config use-context oidc
After switching context, the next kubectl command triggers the browser login. The token is cached so subsequent commands don’t re-prompt until it expires — controlled by Okta’s token lifetime, default one hour.
RBAC — Nothing Changes
The ClusterRoles and ClusterRoleBindings from the previous post work unchanged. The only difference is group membership now comes from Okta instead of the cert O field:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: your-group-binding
subjects:
- kind: Group
name: naxslabs-admins
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: admin-role
apiGroup: rbac.authorization.k8s.io
This works because Okta puts naxslabs-admins in the token’s groups claim, the API server reads it, and matches it against the binding. No changes to RBAC needed when switching from cert auth to OIDC.
Onboarding New Users
New users get a pre-configured kubeconfig — nothing to set up manually. They run kubectl get pods, a browser opens, they log in with their Okta credentials, and they’re in. The kubeconfig contains the issuer URL and client ID, but neither is sensitive — there’s no client secret in this setup.
Store the kubeconfig in Vault so users can pull it themselves:
# Admin stores it
vault kv put secret/kubernetes/kubeconfig config=@~/.kube/config
# User pulls it
vault kv get -field=config secret/kubernetes/kubeconfig > ~/.kube/config
kubectl config use-context oidc
kubectl get pods # browser login triggers, done
Authentication and Session Policies
Getting OIDC working is only part of it. Okta’s authentication policies still govern how users prove their identity before a token is issued — and they apply to your Kubernetes app like any other. This is where the compliance value of OIDC becomes concrete: you can enforce the same authentication standards on cluster access that you enforce everywhere else.
It’s easy to get OIDC working with a permissive policy and forget to tighten it up. The authentication policy attached to your Kubernetes app determines what factors are required, how often users must re-authenticate, and whether access can be stepped up. A working OIDC integration with no authentication policy requirements is not meaningfully more secure than cert-based auth.
- MFA requirements — password alone or a second factor (Okta Verify, WebAuthn, TOTP). Requiring MFA for cluster access satisfies IA-2(1) and is the single most impactful authentication control you can enforce here
- Re-authentication frequency — cluster access warrants a tighter session window than a general SaaS app. AC-12 session termination requirements apply, and a one-hour token lifetime is a reasonable starting point
- Network zone restrictions — limit cluster authentication to known IP ranges or VPN if the cluster isn’t public-facing
- Device trust — if you have Okta Verify configured, you can require a managed device before issuing tokens
App assignment — is this user or group assigned to the app?
Authentication policy — does the user meet the factor requirements?
Authorization server access policy — does the authorization server have a policy covering this app and grant type?
All three must pass. Failing any one results in an access denied error even if the other two are correct.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
no_matching_policy in Okta logs | Authorization server access policy missing for this app | Security → API → Authorization Servers → default → Access Policies |
| Claim not present in API server logs | Token missing the claim in --oidc-username-claim | Use sub — always present in Okta tokens |
| Unauthorized after successful Okta login | API server rejecting token | kubectl logs -n kube-system kube-apiserver-km01 | grep -i oidc |
| Issuer URL mismatch | iss claim doesn’t match --oidc-issuer-url exactly | Check Authorization Server issuer URL in Okta matches the flag |
| Browser opens but token not cached | Old cache from failed attempts | rm -rf ~/.kube/cache/oidc-login |
Control Mapping
Moving Kubernetes authentication to Okta OIDC connects cluster access to your existing identity governance program. The controls it satisfies go beyond what certificate-based auth can provide.
AC-2 (Account Management) requires managing accounts through their full lifecycle including termination. With cert-based auth, revoking access means waiting for a cert to expire — Kubernetes has no native revocation mechanism. With OIDC, disabling a user in Okta immediately invalidates their next token request. The Okta account is the access record, and offboarding in Okta offboards from the cluster simultaneously.
AC-12 (Session Termination) requires that sessions are terminated after a defined period of inactivity or time. The Okta token lifetime is the session control — default one hour, configurable per app. Setting a tighter window for cluster access than for general SaaS applications is a reasonable and defensible AC-12 implementation decision.
AC-17 (Remote Access) requires that remote access sessions are authorized, monitored, and controlled. Network zone restrictions in the Okta authentication policy — limiting cluster authentication to known IPs or VPN — is AC-17 enforcement at the identity layer before the request ever reaches the API server.
IA-2 (Identification and Authentication — Organizational Users) requires unique identification and authentication, with IA-2(1) specifically requiring MFA for privileged access. Configuring the Okta authentication policy to require a second factor for cluster access satisfies IA-2(1) for what is effectively privileged infrastructure access. Certificate-based auth has no equivalent enforcement mechanism.
AU-2 and AU-12 (Audit Events and Audit Record Generation) require that authentication events are logged with sufficient detail. The API server audit log now contains Okta user identities from the sub or email claim rather than opaque certificate CNs. With a human-readable identity in the audit log, access review and incident investigation become significantly more useful — you can trace a cluster action back to a specific person rather than a certificate serial number.
Key Points
- OIDC is additive — cert-based admin access keeps working alongside it as a fallback
- The API server validates tokens; kubelogin fetches them — two separate concerns, debug them separately
- Use
subfor the username claim, but consider addingemailorpreferred_usernameif your environment is under compliance scrutiny — readable identities in audit logs matter - Three Okta policy layers must all be correct: app assignment, authentication policy, and authorization server access policy
- Disable a user in Okta and they immediately lose cluster access — no waiting for cert expiry, no server-side cleanup
- Configure MFA in the Okta authentication policy for the cluster app — this is IA-2(1) and is the most impactful single control this integration enables
- End users never touch the OIDC config — they get a pre-built kubeconfig from Vault and just log in
