"Kubernetes OIDC" names two different jobs. One is application sign-in: the
applications on your cluster send their users to an OpenID Connect issuer and get back
tokens. The other is API-server authentication: a person runs kubectl, and the
Kubernetes API server checks an ID token before RBAC decides what they may do. Nauthera is
built for the first. This page shows how its pieces fit, then how the second job differs
and what Nauthera does and does not provide for it.
Application sign-in: the pieces
- Your app's repository or Helm chart holds an
OIDCClientnext to its Deployment. Argo CD, Flux, Helm orkubectl applyputs it in the app's namespace. - The Nauthera operator watches
OIDCClientresources. - It registers the client with nauthera-server over the gRPC
AdminService, with mutual TLS, and writes the server's answer to the resource's status. - It writes the generated client secret, with the client ID and the issuer, into a Secret in the same namespace.
- The Deployment reads those values from the Secret.
- A user opens the app.
- The app sends the browser to the issuer's
/authorizeendpoint, where the user signs in on Nauthera's hosted pages. - The app exchanges the returned code for tokens at
/tokenand verifies the ID token against the issuer's published keys.
The issuer
nauthera-server is the issuer, an HTTPS URL with no path such as
https://auth.example.com. Each organization is an issuer of its own, with its own
discovery document, signing keys, users and audit log: the main organization at the
issuer URL, any other at https:. Each organization's
signing keys are encrypted and bound to it, so a key copied into another tenant's row does
not decrypt. An app only needs the
issuer; everything else is in the discovery document at
/. The server keeps its state in PostgreSQL, and in
Redis for any issuer whose host is not loopback. See
Organizations for every URL.
The client, as a manifest
apiVersion: nauthera.io/v1alpha1
kind: OIDCClient
metadata:
name: storefront
namespace: storefront
spec:
clientId: storefront
displayName: Storefront
redirectUris:
- https://shop.example.com/callback
postLogoutRedirectUris:
- https://shop.example.com/
grantTypes: [authorization_code, refresh_token]
responseTypes: [code]
scopes: [openid, profile, email, offline_access]
tokenEndpointAuthMethod: client_secret_basic
pkcePolicy: required_s256
allowRefresh: true
deletionPolicy: Retain
secret:
generate: true- Which organization it belongs to follows from its namespace. An organization lists
the namespaces it admits in its
Organizationresource'sspec.namespaces, and the main organization admits those in the chart valueoperator., which is empty by default. A client in a namespace no organization admits is refused withmainOrganizationNamespaces NamespaceNotAdmitted. See Which organization a resource configures. - Contradictions are refused at
kubectl apply, before anything reaches the server: a public client withoutrequired_s256, a refresh grant withoutallowRefresh, a generated secret on a public client. - The status says what happened:
Readyonce the server holds the client,Refusedwith the server's reason,Driftedwhen someone changed the client outside the operator. See the OIDCClient reference.
The generated Secret
With secret.generate: true, the server mints the secret, keeps only its digest and
returns the plaintext once. The operator writes it into a Secret named after the resource
(or spec.secret.secretName) with three keys:
| Key | Holds |
|---|---|
client-id | The client ID, storefront |
client-secret | The generated secret |
issuer | The organization's issuer URL |
The Secret is owned by the OIDCClient, and the operator never writes over a Secret it
does not own. To rotate the secret, delete the Secret: the operator mints a new one and
writes it again. A workload reads it as it reads any Secret:
# The storefront Deployment's container (excerpt)
env:
- name: OIDC_ISSUER
valueFrom:
secretKeyRef: { name: storefront, key: issuer }
- name: OIDC_CLIENT_ID
valueFrom:
secretKeyRef: { name: storefront, key: client-id }
- name: OIDC_CLIENT_SECRET
valueFrom:
secretKeyRef: { name: storefront, key: client-secret }Environment variables are read when the pod starts, so restart the Deployment after a rotation, or mount the Secret as files your app re-reads.
The application
Any OpenID Connect library works: point it at the issuer, use the authorization code flow with PKCE (S256), register the exact redirect URIs it sends, and verify ID tokens against the issuer's key set. Nauthera matches redirect URIs exactly, with no wildcards.
The ID token carries sub, and with the email and profile scopes email,
email_verified, name and preferred_username. There is no groups claim and no
custom group model, so an app that maps roles does it by email, by sub, or, for an
organization brokered by another, by member_org_slug.
Connect an Application walks through the protocol step by step,
and the framework guides cover Better Auth, Spring Boot, Go and .NET.
kubectl and the API server: a different job
When a person runs kubectl against a cluster that uses OpenID Connect, the API server
trusts an external issuer, either through its --oidc-* flags or through a structured
AuthenticationConfiguration file given with --authentication-config. The Kubernetes
docs say the file form is stable since v1.34 and was first available in v1.29, that it
takes several JWT authenticators with CEL claim validation and mappings, and that an API
server given both the file and any --oidc-* flag exits with an error
(Kubernetes v1.37 authentication docs,
read on November 4, 2026). On the laptop, a credential plugin such as
kubelogin (kubectl oidc-login, latest release
v1.36.4 on November 4, 2026) runs the browser or device flow and hands kubectl the ID
token. The API server maps the
token's claims to a username and groups, and RBAC binds those.
| What API-server OIDC needs | Nauthera today |
|---|---|
| An HTTPS issuer with discovery and keys the API server can reach | Each organization is one, though not tested with an API server. |
| A username claim | sub, and email with email_verified when the email scope is granted. |
| A groups claim, for RBAC bindings to groups | None. RBAC could bind single users only. |
| A public client for kubelogin, with PKCE and a loopback redirect | OIDCClient supports public clients, which must use required_s256, and http://localhost redirects, matched exactly. kubelogin's defaults are http://localhost:8000 and http://localhost:18000 (usage). |
| The token's signing algorithm | The --oidc-signing-algs flag defaults to RS256 and accepts ES256 among others (kube-apiserver reference, v1.37). Nauthera signs ID tokens with ES256 when it holds an EC key, as it does when the chart's cert-manager setup issues its keys, unless keys.default_alg or the client's idTokenSignedResponseAlg asks for RS256. |
| Documentation and tests | None. Nauthera's docs and tests cover application sign-in only. |
So Nauthera does not provide kubectl login today, and this page does not claim it
works. If you need it now, use a provider documented for it: Dex, for one, says in its
README (v2.46.1) that it "can
drive API server authentication through the OpenID Connect plugin", or the identity
integration of your managed Kubernetes service. Workload identity is a third, separate
mechanism: there the cluster is the issuer of ServiceAccount tokens, and Nauthera plays no
part in it.
Where it stands
Nauthera is pre-1.0, in a design-partner program: its signed server and operator images and its Helm chart are public on the GitHub Container Registry, and teams can apply for one of five design-partner seats to pilot it with direct support. The source is private. The operator reconciles OIDCClient resources in
the code today; the roadmap shows what is next.