Kubernetes Authentication with Okta OIDC

← All guides

Kubernetes Authentication with Okta OIDC

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 AuthOIDC / Okta
User identityCN field in certToken claims from Okta
Group membershipO field in certGroups claim in token
User managementManual cert generationManaged in Okta
Revoking accessCert expiry or CRLDisable in Okta — immediate
MFA enforcementNot possibleOkta authentication policy
RBACClusterRole/BindingSame — 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.

User runs kubectl
  ↓
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
OIDC is additive

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.

Why Native, not Web?

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:

FieldValue
Namegroups
Include in token typeID Token (Always)
Value typeGroups
FilterMatches regex .*
Include inAny 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:

Issuer URL

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
Why sub and not email

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.

Don’t skip this

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
Three policy layers — all must pass

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

ErrorCauseFix
no_matching_policy in Okta logsAuthorization server access policy missing for this appSecurity → API → Authorization Servers → default → Access Policies
Claim not present in API server logsToken missing the claim in --oidc-username-claimUse sub — always present in Okta tokens
Unauthorized after successful Okta loginAPI server rejecting tokenkubectl logs -n kube-system kube-apiserver-km01 | grep -i oidc
Issuer URL mismatchiss claim doesn’t match --oidc-issuer-url exactlyCheck Authorization Server issuer URL in Okta matches the flag
Browser opens but token not cachedOld cache from failed attemptsrm -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.

NIST AC-2 NIST AC-12 NIST AC-17 NIST IA-2 NIST AU-2 NIST AU-12

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 sub for the username claim, but consider adding email or preferred_username if 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
Darnell Keith

Not sure where you stand on this?

A Gap Assessment or IAM Governance Assessment tells you exactly where the gaps are — and what to do about them, in order of what matters most.

Get in Touch
NAXS Labs
Logo