Skip to content
Built, tested on kind, in every signed release

Your cluster already keeps things true. Let it keep your OAuth clients too.

How Kubernetes operators work, how they differ from Ansible and Terraform, and why Nauthera registers OAuth clients with one, for teams that already ship with Helm or GitOps.

How Nauthera fits in a clusterManifests in Git are synced into the cluster. In the app's namespace sit a Deployment, an OIDCClient and a Secret. The Nauthera operator watches the OIDCClient, registers it with nauthera-server over mutual TLS, and writes the generated client secret into the Secret, which the Deployment mounts.git · storefront/clusternamespace: storefrontDeploymentOIDCClientSecretclient-secretstatusReady · Registerednauthera-operatorwatch · compare · act · reportnauthera-server · control plane :9091syncmTLSdelivermount
The app, its client and its secret live in one namespace. The operator does the talking.

The idea

An operator is a robot that reads your manifests.

Kubernetes runs on control loops: you declare what you want, controllers keep making it so. A Deployment controller keeps three replicas running however often a node dies.

An operator is the same idea for something Kubernetes doesn't know about. It adds a new kind of resource (a CRD, here OIDCClient) and a controller that keeps the real thing, a client in your identity provider, matching it.

Declarative

You say what should exist, not the steps to create it.

Continuous

It keeps checking; it doesn't wait to be run.

Native

RBAC, namespaces, kubectl, events and status all just work.

Self-describing

Every resource carries its own state for anyone to read.

How it works

Watch, compare, act, report. Again and again.

Pick a stage to see what the Nauthera operator does with one OIDCClient.

01 / 04

Watch

The operator watches OIDCClient resources through the Kubernetes API. A new manifest, an edit or a deletion wakes it up; so does a regular resync, so nothing depends on catching every event.

$ kubectl apply -f oidcclient.yaml
oidcclient.nauthera.io/example-webapp created

Three ways to automate

Ansible pushes. Terraform plans. An operator keeps watch.

All three are declarative-ish and idempotent. The difference is when they act and where they keep what they know.

Push, when you run it

Ansible

A playbook connects to targets and runs tasks in order. Nothing happens between runs.

  • No agent: SSH or an API is enough
  • Great for machines, OS settings and one-off procedures
  • Readable, ordered playbooks
  • Only acts when someone runs it
  • No record of what exists beyond the last run's output
  • Secrets need a vault and extra tasks

Plan, then apply

Terraform

It compares configuration with a state file and the provider's API, shows a plan, and changes things when you apply it.

  • A preview (plan) of every change before it happens
  • One tool across clouds and SaaS APIs
  • Ideal for the infrastructure under the cluster
  • State file to store, lock and protect, generated secrets included
  • Drift is found only when someone plans
  • App teams usually wait for a central apply

A loop that never stops

Kubernetes operator

A controller in the cluster watches resources and keeps the outside world in line with them, continuously.

  • Continuous: drift and outages are handled without a run
  • Lives where the app lives, in the same manifests
  • Kubernetes RBAC and namespaces decide who can change what
  • Needs Kubernetes; nothing outside a cluster
  • CRDs are cluster-wide: whoever installs or upgrades them needs those rights
  • One more controller to run, monitor and upgrade

What happens when…

Pick a situation. Ansible and Terraform in general; the operator as Nauthera's does it.

Ansible

At the next run

Nothing notices until the playbook runs again. If a task manages that field, the run sets it back and reports the task as changed; --check --diff shows the drift without touching it.

Terraform

At the next plan

The change shows up as drift in the next plan; apply writes the configuration back over it.

Nauthera operator

At the next reconcile

The operator sees a revision it didn't write and reports the resource as Drifted. It writes nothing over the change until the manifest changes or you annotate it nauthera.io/restore: a person decides.

They work together. Terraform is a fine way to build the cluster and its cloud around it; Ansible, to look after machines. The operator takes over inside the cluster, where your apps and their manifests already are.

Fits how you ship

Already on Helm or GitOps? You're done wiring.

An OIDCClient is just another manifest, so whatever delivers your Deployment delivers its client. Pick your tool.

  1. 1Add an OIDCClient template to your app's own chart, next to its Deployment.
  2. 2helm upgrade renders both; Kubernetes stores the OIDCClient.
  3. 3The operator registers it and writes the Secret, named after the resource.
  4. 4The pod reads client-secret from that Secret; until it exists, Kubernetes holds the pod back.
