The problem
We built Nauthera because of how we used to run sign-in ourselves. Our clusters were already GitOps: every Deployment, chart and network policy came from a commit. Our identity provider was the exception. Each OIDC client for Argo CD, Grafana or an internal tool was created by hand in Keycloak's admin console.
That kept the one thing every app depends on outside the review, history and rollback that everything else had. A new environment meant clicking the same clients again, and nothing in Git said which clients should exist.
What we changed
Every environment, dev, staging and prod, now runs Nauthera as its identity provider, installed from its Helm chart with the operator beside it. The rule that holds for the rest of the cluster now holds for sign-in too: the only way to change it is a commit.
- Clients are
OIDCClientmanifests in the namespace of the app that uses them. Argo CD applies them like any other resource. - The operator registers each one with the server over mutual TLS and, for
a confidential client, asks the server to mint the secret and writes it into a
Kubernetes
Secretthe app reads. No one sees or copies it. - Organizations for our early adopters are
Organizationmanifests, each with its own issuer, its own Argo CD project and, where they want one, aBrandingand anIdentityProviderfor sign-in with Google. - Removing a manifest unregisters the client (
deletionPolicy: Delete), so deleting an app no longer leaves a forgotten client behind.
Adding an app's sign-in
This is the client Argo CD signs in with, as it sits in one of our environment repositories. It is a public client, so there is no secret at all: authorization code with PKCE, and a loopback redirect for the CLI.
# platform/argocd/sso/oidcclient.yaml
apiVersion: nauthera.io/v1alpha1
kind: OIDCClient
metadata:
name: argocd
namespace: argocd
spec:
clientId: argocd
displayName: Argo CD
redirectUris:
- https://argocd.nauthera.io/auth/callback
- http://localhost:8085/auth/callback # argocd login --sso
grantTypes: [authorization_code]
responseTypes: [code]
scopes: [openid, profile, email]
tokenEndpointAuthMethod: none
pkcePolicy: required_s256
idTokenTtlSeconds: 3600
accessTokenTtlSeconds: 600
deletionPolicy: Delete # removing this file unregisters the clientGrafana's client is confidential and renews its tokens, so beside its name and redirect URI a few lines differ, and the operator does the rest:
grantTypes: [authorization_code, refresh_token]
scopes: [openid, profile, email, offline_access]
tokenEndpointAuthMethod: client_secret_basic
allowRefresh: true
accessTokenTtlSeconds: 300
secret:
generate: true
secretName: grafana-oidc-nauthera # Grafana reads it from its environmentGrafana runs one sign-in client per organization, so each user lands in the
Grafana organization that matches the Nauthera organization they signed in with.
Each of those clients is the same manifest naming a different organization in
spec.organization.
What it changed
- Sign-in is reviewed like code. A new client, a changed redirect URI or a shorter token lifetime is a pull request with a diff, and the history says who changed what and when.
- Every environment is reproducible. Dev, staging and prod declare their clients in their own repositories. Rebuilding a cluster brings its clients back with it, with no list to click through again.
- Secrets stay in the cluster. For apps in the cluster, client secrets are minted by the server and delivered by the operator into the app's namespace; they never pass through Git or a person. The repository holds no client secrets, only sealed credentials for things like DNS and mail.
- One place to see who signed in. Argo CD and Grafana sign-ins all land in Nauthera's audit log. The admin dashboard's Threats page reads it as attack signals and shows the period's refused sign-ins, and the same signals reach Prometheus as metrics with alert rules.
- Sign-out reaches every app. Grafana takes no back-channel logout, so its clients use five-minute access tokens with refresh: when a Nauthera session ends, its refresh tokens are revoked, Grafana's next renewal is refused, and its session ends within five minutes.
What's still rough
Running it ourselves is how we find the gaps first. These are the ones we work around today:
- Apps outside the cluster still get their secret by hand. The operator writes secrets into the cluster. An app hosted elsewhere, such as a site on Vercel, has its secret copied into that platform's environment.
- Organization clients wait for their organization. Until an
Organizationhas an issuer, the operator holds its clientsPendingwith the reasonOrganizationNotReady. We put such clients a sync wave behind theirOrganization. - It runs on pre-1.0 releases. Our environments pin the chart, public on GHCR, to one version and its images by digest. The roadmap tracks what stands between these releases and 1.0.
Run it the same way
The manifests above are the whole integration. The operator docs cover installing it, the OIDCClient reference lists every field, and Operator or dynamic registration? explains when to use which.