Your first application¶
An end-to-end example: a connection, an OAuth2 provider, an application, and a workload consuming the generated credentials. We will use Grafana, because its OIDC configuration is short enough to fit on a page.
This is a design document, not a tutorial
OAuth2Provider and Application do not exist yet — no Go types, no
CRDs, no controller. The manifests below will be rejected by the API server
today.
They are published so the API shape can be reviewed before it is built. The two connection kinds do exist as Go types, but nothing reconciles them either. See Implementation status.
What we are building¶
Secret: authentik-api-token your authentik API token
│
▼
AuthentikConnection/default which authentik, and with what token
│
▼
OAuth2Provider/grafana ──────────▶ Secret: grafana-oidc
│ (clientID, clientSecret — written by
▼ the operator, consumed by Grafana)
Application/grafana what users click in the authentik library
0. Prerequisites¶
The operator installed (Installation), and a Secret holding
an authentik API token (Creating an API token):
kubectl create namespace my-apps
kubectl create secret generic authentik-api-token \
--namespace my-apps \
--from-file=token=./token.txt
1. The connection¶
apiVersion: authentik.k8s.rka.sh/v1alpha1
kind: AuthentikConnection
metadata:
name: default
namespace: my-apps
spec:
url: https://authentik.example.com
tokenSecretRef:
name: authentik-api-token
key: token
probeInterval: 5m
kubectl apply -f connection.yaml
kubectl -n my-apps wait --for=condition=Ready authentikconnection/default --timeout=60s
Do not go further until this is Ready. Everything downstream refuses to
reconcile against a connection that is not, and reports ConnectionNotReady
rather than a useful error of its own.
$ kubectl -n my-apps get akconn
NAME URL VERSION READY AGE
default https://authentik.example.com 2026.8.2 True 12s
2. The OAuth2 provider¶
apiVersion: authentik.k8s.rka.sh/v1alpha1
kind: OAuth2Provider
metadata:
name: grafana
namespace: my-apps
spec:
connectionRef:
kind: AuthentikConnection # (1)!
name: default
adoptionPolicy: FailOnConflict # (2)!
deletionPolicy: Delete
authorizationFlow: default-provider-authorization-implicit-consent # (3)!
invalidationFlow: default-provider-invalidation-flow
clientType: confidential
redirectURIs:
- matchingMode: strict
url: https://grafana.example.com/login/generic_oauth
propertyMappings: # (4)!
- goauthentik.io/providers/oauth2/scope-openid
- goauthentik.io/providers/oauth2/scope-email
- goauthentik.io/providers/oauth2/scope-profile
signingKey: authentik Self-signed Certificate # (5)!
accessTokenValidity: hours=1
refreshTokenValidity: days=30
credentialsSecretRef: # (6)!
name: grafana-oidc
- Optional —
AuthentikConnectionis the default. Set it toClusterAuthentikConnectionto use a cluster-scoped connection. - The default. The reconcile fails rather than taking over a pre-existing
authentik provider named
grafana. See ADR 0002. - Flows are referenced by slug, not by UUID. The operator resolves the slug against the connection's authentik on every reconcile.
- Property mappings are referenced by name. The operator resolves each to its UUID.
- The signing keypair is referenced by name.
- Where the operator writes the generated
clientIDandclientSecret. TheSecretis created in this resource's own namespace.
Names and slugs, not UUIDs¶
authentik's API identifies flows, property mappings and certificate keypairs by UUID. Those UUIDs are generated per-instance, so a manifest containing one is not portable between your staging and production authentik — and is unreadable in review.
So the CRDs reference these by the human identifier and the operator resolves it on each reconcile:
| Referenced by | Resolved to |
|---|---|
| Flow — by slug | Flow UUID |
| Property mapping — by name | Mapping UUID |
| Certificate keypair — by name | Keypair UUID |
The failure modes get their own condition reasons:
- No match —
ReferenceNotFound. Usually a typo, or a flow that exists in staging but was never created in production. - More than one match —
ReferenceAmbiguous. Property mapping names are not unique in authentik, so this is a real possibility. The operator refuses to guess rather than picking one and silently binding the wrong mapping.
3. The application¶
apiVersion: authentik.k8s.rka.sh/v1alpha1
kind: Application
metadata:
name: grafana
namespace: my-apps
spec:
connectionRef:
name: default
adoptionPolicy: FailOnConflict
deletionPolicy: Delete
slug: grafana # (1)!
displayName: Grafana
group: Observability # (2)!
providerRef: # (3)!
kind: OAuth2Provider
name: grafana
meta:
description: Dashboards and alerting
launchURL: https://grafana.example.com
publisher: Platform team
policyEngineMode: any
- The slug is authentik's identity for the application and appears in its URLs. Changing it later renames the object in authentik.
- Optional. Groups applications in the user-facing library page.
- A reference to a provider resource in this namespace — not to an authentik primary key.
Ordering does not matter¶
Application.provider in authentik's API is an int32 primary key, which
only exists once the provider has actually been created. So an Application
applied before its provider cannot be fully created.
It does not fail. It waits:
- The
Applicationis admitted and reconciled. - The provider reference does not resolve yet, so it reports
Ready=Falsewith reasonReferenceNotFoundand requeues with backoff. - When the
OAuth2Providerbecomes ready and publishes its numericstatus.remoteID, theApplicationreconciles again and converges.
This matters for GitOps, where a whole directory is applied at once in
whatever order the tool chose. kubectl apply -f . converges; it does not
require you to sequence the files.
kubectl apply -f provider.yaml -f application.yaml
kubectl -n my-apps wait --for=condition=Ready application/grafana --timeout=120s
Watching convergence
Seeing ReferenceNotFound for a few seconds after a bulk apply is normal.
Seeing it for minutes means the provider itself is not becoming ready —
check the provider, not the application.
4. Consume the credentials¶
The operator creates grafana-oidc in my-apps with two keys:
| Key | Contents |
|---|---|
clientID |
The OAuth2 client ID authentik generated |
clientSecret |
The OAuth2 client secret authentik generated |
env:
- name: GF_AUTH_GENERIC_OAUTH_ENABLED
value: "true"
- 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
- name: GF_AUTH_GENERIC_OAUTH_AUTH_URL
value: https://authentik.example.com/application/o/authorize/
- name: GF_AUTH_GENERIC_OAUTH_TOKEN_URL
value: https://authentik.example.com/application/o/token/
- name: GF_AUTH_GENERIC_OAUTH_API_URL
value: https://authentik.example.com/application/o/userinfo/
Environment variables do not reload
A Pod reads secretKeyRef values once, at start. If the client secret is
ever regenerated, the Pod keeps the stale value until it restarts. See
Credentials for the rotation procedure.
5. Verify¶
Then open your authentik library page. Grafana should appear under Observability, and clicking it should land you in a logged-in Grafana.
If it does not:
kubectl -n my-apps describe application grafana
kubectl -n my-apps describe oauth2provider grafana
kubectl -n my-apps get events --sort-by=.lastTimestamp
Work bottom-up — connection, then provider, then application. A failure at one
layer shows up as ConnectionNotReady or ReferenceNotFound at the layer
above, and chasing the symptom rather than the cause wastes time.
Cleaning up¶
With deletionPolicy: Delete (the default) the authentik objects go too. Set
deletionPolicy: Orphan to keep them — useful when handing an object back to
manual management, or when tearing down a cluster whose authentik outlives it.
The generated grafana-oidc Secret is owned by the OAuth2Provider and is
garbage-collected with it.
Next¶
- Connections — choosing between the two kinds.
- Providers — SAML and proxy providers, and reference resolution in depth.
- Credentials — rotation, and why secrets never appear in
status. - Troubleshooting — when the above does not work.