Providers¶
A provider is how authentication happens: an OAuth2/OIDC client, a SAML service provider, or a proxy provider fronted by an outpost. An Application points at exactly one.
Planned design — nothing here exists yet
OAuth2Provider, SAMLProvider and ProxyProvider have no Go types, no
CRDs and no controller. This page describes the intended design so it can be
reviewed before it is built. Manifests here will be rejected by the API
server today.
The three kinds¶
| Kind | For | Needs an outpost |
|---|---|---|
OAuth2Provider |
Apps speaking OAuth2 or OIDC | No |
SAMLProvider |
Apps speaking SAML 2.0 | No |
ProxyProvider |
Apps with no SSO support at all, fronted by forward-auth | Yes |
All three share the same envelope:
spec:
connectionRef:
kind: AuthentikConnection # default
name: default
adoptionPolicy: FailOnConflict # default
deletionPolicy: Delete # default
References are resolved by name, not UUID¶
This is the part of the design most worth reviewing, because it is where the Kubernetes model and authentik's model genuinely disagree.
authentik's API identifies flows, property mappings and certificate keypairs by UUID. Those UUIDs are generated per instance. A manifest containing one is therefore:
- not portable — the same flow has different UUIDs in your staging and production authentik, so the same manifest cannot be applied to both;
- unreviewable —
ba7ded7b-...in a pull request tells a reviewer nothing; - fragile — recreating a flow changes its UUID and silently breaks every manifest referencing it.
So the CRDs reference the human identifier and the operator resolves it on every reconcile:
| Reference | Written as | Resolved to | Example |
|---|---|---|---|
| Flow | slug | Flow UUID | default-provider-authorization-implicit-consent |
| Property mapping | name | Mapping UUID | goauthentik.io/providers/oauth2/scope-email |
| Certificate keypair | name | Keypair UUID | authentik Self-signed Certificate |
Resolution failures¶
Resolution happens before anything is written, so a failure never leaves a half-configured provider in authentik.
ReferenceNotFound-
Nothing in authentik matched. Usually a typo, or a flow that exists in staging but was never created in production. The condition message names what was being looked for. The resource requeues with backoff, so creating the missing flow fixes it without re-applying anything.
ReferenceAmbiguous-
More than one object matched. Property mapping names are not unique in authentik, so two mappings can legitimately share a name. The operator refuses to guess.
Why ambiguity is a hard failure
Picking the first match would bind a scope mapping the author did not mean, and the symptom would be a token missing a claim — noticed weeks later, by an application behaving subtly wrong. There is no safe guess, so the operator stops and names both candidates in the condition message.
Fix it by renaming one of the mappings in authentik so the reference is unambiguous.
Resolution costs API calls
Every reconcile resolves every reference. With many providers sharing the same flows this is repetitive, so resolution results are cached per connection for the duration of a reconcile. A flow renamed in authentik is picked up on the next reconcile, not instantly.
OAuth2Provider¶
apiVersion: authentik.k8s.rka.sh/v1alpha1
kind: OAuth2Provider
metadata:
name: grafana
namespace: my-apps
spec:
connectionRef:
name: default
authorizationFlow: default-provider-authorization-implicit-consent
invalidationFlow: default-provider-invalidation-flow
clientType: confidential # or public
redirectURIs:
- matchingMode: strict
url: https://grafana.example.com/login/generic_oauth
propertyMappings:
- goauthentik.io/providers/oauth2/scope-openid
- goauthentik.io/providers/oauth2/scope-email
- goauthentik.io/providers/oauth2/scope-profile
signingKey: authentik Self-signed Certificate
subMode: hashed_user_id
issuerMode: per_provider
includeClaimsInIDToken: true
accessCodeValidity: minutes=1
accessTokenValidity: hours=1
refreshTokenValidity: days=30
credentialsSecretRef:
name: grafana-oidc
Client type¶
confidential-
The client can keep a secret — a server-side application. authentik generates both a client ID and a client secret, and the operator writes both to
credentialsSecretRef. public-
The client cannot keep a secret — a browser SPA or a mobile app. Only a client ID is generated. The written
SecretcontainsclientIDalone, with noclientSecretkey.
Changing clientType is not a small edit
Switching confidential to public discards the client secret. Every
workload consuming clientSecret from the generated Secret breaks at its
next restart, and the key simply vanishes rather than becoming empty.
Redirect URIs¶
Modern authentik models these as a list of objects with a matching mode rather than a newline-separated string, and the CRD follows that:
redirectURIs:
- matchingMode: strict
url: https://grafana.example.com/login/generic_oauth
- matchingMode: regex
url: ^https://grafana\.example\.com/.*$
regex mode is a footgun
An unanchored or overly broad pattern lets an attacker redirect the
authorization code to a host they control, which is a full account
takeover. Prefer strict. If you must use regex, anchor both ends and
escape the dots.
Generated credentials¶
See Credentials for the full lifecycle. In short: the operator
creates the Secret in the provider's own namespace, sets an owner reference so
it is garbage-collected with the provider, and never puts the secret value in
status, in an event, or in a log line.
SAMLProvider¶
apiVersion: authentik.k8s.rka.sh/v1alpha1
kind: SAMLProvider
metadata:
name: awscli
namespace: my-apps
spec:
connectionRef:
name: default
authorizationFlow: default-provider-authorization-implicit-consent
invalidationFlow: default-provider-invalidation-flow
acsURL: https://signin.aws.amazon.com/saml
audience: urn:amazon:webservices
issuer: https://authentik.example.com
signingKey: authentik Self-signed Certificate # resolved by name
verificationKP: "" # optional, for signed AuthnRequests
propertyMappings:
- authentik default SAML Mapping: Username
- authentik default SAML Mapping: Groups
nameIDMapping: authentik default SAML Mapping: Email
digestAlgorithm: http://www.w3.org/2001/04/xmlenc#sha256
signatureAlgorithm: http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
assertionValidNotBefore: minutes=-5
assertionValidNotOnOrAfter: minutes=5
sessionValidNotOnOrAfter: minutes=86400
The signing key is the whole trust relationship
Anything holding the private half of signingKey can forge assertions for
every service provider trusting it — sign in as any user, anywhere. The
operator references the keypair by name and never reads its private material
through the API, but the authentik token it holds may be able to. Scope the
token accordingly.
Colons in property mapping names
authentik's default SAML mapping names contain a colon
(authentik default SAML Mapping: Username). In YAML that must be quoted
when it appears as a scalar that could be read as a mapping key. Prefer:
ProxyProvider¶
For applications with no SSO support at all. Requests are intercepted by an outpost that authenticates the user and then forwards — or refuses — the request.
apiVersion: authentik.k8s.rka.sh/v1alpha1
kind: ProxyProvider
metadata:
name: internal-tool
namespace: my-apps
spec:
connectionRef:
name: default
authorizationFlow: default-provider-authorization-implicit-consent
invalidationFlow: default-provider-invalidation-flow
mode: forward_single # proxy | forward_single | forward_domain
externalHost: https://tool.example.com
internalHost: http://internal-tool.my-apps.svc.cluster.local:8080
internalHostSSLValidation: true
skipPathRegex: |
^/healthz$
^/metrics$
basicAuthEnabled: false
interceptHeaderAuth: true
Modes¶
proxy-
The outpost terminates the request and proxies to
internalHost. Use when the outpost is the only way in. forward_single-
Your existing ingress calls the outpost's auth endpoint for one host.
internalHostis not used. forward_domain-
As above, but covering a whole domain with one provider. Fewer objects, and a correspondingly larger blast radius if the configuration is wrong.
A proxy provider is not wired up until an outpost carries it
Creating a ProxyProvider configures nothing on its own. It becomes Ready
in authentik's sense while enforcing nothing, because no outpost is serving
it. Assign it to an Outpost, or traffic reaches the
application unauthenticated.
skipPathRegex bypasses authentication entirely
Every matching path is served with no authentication at all. Anchor the
patterns. /health without anchors also matches
/health/../admin/secrets on some servers.
Status¶
All three publish the same ManagedResourceStatus:
| Field | Meaning |
|---|---|
status.conditions |
Ready and Synced. |
status.observedGeneration |
The metadata.generation this status reflects. |
status.remoteID |
authentik's own identifier — a numeric primary key for providers. |
status.remoteName |
The name last observed in authentik. Informational. |
status.adopted |
Whether this resource took over a pre-existing object. |
status.lastSyncedTime |
Last successful reconcile. |
status.remoteID is the authoritative handle
Once set, lookups prefer it over the name. Renaming the provider on either side therefore does not cause the operator to lose track of the object and create a duplicate.
It is also what an Application binds to — see
Applications.
Adoption¶
adoptionPolicy defaults to FailOnConflict. If an authentik provider with the
target name already exists and the operator cannot establish that it created it,
the reconcile fails with AdoptionConflict, modifies nothing, and requeues.
Set adoptionPolicy: AdoptExisting to take it over deliberately.
ADR 0002 explains why this is not the
default.
Do not template AdoptExisting across everything
It is per-object precisely so importing one legacy provider does not lower the bar for the rest. A blanket setting applied by a Helm template recreates exactly the risk the default exists to prevent.
See also¶
- Applications — binding a provider to something users can click.
- Outposts — required for proxy providers.
- Credentials — what happens to generated client secrets.
- Conditions — every reason, with fixes.