Generated credentials¶
When authentik creates an OAuth2 provider it generates a client ID and, for confidential clients, a client secret. Those values exist only inside authentik until something copies them out — and copying them out safely is the whole job of this page.
Planned design — nothing here exists yet
OAuth2Provider has no Go type, no CRD and no controller. This page
describes the intended design so it can be reviewed before it is built.
The problem¶
A workload needs the client secret. The obvious ways to get it there are all bad:
- Copy it by hand. Works once, then rots. Nobody remembers where it came from when it needs rotating.
- Put it in
status. Convenient and catastrophic —statusis readable by anything withgeton the resource, andget providersis not usually treated as a credential-bearing permission. - Log it. Operator logs are shipped to a log aggregator, indexed, and retained for a year, usually with much broader read access than the cluster.
So the operator writes them to a Secret, and to nowhere else.
How it works¶
apiVersion: authentik.k8s.rka.sh/v1alpha1
kind: OAuth2Provider
metadata:
name: grafana
namespace: my-apps
spec:
connectionRef:
name: default
clientType: confidential
credentialsSecretRef:
name: grafana-oidc
On a successful reconcile the operator creates or updates grafana-oidc in
my-apps:
| Key | Present when | Contents |
|---|---|---|
clientID |
Always | The OAuth2 client ID authentik generated. |
clientSecret |
clientType: confidential |
The OAuth2 client secret. Absent entirely for public clients. |
$ kubectl -n my-apps get secret grafana-oidc -o jsonpath='{.data}' | jq 'keys'
[
"clientID",
"clientSecret"
]
Rules the operator follows¶
Same namespace, always
The Secret is created in the provider's own namespace.
credentialsSecretRef has no namespace field.
A cross-namespace write would let anyone who can create a provider in their
own namespace plant a Secret in someone else's — a way to overwrite a
Secret a workload elsewhere depends on. Same reasoning as
connections.
Owned by the provider
The Secret carries an owner reference to the OAuth2Provider, so deleting
the provider garbage-collects it. No orphaned credentials accumulate.
Never in status, events, or logs
The operator records the Secret's name in status and in events, and
never its contents. Condition messages carrying an authentik API error are
scrubbed before being written, because authentik error bodies occasionally
echo a submitted value back.
Only the keys it manages
Updating the Secret replaces clientID and clientSecret and leaves any
other key untouched. You can keep extra keys — an issuer URL, a
pre-rendered config file — in the same Secret, and the operator will not
remove them.
Pre-existing Secrets¶
If a Secret with that name already exists and the operator does not own it,
the reconcile fails with AdoptionConflict rather than overwriting it. This is
the same principle as adoption of authentik objects,
applied to Kubernetes: silently taking over a Secret somebody else manages is
how an operator breaks an unrelated workload.
Consuming the credentials¶
env:
- name: GF_AUTH_GENERIC_OAUTH_CLIENT_ID
valueFrom:
secretKeyRef:
name: grafana-oidc
key: clientID
- name: GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: grafana-oidc
key: clientSecret
Simple, and read once at Pod start. A rotated secret is not picked up until the Pod restarts.
volumes:
- name: oidc
secret:
secretName: grafana-oidc
volumeMounts:
- name: oidc
mountPath: /etc/oidc
readOnly: true
The kubelet refreshes mounted Secret contents (typically within a minute).
If your application re-reads the file, rotation needs no restart. Most do
not — check before relying on it.
envFrom puts both keys in the environment
envFrom.secretRef injects every key, including clientSecret, under its
own name. Anything that dumps the environment on a crash — many language
runtimes do — writes the client secret into a stack trace.
Rotation¶
Rotating the client secret is a two-sided operation: authentik must generate a new one, and every consumer must pick it up.
Rotation causes downtime unless you sequence it
The old secret stops working the moment the new one is generated. Any Pod still holding the old value fails authentication until it restarts.
The procedure:
- Trigger regeneration. In authentik, edit the provider and generate a new client secret.
-
Wait for the operator to reconcile — up to the connection's
probeInterval(default5m) — or force it: -
Confirm the
Secretchanged: -
Restart consumers:
Automate step 4
A Reloader-style controller that watches the Secret and restarts
Deployments referencing it removes the manual step, which is the one people
forget. Without it, rotation looks successful and logins start failing at
the next unrelated restart — hours or days later, with no obvious cause.
What the operator does not do¶
- It does not rotate on a schedule. There is no
rotationInterval. Rotation is initiated in authentik or by a human. - It does not restart your workloads. Owner references do not exist between
a
Secretand its unrelated consumers. - It does not keep the previous secret. There is no overlap window; the
Secretholds one value.
Deletion¶
deletionPolicy on the provider governs the authentik side. The Kubernetes
Secret is always garbage-collected with the provider, because it is owned by
it.
| Provider deleted with | authentik provider | Generated Secret |
|---|---|---|
deletionPolicy: Delete (default) |
Deleted | Deleted |
deletionPolicy: Orphan |
Kept | Deleted |
Orphan keeps the provider but loses your copy of the secret
The authentik provider survives with its client secret intact, but the
Kubernetes Secret is garbage-collected. If you still need those values,
copy them out before deleting the resource:
Otherwise you will be regenerating the secret in authentik and reconfiguring the consumer by hand.
Hardening¶
- Encrypt
Secrets at rest. These are live credentials for an identity provider. Encryption at rest is the baseline. - Scope
get secrets. In the namespace holding generated credentials, limit it to the workloads that need them.get secretson a namespace is read access to every client secret in it. - Watch what leaves the cluster. GitOps tooling, backup jobs and log
shippers that sync or index
Secrets extend the blast radius wherever they send them. - Prefer public clients where the architecture allows. A browser SPA using
PKCE has no client secret to leak.
clientType: publicis a smaller attack surface thanconfidentialplus careful secret handling.
See Security for the full model.
See also¶
- Providers — the full
OAuth2Providerspec. - Your first application — end-to-end example.
- Security — blast radius of each credential.