Skip to content
Solutions

Pre-1.0 · Design-partner program

OIDC on Kubernetes, from the issuer to the app.

How an OIDC issuer, the Nauthera operator, OIDCClient resources and their Secrets fit together on Kubernetes, and how API-server login with kubectl differs.

By Michael Beutler, FounderPublished

"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

How Nauthera signs users in to an application on KubernetesA manifest from Git or a Helm chart creates an OIDCClient in the application's namespace. The Nauthera operator watches it, registers the client with nauthera-server over mutual TLS, and writes the client ID, client secret and issuer into a Secret, which the application's Deployment reads. The browser opens the application, which sends it to the issuer's authorization endpoint to sign in, then exchanges the code for tokens at the token endpoint. The server keeps its state in PostgreSQL and Redis.Git or Helmoidcclient.yamlBrowserthe userclusternamespace: storefrontOIDCClient storefrontstatus: ReadySecret storefrontclient-id · client-secret · issuerDeployment storefrontreads the Secretnamespace: nautheranauthera-operatorwatch · register · writenauthera-serverissuer https://auth.example.com/.well-known/openid-configuration/authorize · /token · /userinfo/.well-known/jwks.jsonPostgreSQLRedis12345678
One application and its client. The numbers follow the steps below.
  1. Your app's repository or Helm chart holds an OIDCClient next to its Deployment. Argo CD, Flux, Helm or kubectl apply puts it in the app's namespace.
  2. The Nauthera operator watches OIDCClient resources.
  3. 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.
  4. It writes the generated client secret, with the client ID and the issuer, into a Secret in the same namespace.
  5. The Deployment reads those values from the Secret.
  6. A user opens the app.
  7. The app sends the browser to the issuer's /authorize endpoint, where the user signs in on Nauthera's hosted pages.
  8. The app exchanges the returned code for tokens at /token and 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://auth.example.com/orgs/<slug>. 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 /.well-known/openid-configuration. 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 Organization resource's spec.namespaces, and the main organization admits those in the chart value operator.mainOrganizationNamespaces, which is empty by default. A client in a namespace no organization admits is refused with NamespaceNotAdmitted. See Which organization a resource configures.
  • Contradictions are refused at kubectl apply, before anything reaches the server: a public client without required_s256, a refresh grant without allowRefresh, a generated secret on a public client.
  • The status says what happened: Ready once the server holds the client, Refused with the server's reason, Drifted when 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:

KeyHolds
client-idThe client ID, storefront
client-secretThe generated secret
issuerThe 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 needsNauthera today
An HTTPS issuer with discovery and keys the API server can reachEach organization is one, though not tested with an API server.
A username claimsub, and email with email_verified when the email scope is granted.
A groups claim, for RBAC bindings to groupsNone. RBAC could bind single users only.
A public client for kubelogin, with PKCE and a loopback redirectOIDCClient 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 algorithmThe --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 testsNone. 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.

Release updates

Hear when the operator ships

One email when the operator ships. No newsletter.

By joining, you agree to our Privacy Policy and Terms.

Design partners

Apply for a design-partner seat

Five teams get calls with the founder, a say in the operator's design, and direct support when they pilot it.

Optional

We store your answers with your email address to select design partners. Privacy Policy