Identity: Authentik¶
Authentik is the single sign-on for the cluster. It runs in the identity namespace from
the upstream Helm chart. Kustomize inflates the chart in
k8s/talos/infra/authentik/, with
its settings in values.yaml. Blueprints
declare everything it serves: users, groups, providers, applications, flows, the outpost. A
rebuilt cluster comes back with the same logins, and nobody has to use the admin UI.
| Piece | What it is |
|---|---|
authentik-server |
web UI, OIDC endpoints and the embedded outpost, Service port 80 |
authentik-worker |
runs the blueprints and manages the outpost through the Kubernetes API |
authentik-postgresql |
the chart's Postgres 17 StatefulSet, 8 Gi on syno-nfs-csi |
authentik-db-backup |
nightly pg_dump CronJob, see Database and backups |
Redis is off.
Hostnames¶
auth.local.bigd.noon the private gateway. The OIDC issuers use this host.auth.bigd.noon the public gateway. Cloudflare proxies it, and theauthentik-cloudflare-onlyMiddleware (cloudflare-only-middleware.yaml) filters it. The Middleware admits Cloudflare's ranges only. The public host exists so browsers on the internet can sign in tohub.bigd.no. It exposes the whole of Authentik, admin UI included.
The login page is public, so the login flow enforces MFA for every user (see Blueprints).
What it fronts¶
There are two kinds of client. Apps that speak OIDC get an OAuth2 provider and do the login
themselves. Apps that do not speak OIDC get a proxy provider in forward_single mode.
Traefik asks Authentik's embedded outpost before each request reaches them.
| App | Kind | Host | Group that grants access | Client side |
|---|---|---|---|---|
| Argo CD | OIDC, confidential | argocd.local.bigd.no |
argocd-admins (mapped to role:admin) |
argocd/values.yaml, argocd-oidc-secret.yaml |
| Kargo | OIDC, public PKCE | kargo.local.bigd.no |
kargo-admins |
kargo/values.yaml |
| Grafana | OIDC, confidential | grafana.local.bigd.no |
grafana-admins (GrafanaAdmin, everyone else Viewer) |
kube-prometheus-stack/values.yaml, grafana-oidc-secret.yaml |
| Outline | OIDC, confidential | docs.local.bigd.no |
outline-users (policy binding) |
outline/deployment.yaml, outline/externalsecret.yaml |
| reelsmith control panel | forward-auth | reelsmith.local.bigd.no |
reelsmith-admins (policy binding) |
httproute-admin.yaml, middleware-authentik.yaml |
| homepage | forward-auth | hub.bigd.no |
homepage-users (policy binding) |
httproute.yaml, middleware-authentik.yaml |
| Alertmanager | forward-auth, except webhook POST /api/v2/alerts |
alertmanager.local.bigd.no |
alertmanager-admins (policy binding) |
httproutes.yaml, middleware-authentik.yaml |
In Argo CD and Kargo, SSO users outside the group get no role. In Grafana they sign in as
Viewer (role_attribute_strict: false). The reelsmith panel has no login of its own. It
runs with GATEWAY_ADMIN_TRUST_PROXY_AUTH
(configmap.yaml), so the forward-auth
filter on its route is the only lock. If you remove that filter, the panel is open.
Blueprints¶
Each blueprint is a ConfigMap listed under blueprints.configMaps in values.yaml. The
chart mounts each one into the worker at /blueprints/mounted/cm-<name>. The worker
applies it on start and on reconcile. The ConfigMaps sync at wave -1, before the chart's
Deployments.
| ConfigMap | File | Sets up |
|---|---|---|
authentik-blueprint-argocd |
argocd-blueprint.yaml |
group argocd-admins, confidential OAuth2 provider argocd, application slug argocd |
authentik-blueprint-kargo |
kargo-blueprint.yaml |
group kargo-admins, public OAuth2 provider with client_id: kargo, application slug kargo |
authentik-blueprint-grafana |
grafana-blueprint.yaml |
group grafana-admins, confidential OAuth2 provider grafana (authorization code only, no refresh token), application slug grafana |
authentik-blueprint-outline |
outline-blueprint.yaml |
group outline-users, confidential OAuth2 provider outline, application slug outline, policy binding to the group |
authentik-blueprint-reelsmith |
reelsmith-blueprint.yaml |
group reelsmith-admins, proxy provider reelsmith for https://reelsmith.local.bigd.no, application, policy binding to the group |
authentik-blueprint-login-flow |
login-flow-blueprint.yaml |
username (or email) and password on one page, 30-day sessions, MFA for everyone with TOTP or WebAuthn enrolment forced at next sign-in |
authentik-blueprint-homepage |
homepage-blueprint.yaml |
group homepage-users, proxy provider homepage for https://hub.bigd.no, application, policy binding to the group |
authentik-blueprint-alertmanager |
alertmanager-blueprint.yaml |
group alertmanager-admins, proxy provider alertmanager for https://alertmanager.local.bigd.no, application, policy binding to the group |
authentik-blueprint-embedded-outpost |
embedded-outpost-blueprint.yaml |
the embedded outpost's provider list and its authentik_host of https://auth.bigd.no |
Rules that apply to all of them:
- The blueprint declares group membership. Every group holds
akadmin. Reconciliation resets the list, so a member added in the UI is removed again. Add members in the blueprint. - An application without a policy binding is open to every authenticated user. The forward-auth apps and Outline carry a binding to their group.
- OAuth2 providers set
grant_typesexplicitly. If the field is empty, Authentik rejects every grant. - Scope mappings are found by their
managedidentifier. Authentik has nogroupsscope. Thegroupsclaim comes fromprofile. A client that requests agroupsscope getsinvalid_scope. Kargo'sadditionalScopes: []exists for this reason. - Proxy providers set
access_token_validity: hours=24, so the outpost cookie outlives a working session. The re-check is silent while the 30-day Authentik session is alive.
The embedded outpost must list every forward-auth provider¶
Reconcile replaces the outpost's providers field as a whole, so one blueprint owns it. A
forward-auth provider that is not listed there answers 404 on
/outpost.goauthentik.io/auth/traefik, and the app behind it is unreachable. The same
blueprint first applies each provider blueprint by name (metaapplyblueprint with
required: true), so its !Find lookups resolve. Reconcile also replaces config as a
whole. The browser sign-in host must stay the public https://auth.bigd.no, because
browsers reach hub.bigd.no from networks where auth.local.bigd.no does not resolve.
Issuer URLs come from the application slug¶
Each OAuth2 provider uses issuer_mode: per_provider, so the issuer is
https://auth.local.bigd.no/application/o/<application slug>/. The path segment is the
application slug, not the provider name. The trailing slash is required. The client must
match it exactly:
| Client | Setting | Value |
|---|---|---|
| Argo CD | oidc.config.issuer in argocd-cm |
https://auth.local.bigd.no/application/o/argocd/ |
| Kargo | api.oidc.issuerURL |
https://auth.local.bigd.no/application/o/kargo/ |
A mismatch shows in Argo CD as oidc: issuer did not match, and in Kargo as failed
discovery. Only the issuer and the discovery document are per application. Authorize,
token and userinfo are mounted globally without the slug. This is why Grafana's
auth_url, token_url and api_url carry no slug.
Redirect URIs use matching_mode: strict. Both CLIs have a localhost callback
(http://localhost:8085/auth/callback for argocd login --sso,
http://localhost:8087/auth/callback for kargo login --port 8087). Keep the port a
literal integer. A regex port makes Authentik's discovery endpoint return 500.
Secrets¶
One ExternalSecret,
authentik-secrets.yaml,
reads Bitwarden into the authentik-secrets Secret through the
bitwarden-secretsmanager store. The pattern is in
../secrets/README.md.
| Key | Used by |
|---|---|
AUTHENTIK_SECRET_KEY |
Authentik itself |
postgres-password |
server, worker, the chart's Postgres and the backup CronJob |
ARGOCD_OIDC_CLIENT_ID, ARGOCD_OIDC_CLIENT_SECRET |
argocd-blueprint.yaml |
GRAFANA_OIDC_CLIENT_ID, GRAFANA_OIDC_CLIENT_SECRET |
grafana-blueprint.yaml |
OUTLINE_OIDC_CLIENT_ID, OUTLINE_OIDC_CLIENT_SECRET |
outline-blueprint.yaml |
The chart loads the whole Secret into the pods with envFrom. The blueprints read the
client credentials with !Env NAME. There is no !Env default on purpose: a missing
variable fails the blueprint import instead of creating a provider with an empty secret.
Keys must be valid C identifiers (SCREAMING_SNAKE_CASE), or envFrom drops them without
an error.
The client side of each pair reads the same Bitwarden items. Argo CD's argocd-oidc
Secret takes the client secret (its client ID is inline in argocd/values.yaml). Grafana's
grafana-oidc Secret takes both as GF_OAUTH_CLIENT_ID and GF_OAUTH_CLIENT_SECRET.
Kargo is a public PKCE client and has no secret anywhere.
The ExternalSecret syncs at wave -2, before the Deployments. The container reads
envFrom once at start, so running pods do not see a key added to the ExternalSecret
later. After you add a key, restart both Deployments by hand:
kubectl -n identity rollout restart deployment/authentik-server deployment/authentik-worker
Database and backups¶
Authentik's Postgres is the chart's authentik-postgresql StatefulSet (Postgres 17, with
host, user and database all authentik). It is separate from the logeverylift Postgres in
../data/postgres.md.
db-backup.yaml runs
pg_dump --format=custom at 00:50 Europe/Oslo into a static NFS volume at
nas.local.bigd.no:/volume1/k8s-backups/postgres/authentik (PV reclaim policy Retain).
It keeps 30 days. The dump image stays on the server's major version, because pg_dump
refuses a newer server. The job pod carries app.kubernetes.io/instance: authentik, the
label that the Authentik CiliumNetworkPolicy admits to Postgres. Without it, the dump
cannot connect once policies are enforced. The monthly restore test loads the newest dump
into a scratch Postgres and checks authentik_core_user.
Schedule and coverage are in ../backups/README.md. The restore
procedure is the Postgres section of ../backups/restore.md.
Network policy¶
ciliumnetworkpolicy.yaml
admits ingress from Traefik, from pods labelled app.kubernetes.io/instance: authentik
in identity, and from the host and health entities. It allows egress to CoreDNS, to
those same pods and to the Kubernetes API (the worker manages outposts through it). No
external identity provider is configured.
Configured outside Git¶
Blueprints cannot set tenant settings, so these live in Authentik's database.
| Where | Setting |
|---|---|
| System > Settings > Avatars | initials. The default gravatar,initials fetches from gravatar.com, which the egress policy blocks. |
Admin and recovery access¶
The admin user is akadmin. Every blueprint references it by username and adds it to
every group. The repo sets no bootstrap password or token, so the akadmin credentials
exist only in Authentik's database. The nightly dump covers them.
If Authentik is down, each OIDC app keeps a local login:
- Argo CD keeps its built-in local admin, which bypasses RBAC.
policy.default: ""gives unmapped users nothing. - Kargo keeps the chart's admin account enabled.
- Grafana keeps the login form (
disable_login_form: false,oauth_auto_login: false).
The forward-auth apps have no fallback. When the outpost is unreachable, reelsmith's panel
and hub.bigd.no are closed.
Put a new app behind login¶
For an app with no OIDC support, copy the reelsmith (private host) or homepage (public
host) pattern. <app> is the app's namespace and name.
- Add
k8s/talos/infra/authentik/<app>-blueprint.yaml, modelled onreelsmith-blueprint.yaml. It holds a group, a proxy provider withmode: forward_singleandexternal_hostset to the browser-facing URL, an application, and a policy binding from the application to the group. Give the blueprint a uniquemetadata.name. Annotate the ConfigMap with sync wave-1. - Add the ConfigMap name to
blueprints.configMapsinvalues.yaml. Add the file tokustomization.yaml. - In
embedded-outpost-blueprint.yaml, add ametaapplyblueprintentry for the new blueprint name and a!Findfor the new provider underproviders. If you skip this, the app answers 404. - Add
referencegrant-<app>.yamlink8s/talos/infra/authentik/, modelled onreferencegrant-reelsmith.yaml. It grants fromHTTPRoutein the app's namespace to theauthentik-serverService only. Without it, the route stays atResolvedRefs=Falseand the sign-in callback returns 500. Add it tokustomization.yaml. - In the app's directory, add a Traefik
Middlewarenamed<app>-authentik, modelled on the app'smiddleware-authentik.yaml. SetforwardAuth.addresstohttp://authentik-server.identity.svc.cluster.local/outpost.goauthentik.io/auth/traefik. It must live in the route's namespace, because Traefik resolves an HTTPRouteExtensionRefthere. SettrustForwardHeader: trueon the private gateway, which overwrites theX-Forwarded-*headers itself. Set it tofalseon a public host. There, a client-suppliedX-Forwarded-Hostwould choose which provider Authentik evaluates. - In the app's HTTPRoute, add a first rule for
PathPrefix: /outpost.goauthentik.io/with a cross-namespace backendRef toauthentik-serverinidentity, port 80, and no auth filter. An auth filter there deadlocks the sign-in. Put the<app>-authentikExtensionReffilter on the/rule. On a public host, put the app's Cloudflare-only Middleware on both rules, as homepage does. Keep the host out of any Cloudflare cache rule, because the session cookie decides every response. - After ArgoCD syncs, the worker restarts with the new file mounted. Authentik only
registers a new blueprint file on its scheduled discovery. Until then, the
embedded-outpostblueprint errors on the missingmetaapplyblueprint, and the host answers 404 from Authentik. Run discovery instead of waiting:
kubectl -n identity exec deploy/authentik-worker -- ak shell -c \
"from authentik.blueprints.v1.tasks import blueprints_discovery; blueprints_discovery.send()"
- Sign in at the app's host. Confirm that a user outside the group is refused.
For an app that speaks OIDC, copy
grafana-blueprint.yaml or,
for a public PKCE client, the Kargo one. Create the client ID and secret in Bitwarden
(see ../secrets/README.md). Add them to
authentik-secrets.yaml as C-identifier keys. Read them with !Env. Restart both
Authentik Deployments. Point the app at an ExternalSecret for the same Bitwarden items.
Set the app's issuer to https://auth.local.bigd.no/application/o/<slug>/ and its scopes
to openid profile email (plus offline_access only if it redeems refresh tokens). No
outpost or ReferenceGrant change is needed.