Authentik¶
Authentik is the cluster's identity provider. Every platform UI sits behind one login instead of a chart-default password each — and Hubble UI, which ships no authentication at all, would otherwise publish every network flow in the cluster to anyone who could reach the hostname.
| Service | Authentication |
|---|---|
| ArgoCD | OIDC, local admin disabled |
| Grafana | OIDC, login form disabled |
| Home Assistant | Proxy outpost in front of its own login — see Pitfalls |
| Umami | Proxy outpost in front of its own login; the tracker paths pass through — see Pitfalls |
| Rook dashboard, Hubble UI, Prometheus, Alertmanager, Flowscape, Extractor admin UI | Proxy outpost |
| Wollbi-Fescht, Wollbi Adventsfenster | Proxy outpost on the admin paths, with no infra-users binding, see Groups and roles |
At a glance¶
| Namespace | authentik |
| Depends on | OpenBao for its OIDC client secrets, CloudNativePG for the database, Rook-Ceph under it |
| If it is down | Every platform UI loses its only login — see When Authentik is down |
| Health check | kubectl -n authentik get pods,clusters → server and worker Ready, authentik-db healthy |
| UI | auth.k8s.wlkr.ch |
| Files | payload/platform/authentik/ |
Configuration¶
Two integration styles¶
OIDC, for applications that can do it themselves: ArgoCD and Grafana map group membership to a role. Proxy, for the rest: the hostname resolves to Authentik's embedded outpost, a reverse proxy that authenticates the request and only then forwards it to the backend. It needs nothing from the application and protects exactly as far as nothing else can reach the backend.
flowchart LR
U([Browser]) --> GW[infra-gateway / apps-gateway]
GW -->|argo, monitoring, OIDC workloads| APP[ArgoCD / Grafana / OIDC workloads]
APP -.->|OIDC redirect to auth.k8s.wlkr.ch| AK[Authentik]
GW -->|hubble, rook, prometheus, alertmanager, home, flowscape, analytics, extractor-admin| AK
AK -->|authenticated| BE[Hubble UI / Ceph dashboard / ...]
One hostname¶
Authentik answers on auth.k8s.wlkr.ch only, on apps-gateway, which the
LAN reaches through the router's local record and the outside through
Route53. Every OIDC client and the embedded outpost use that one name.
The outpost is why there is only one. A proxied application's route points
at authentik-server, but the login itself is an OAuth dance: the outpost
sends the browser to the outpost's authentik_host to authenticate and gets
it back on the application's own hostname. With Authentik on two names and
authentik_host on the LAN-only one, an application on apps-gateway
worked at home and redirected outside users to a name that does not
resolve. blueprints.yaml pins authentik_host because the embedded
outpost otherwise fills it from web.base_url on first start and never
updates it.
A client must never mix names
The iss claim is the hostname the token was issued through. A discovery URI on one name and a redirect on another fails every login with an error that blames the token. Should a second hostname ever come back, pin each client to exactly one.
Where the proxied routes point¶
A proxied application's route lives with the application: Hubble's in
payload/platform/cilium/, Rook's in payload/platform/rook-ceph/,
Prometheus's and Alertmanager's in payload/platform/monitoring/, and Home
Assistant's, Flowscape's, Umami's, Wollbi-Fescht's, Wollbi
Adventsfenster's and the Extractor admin UI's in the
workloads repository,
each with authentik-server as backendRef. Gateway API forbids a
cross-namespace backendRef unless the target namespace grants it, so
referencegrant.yaml here allows HTTPRoute objects from exactly those
namespaces to that Service only. A proxied workload is therefore two pull
requests, by design: a workload cannot take itself out from behind the
authentication layer on its own.
Database¶
database.yaml is a CloudNativePG Cluster with replicas, the only one on
the cluster that has any. Every login on every platform UI goes through
Authentik, so its database must survive a node drain: with one instance a
reboot kills the database; with replicas, one takes over within a minute,
once enablePDB: false lets the drain evict the primary.
The chart's own Postgres subchart is off — both rules are in
the contract.
The replicas only survive a node loss while they sit on separate nodes, which
the operator's own preferred anti-affinity does not guarantee — hence the
topologySpreadConstraints block, under the rule in Replica
placement.
Chart values¶
| Setting | Why |
|---|---|
authentik.disable_update_check, authentik.disable_startup_analytics and authentik.avatars |
Telemetry is switched off in config, not permitted by network policy — the same rule as Grafana, Loki and Alloy. avatars only seeds a fresh database: since 2024.2 the setting lives in System → Settings, and an existing install keeps calling Gravatar until it is set to initials there. Blueprints cannot set it: system settings are an internally managed model, and upstream declined a config-level override in goauthentik/authentik#26470 |
authentik.web.base_url |
Authentik builds e-mail links and outpost redirects from it and cannot infer it; unset, every admin page shows "The base URL has not been configured". The chart value backfills the tenant, so a rebuilt cluster needs no click |
metrics.enabled and metrics.serviceMonitor.enabled |
The ServiceMonitor renders only when both are set; the switch alone produces nothing, silently. The worker is scraped too: tasks, outpost state and blueprint runs are measured there |
postgresql.enabled: false, authentik.postgresql.host: authentik-db-rw and the global.env entry |
The database is the Cluster above. The password is the one CloudNativePG generated into authentik-db-app, read as AUTHENTIK_POSTGRESQL__PASSWORD from that Secret; it is never copied into OpenBao or Git |
server.topologySpreadConstraints |
The chart's podAntiAffinity preset is soft, which is not enough to hold the replicas apart — see Replica placement. The worker is not replicated and needs none |
worker.podAnnotations homelab.wlkr.ch/secret-generation |
Bumped whenever authentik-secrets or a workload's authentik-secrets-<workload> gains a key, so ArgoCD restarts the worker in the same sync — see Pitfalls |
Workload blueprints as a projected volume, optional: true |
blueprints.configMaps renders a plain configMap volume, and the kubelet refuses a pod whose ConfigMap is missing. Workload blueprints arrive with the workloads, which a fresh cluster may not have yet, so a required mount would keep the worker from starting |
Where a blueprint lives¶
Providers and applications are blueprints, so a rebuild reproduces them.
!Find resolves objects Authentik ships, !KeyOf references another entry in
the same blueprint, and !Env reads an environment variable, which keeps
client secrets out of Git. blueprints_discovery runs on worker startup,
hourly and on a file watcher, asynchronously, so anything configuring itself
against a provider must tolerate it not existing yet.
| What | Lives in | Why there |
|---|---|---|
| Platform providers, applications, the outpost | blueprints.yaml |
Mounted into the worker as a ConfigMap |
| A workload's provider and application | The workload's directory in the workloads repository, as the ConfigMap authentik-blueprint-<application> targeted at authentik |
The provider and the application it authenticates change in one commit; the authentik-foreign-objects policy refuses any other object a workload sends here |
| Client credentials | Generated by a PushSecret beside their reader — secrets.yaml for the platform's, the workload's own secrets.yaml for a workload's — and read here by secrets-<workload>.yaml |
A workload can mint a pair but not register it: the ExternalSecret and mount entry here keep SSO onboarding a deliberate two-repository act |
The mount entry and secret-generation |
application.yaml |
One projected-volume source per workload, with or without credentials; a new credential needs the worker restarted |
The outpost's providers: list |
blueprints.yaml |
It replaces one global object; two repositories writing it would overwrite each other |
referencegrant.yaml |
Here | A proxied workload's route needs granting from this side |
One issuer for OpenCloud¶
OpenCloud's web, desktop, iOS and Android clients are four OIDC clients with
IDs fixed by the clients, so four providers in Authentik, each under an
application of its own. The server and the native clients verify every
token against one issuer, and Authentik derives a provider's issuer from its
application slug, so the four would disagree. All four therefore use
issuer_mode: global, whose issuer is the bare hostname, and the
HTTPRoute rewrites /.well-known/openid-configuration on that hostname to
the opencloud application's discovery document, which is where OpenCloud
and its clients fetch it. The document names the global issuer and the
shared endpoints, so it serves the other three providers as well. Nothing
else uses the root path: every other provider is found under its slug.
User settings flow¶
Dependency-Track keys its accounts on the preferred_username
claim, so a user who could rename themselves could take over another's
account. 05-user-settings.yaml therefore gives the default brand its own
user settings flow with no username field; a username changes only under
Directory → Users. It replaces the brand's flow instead of editing
the default one because the default blueprint rewrites its own objects on
every upgrade, and because the System → Settings switch
default_user_change_username is not a model a blueprint can set.
Authentication flow¶
auth.k8s.wlkr.ch is reachable from the Internet, and the default
authentication flow lets a user without a device in on a password alone.
06-authentication.yaml gives the default brand its own flow out of the
default identification, password and login stages, with two additions. It is
an own flow for the same reason as the user settings
flow: the default blueprint rewrites its own objects on
every upgrade.
| Addition | Why |
|---|---|
Authenticator validation with not_configured_action: configure; TOTP and WebAuthn as setup stages, static tokens accepted as a device class |
A password alone is one leak away from every application, so a user without a device enrols one at the next login instead of being let through. Static tokens are the recovery codes for a lost phone, enrolled afterwards under Settings → MFA devices, which is why they are not offered as the first device |
A deny stage after identification, selected by a reputation policy on the address and the username with threshold -5 |
Every failed login scores both -1, so after five an address or a username is refused for a day (AUTHENTIK_REPUTATION__EXPIRY); the password stage's own five-attempt cancel only ends the current attempt. The policy passes for a bad score, so it selects a deny stage rather than guarding the flow, and it is evaluated when the stage is reached because the flow's own policies see an anonymous request before the username is known |
To lift a lockout early, delete the score under Customization → Policies → Reputation scores.
Bootstrap credentials¶
Authentik's built-in bootstrap blueprint creates akadmin from
AUTHENTIK_BOOTSTRAP_PASSWORD only while no akadmin exists, and reads
nothing from it afterwards. 06-authentication.yaml then sets the account
is_active: false: a password-only superuser that never logs in is the one
worth guessing, and a disabled one authenticates nowhere. The blueprint
touches only is_active, so the password and the authentik Admins
membership stay for break-glass.
Break-glass is ak shell from a cluster-admin kubeconfig, which nothing
in Authentik can lock out:
kubectl -n authentik exec deploy/authentik-server -c server -- ak shell -c \
'from authentik.core.models import User; User.objects.filter(username="akadmin").update(is_active=True)'
Then log in with bootstrap-password from OpenBao — see Generated
secrets — enrol a second factor like any
user, and switch the account off again with the same command and
is_active=False: a blueprint is re-applied only when its file changes, so
nothing switches it off for you.
| Variable | Set | Why |
|---|---|---|
AUTHENTIK_BOOTSTRAP_PASSWORD |
Always, from bootstrap-password |
Without it a fresh install creates akadmin with no usable password, and the initial-setup flow that sets one is open to whoever reaches auth.k8s.wlkr.ch first. Blanking it after the first start buys nothing: the account keeps the password |
AUTHENTIK_BOOTSTRAP_TOKEN |
Never | It mints a non-expiring superuser API token, authentik-bootstrap-token, which nothing here uses, and while the variable is set the blueprint recreates the token whenever it is deleted |
A cluster installed before the token was dropped still holds it: delete
authentik-bootstrap-token under Directory → Tokens and App
passwords, once the worker has restarted without the variable.
Groups and roles¶
Authorisation is group membership. The blueprints create these groups; membership is set by hand under Directory → Groups:
| Group | Grants |
|---|---|
infra-users |
Past the Authentik login of every infra application: ArgoCD, Grafana, Hubble, the Ceph dashboard, Prometheus, Alertmanager, Flowscape and the Extractor admin UI. Grafana Viewer for a direct member |
argocd-admins |
ArgoCD role:admin, and infra-users |
argocd-viewers |
ArgoCD role:readonly, and infra-users |
grafana-admins |
Grafana Admin, and infra-users |
grafana-editors |
Grafana Editor, and infra-users |
wollbi-editors |
A Payload editor on advent.wollbi.ch and fest.wollbi.ch, created on first sign-in; declared in the two sites' blueprints in the workloads repository |
Every infra application carries a policy binding to infra-users, so an
account outside it, such as a wollbi editor's, is refused by Authentik
before the application sees it. The four role groups have infra-users as
their parent, and Authentik counts a child group's members as members of
the parent. Grafana only sees the groups a user belongs to directly, and
with role_attribute_strict it refuses a login that matches none of its
three. ArgoCD's policy.default is empty, so a user in neither ArgoCD group
gets no access. The wollbi sites bind wollbi-editors instead. Home
Assistant and Umami have no binding: they have logins of their own.
Usage¶
Client secrets¶
The OIDC client IDs and secrets are generated once into OpenBao and read by
both sides — Authentik through !Env, ArgoCD and Grafana through their own
ExternalSecret — so a rebuilt Authentik gets the same credentials and
nothing is copied out of a UI. The PushSecret in secrets.yaml writes
kv/authentik/config once — see
Generated secrets — with the secret key, the
bootstrap password and the ArgoCD and Grafana client pairs; its
Password generator's secretKeys is the list of keys. A
workload's client credentials go in the workload's own path (Dependency-Track's in
kv/dependency-track/config,
each read by its own secrets-<workload>.yaml, mounted optional): rewriting kv/authentik/config would rotate the secret key
that signs every session and token, and External Secrets fails an
ExternalSecret whole when one key is missing, so a shared one would leave
a cluster without the workload with no AUTHENTIK_SECRET_KEY either.
The first administrator is created through akadmin, switched on for the
occasion as in Bootstrap credentials; the steps
are in Quickstart step 12.
Adding an application¶
- Generate its client credentials into OpenBao with a
PushSecretbeside the workload's ownExternalSecret, and read them here with anExternalSecretmountedoptional, likesecrets-dependency-track.yaml. A public client (PKCE, as OpenCloud's web and native clients) has no credentials: its client IDs are fixed by the client and sit in the blueprint in clear, so this step and step 3 do not apply. - Mount the workload's blueprint: in
application.yaml, aconfigMapsource namedauthentik-blueprint-<application>withoptional: trueunderworker.volumes, in theprojected.sourcesofblueprints-workloads. - Hand the worker the credentials: in the same file, a
secretReftoauthentik-secrets-<workload>withoptional: trueunderglobal.envFrom, and bumphomelab.wlkr.ch/secret-generation. - Add the provider and application blueprint: an OAuth2 provider with
grant_types, or a proxy provider added to the outpost'sproviders:list. - For a proxied application, point its
HTTPRouteatauthentik-server, add its namespace toreferencegrant.yaml, and add atoEndpointsrule for its pods to theauthentik-serverpolicy inpayload/platform/security/network-policies/authentik.yaml: the outpost dials the backend, and without the rule the login succeeds and the request after it times out. - Bind
infra-usersto an infra application in the blueprint, as anauthentik_policies.policybindingwith the application astarget. An application with no binding is open to every Authentik account.
Adding a user¶
- Create the account under Directory → Users.
- Add it to
infra-users, or to a role group from Groups and roles, under Directory → Groups. Membership is not blueprinted because that would put a username in the repository, and only a superuser passes the bindings without it.
Health check¶
When every login fails, in this order:
-
Server and worker are Ready and the database is healthy:
-
The discovery document names the one hostname as
issuer: -
The embedded outpost is healthy (
Outpostsin the admin UI, or): -
The worker applied the blueprints — a failed one names its file:
Pitfalls¶
A new client credential needs the worker restarted
envFrom injects authentik-secrets once, when the pod starts. A blueprint reading a newly added credential through !Env on a running worker gets an empty string and creates a provider that answers 404 on its discovery endpoint. Bumping secret-generation restarts the worker in the sync that adds the credential; kubectl rollout restart fixes a running cluster but leaves nothing for the next one.
A workload blueprint is code with admin rights
The worker applies a blueprint with full access to Authentik's models, so a blueprint ConfigMap from the workloads repository can create a superuser or add someone to argocd-admins. The admission policy limits which ConfigMap a workload may write, not what it says: review a blueprint change in the workloads repository like a change here.
OAuth2 providers must name their grant types
grant_types defaults to an empty list. A provider that omits it serves no grants, and every login fails with invalid_request: The request is otherwise malformed.
The outpost entry replaces its provider list
authentik_outposts.outpost sets providers wholesale. Every proxied application must be listed there; adding one and forgetting the list silently unassigns the others, which fail open.
Home Assistant and Umami are the exceptions
Each has a login of its own and upstream ships no OIDC provider to replace it, so the outpost sits in front of it: browser users authenticate twice, which is defence in depth rather than single sign-on. Home Assistant's companion apps and webhooks hold a long-lived token and cannot complete an interactive login, so skip_path_regex lets /api/, /auth/token and the external-auth callback through; Home Assistant's own accounts are the only thing guarding those paths, and revoking an Authentik account does not revoke a Home Assistant token. Umami's tracker script and collect endpoint are loaded by every visitor of every site that embeds it, so /script.js and /api/send pass the same way; the rest of the dashboard and API stays behind the login.
Recovery¶
When Authentik is down¶
Single sign-on is a single point of failure for logging in at all. The ArgoCD
path needs a working kubectl, so keep a kubeconfig off-cluster.
ArgoCD — re-enable the local admin account by merging a pull request
that sets admin.enabled in payload/argocd/values.yaml; the argocd
Application self-heals, so a kubectl patch on argocd-cm is reverted at
the next reconcile. ArgoCD syncs the merge without a login. Then restart the
server, which generates argocd-initial-admin-secret, and read the password
as in Quickstart. Revert the pull request once
Authentik is back.
Grafana — the login form is hidden, not removed: the admin account works
through the API, and GF_AUTH_DISABLE_LOGIN_FORM=false brings the form back.
Proxied dashboards have no bypass; port-forward instead:
kubectl -n kube-system port-forward svc/hubble-ui 8080:80
kubectl -n monitoring port-forward svc/kube-prometheus-stack-prometheus 9090:9090
ArgoCD's API key account¶
Disabling the local admin also removes its apiKey capability. If a token is
needed, add an account scoped to what the token is for rather than
re-enabling admin:
The claude account is one, for the in-cluster agents; see
Agent account. The shape: