Skip to content
Compare

Pre-1.0 · Design-partner program

One OAuth client, in Terraform and as a Kubernetes resource.

One OAuth client through the Keycloak Terraform provider and through a Nauthera OIDCClient: definition, secret delivery, drift, deletion and review.

By Michael Beutler, FounderPublished Updated

Many teams that run Keycloak manage its clients with Terraform. This page follows one client, a web shop that signs its users in with the authorization code flow and a client secret, through that workflow and through a Nauthera OIDCClient: how it is defined, how its secret reaches the application, what happens when someone changes it by hand, how it is deleted and how a change is reviewed. 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.

Checked against: the keycloak/keycloak Terraform provider 5.10.0 (published October 5, 2026), its keycloak_openid_client docs and resource code at that tag; the Terraform v1.17 docs; Crossplane v2.4 docs and provider-keycloak v3.2.3; the keycloak-config-cli README on its main branch; all read on November 4, 2026. The Nauthera manifest is checked against nauthera-server's OIDCClient definition. Neither workflow has been run end to end for this page.

At a glance

Keycloak with Terraform 5.10.0Nauthera
DefinitionA keycloak_openid_client resource in a Terraform configuration (provider docs).An OIDCClient manifest beside the application.
SecretGenerated by Keycloak and read back into state, which "Terraform stores ... in a plaintext file" (sensitive data); or supplied by you, write-only, with client_secret_wo.Generated by the server, which keeps a hash; the operator writes it into a Secret. Or supplied by you from a Secret the operator reads (spec.secret.secretRef), hashed the same way.
DriftFound when someone runs terraform plan, which first reads "the current state of any already-existing remote objects" (plan).Noticed by the operator, reported as Drifted, and left until someone restores it.
DeletionRemoving the block makes apply destroy the client (resource behavior).deletionPolicy: Retain by default, or Delete.
ReviewThe plan, with the secret shown as (sensitive value) (sensitive data).The manifest's diff, and a server-side dry run against the CRD's rules.
ScopeMost of Keycloak's configuration: realms, authentication flows, protocol mappers, identity providers and clients, each a resource (resource docs).OAuth clients and the five other nauthera.io resources.

The client

The shop runs in the shop namespace at https://shop.example.com. It is a confidential client: it signs users in with the authorization code flow and PKCE, and authenticates to the token endpoint with a client secret.

1. Definition

With Terraform, the client is a resource in a configuration that holds the Keycloak provider's admin credentials:

resource "keycloak_realm" "shop" {
  realm   = "shop"
  enabled = true
}
 
resource "keycloak_openid_client" "shop" {
  realm_id                        = keycloak_realm.shop.id
  client_id                       = "shop"
  name                            = "Shop"
  access_type                     = "CONFIDENTIAL"
  standard_flow_enabled           = true
  valid_redirect_uris             = ["https://shop.example.com/callback"]
  valid_post_logout_redirect_uris = ["https://shop.example.com/"]
  pkce_code_challenge_method      = "S256"
}

With Nauthera, the client is a manifest in the application's own repository, next to its Deployment:

apiVersion: nauthera.io/v1alpha1
kind: OIDCClient
metadata:
  name: shop
  namespace: shop
spec:
  clientId: shop
  displayName: Shop
  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
  secret:
    generate: true
  deletionPolicy: Retain

It configures the organization that admits the shop namespace, so whoever may create resources there may register clients for that organization and no other (Which organization a resource configures). The Terraform configuration, by contrast, can change whatever its admin credentials can.

2. Secret delivery

Terraform, generated secret. Leave client_secret out and Keycloak generates one. The attribute is optional, computed and sensitive, and the provider sets it from Keycloak's answer on every read (resource code), so the secret lands in the state file, where "Terraform stores your state in a plaintext file, which includes any secret values" (sensitive data). Reaching the shop is then a second step: a kubernetes_secret_v1 resource, whose data "will be stored in the raw state as plain-text" too (docs, hashicorp/kubernetes 3.4.0), or a secret store.

Terraform, write-only secret. With Terraform 1.11 or later, client_secret_wo and client_secret_wo_version send a secret you supply without storing it in state or plan files. The provider's example generates it with an ephemeral resource:

ephemeral "random_password" "shop" {
  length = 32
}
 