storefront chart
# templates/oidcclient.yaml, in your app's chart
apiVersion: nauthera.io/v1alpha1
kind: OIDCClient
metadata:
  name: {{ include "storefront.fullname" . }}
spec:
  clientId: {{ .Values.oidc.clientId }}
  redirectUris:
    - https://{{ .Values.ingress.host }}/callback
  grantTypes: [authorization_code, refresh_token]
  tokenEndpointAuthMethod: client_secret_basic
  pkcePolicy: required_s256
  allowRefresh: true
  secret:
    generate: true
---
# templates/deployment.yaml (excerpt)
env:
  - name: OIDC_CLIENT_SECRET
    valueFrom:
      secretKeyRef:
        name: {{ include "storefront.fullname" . }}
        key: client-secret

One chart for the server and the operator

The server's Helm chart deploys the operator from the same release, with its mTLS client certificate from cert-manager, its permissions and the resource definitions. Your host, issuer, cache and Secret names go in a values file, as Installation shows.

Install the operator
helm
# my-values.yaml: your host, issuer, cache and Secret names (see Installation)
helm upgrade --install nauthera deploy/helm/nauthera-server \
  --values deploy/helm/nauthera-server/profiles/production.yaml \
  --values my-values.yaml \
  --set certManager.grpcTLS.enabled=true \
  --set operator.enabled=true

Why Nauthera uses one

OAuth clients belong to apps, so they should ship like apps.

A client is configuration with a secret attached. Kept in a console, it drifts from the code that depends on it and its secret travels by chat. Kept as a manifest, it is reviewed, versioned and delivered with the app, and the operator handles the secret.

Refused at apply, in the code today
$ grep -E 'tokenEndpointAuthMethod|pkcePolicy' storefront-spa.yaml
  tokenEndpointAuthMethod: none
  pkcePolicy: optional
$ kubectl apply -f storefront-spa.yaml
The OIDCClient "storefront-spa" is invalid: spec: Invalid value: public clients (tokenEndpointAuthMethod 'none') must set pkcePolicy to 'required_s256'
A public client without PKCE is refused before it reaches the server.
  • The client ships with the app

    An OIDCClient sits beside the Deployment it serves: same repository, same review, same release. No ticket to an identity team, no console.

  • No secret copied by hand

    The secret goes from the server straight into a Secret the workload mounts. Delete the Secret to rotate it; the operator writes a new one and records the rotation.

  • Wrong clients never get in

    Rules on the resource definition refuse contradictory clients at kubectl apply. The server checks again and reports a refusal on the resource, field by field.

  • Scoped by namespace and grant

    The operator only sends manifests from the namespaces it covers, and the server only accepts the operations and organizations its identity is granted.

  • Drift is surfaced, not papered over

    A client changed elsewhere is reported as Drifted and left alone until someone chooses to restore it, so a manual hotfix is never silently undone.

  • Outages don't cause damage

    If the server stops answering, nothing is removed and nothing is retried in a storm. Resources wait, then catch up on their own. CI runs a client's whole life on kind, an outage included.

oidcclient.yaml
apiVersion: nauthera.io/v1alpha1
kind: OIDCClient
metadata:
  name: example-webapp
  namespace: storefront
spec:
  clientId: example-webapp
  redirectUris:
    - https://app.example.com/callback
  grantTypes:
    - authorization_code
    - refresh_token
  tokenEndpointAuthMethod: client_secret_basic
  pkcePolicy: required_s256
  allowRefresh: true
  secret:
    generate: true
The whole client, as the operator reconciles it.

Is it for you?

A great fit if you already run Kubernetes.

  • Your apps ship as manifests, with Helm, Argo CD, Flux or kubectl
  • App teams own namespaces and should register their own clients
  • You want secrets delivered, never pasted
  • Platform or security teams want to bound what may be registered

Why not let apps register themselves?

Dynamic client registration (RFC 7591) is built for software meeting a server it has no prior relationship with. For your own apps it leaves the secret, the replicas and the clean-up to each app, and their metadata is only self-asserted. Operator or dynamic registration?

Not on Kubernetes?

Register clients from your own automation through the gRPC admin service, as nauthera-cli does, or by dynamic client registration, and read them through the admin REST API.

Hear when the operator ships

One email when the operator ships. No newsletter.

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