resource "keycloak_openid_client" "shop" {
  realm_id                 = keycloak_realm.shop.id
  client_id                = "shop"
  access_type              = "CONFIDENTIAL"
  standard_flow_enabled    = true
  valid_redirect_uris      = ["https://shop.example.com/callback"]
  client_secret_wo         = ephemeral.random_password.shop.result
  client_secret_wo_version = "1"
}

The application still needs the same value, delivered by another write-only path, such as data_wo on that same kubernetes_secret_v1 resource, or a secret store. Rotating means a new value and a new client_secret_wo_version.

Crossplane. provider-keycloak v3.2.3 is generated from the same Terraform provider (README). Its client takes the secret as input from a Secret you write, through clientSecretSecretRef (client types), and Crossplane writes a resource's connection details into a Secret named by writeConnectionSecretToRef (managed resources).

Nauthera. With secret.generate: true the server mints the secret, returns it to the operator once and keeps only a hash. The operator writes it into the Secret shop/shop under client-id, client-secret and issuer, and the Deployment reads it:

env:
  - name: OIDC_CLIENT_SECRET
    valueFrom:
      secretKeyRef:
        name: shop
        key: client-secret

No one handles the value, and it is in no state file or repository. Deleting the Secret rotates it: the operator mints a new one, writes it, and records a SecretRotated event. See How secrets work.

3. Drift

Someone changes the client by hand: in Keycloak's admin console, or on Nauthera with nauthera-cli under the operator's client certificate, which writes through the same control plane as the operator.

Terraform sees it the next time someone runs terraform plan, which first reads "the current state of any already-existing remote objects", and the next apply writes the configuration back; -refresh-only accepts the change into state instead (plan). Between runs, nothing notices.

Crossplane checks on a poll interval and "overrides any changes made to an external resource outside of Crossplane" (managed resources, v2.4).

keycloak-config-cli applies a realm file through the admin API each time it runs. By default it "will delete and recreate resources that it initially created in previous runs", and for some resource types it deletes anything the file leaves out (managed resources).

Nauthera's operator notices that the server's revision of the client moved without it, marks the resource Drifted, and does not write over the change. The audit log says who made it. To keep the manifest's version, annotate the resource:

kubectl annotate oidcclient shop -n shop nauthera.io/restore=yes

The operator writes the declared state back, records a Restored event and removes the annotation. Changing the manifest has the same effect. See Changes made outside the operator.

4. Deletion

Terraform "destroys resources that exist in the state but no longer exist in the configuration" (resource behavior), so deleting the block and applying removes the client from Keycloak. A client declared with import = true is left in place (provider docs).

Nauthera follows deletionPolicy. Retain, the default, leaves the client registered and releases the Secret from the resource, so deleting a namespace cannot take a live client with it. Delete removes the client from the server, and the Secret goes with the resource; a removal the server does not take keeps the resource in Deleting until it does. See Deleting a resource.

5. Review

Terraform: a pull request shows the configuration's diff, and the plan shows what will change against Keycloak as it is now, with the secret as (sensitive value). Applying it needs the provider's Keycloak admin credentials wherever the plan runs.

Nauthera: the pull request is the manifest's diff, in the application's own repository. kubectl apply --dry-run=server runs the CRD's validation, which refuses a contradictory client, such as a public client without required_s256 or a refresh_token grant without allowRefresh. After the merge, your GitOps tool applies it, and the resource's Ready condition says whether the server took it:

kubectl wait --for=condition=Ready oidcclient/shop -n shop

Where Terraform is the better choice

  • You keep Keycloak. Today that is every production deployment. 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 provider manages realms, flows, mappers and identity providers, not only clients, and supports "the latest three minor versions of Keycloak" (provider README).
  • Your clients belong to more than Kubernetes. Terraform declares a client beside the DNS record, the database and the cloud resources it goes with, in one plan.
  • You want every change planned against live state before it is applied, by a person, rather than reconciled by a controller.
  • You already keep secrets out of state, with client_secret_wo and a write-only or external delivery. Then the secret argument for an operator is gone, and what remains is drift between runs and the second step that delivers the secret.

The operator page compares the two models in general: Why a Kubernetes operator. Moving clients and users from Keycloak is covered under Migrating from Keycloak.

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