Skip to content

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.no on the private gateway. The OIDC issuers use this host.
  • auth.bigd.no on the public gateway. Cloudflare proxies it, and the authentik-cloudflare-only Middleware (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 to hub.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.

Browsers reach forward-auth apps through the Traefik gateways and OIDC apps sign in against authentik-server

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_types explicitly. If the field is empty, Authentik rejects every grant.
  • Scope mappings are found by their managed identifier. Authentik has no groups scope. The groups claim comes from profile. A client that requests a groups scope gets invalid_scope. Kargo's additionalScopes: [] 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.

  1. Add k8s/talos/infra/authentik/<app>-blueprint.yaml, modelled on reelsmith-blueprint.yaml. It holds a group, a proxy provider with mode: forward_single and external_host set to the browser-facing URL, an application, and a policy binding from the application to the group. Give the blueprint a unique metadata.name. Annotate the ConfigMap with sync wave -1.
  2. Add the ConfigMap name to blueprints.configMaps in values.yaml. Add the file to kustomization.yaml.
  3. In embedded-outpost-blueprint.yaml, add a metaapplyblueprint entry for the new blueprint name and a !Find for the new provider under providers. If you skip this, the app answers 404.
  4. Add referencegrant-<app>.yaml in k8s/talos/infra/authentik/, modelled on referencegrant-reelsmith.yaml. It grants from HTTPRoute in the app's namespace to the authentik-server Service only. Without it, the route stays at ResolvedRefs=False and the sign-in callback returns 500. Add it to kustomization.yaml.
  5. In the app's directory, add a Traefik Middleware named <app>-authentik, modelled on the app's middleware-authentik.yaml. Set forwardAuth.address to http://authentik-server.identity.svc.cluster.local/outpost.goauthentik.io/auth/traefik. It must live in the route's namespace, because Traefik resolves an HTTPRoute ExtensionRef there. Set trustForwardHeader: true on the private gateway, which overwrites the X-Forwarded-* headers itself. Set it to false on a public host. There, a client-supplied X-Forwarded-Host would choose which provider Authentik evaluates.
  6. In the app's HTTPRoute, add a first rule for PathPrefix: /outpost.goauthentik.io/ with a cross-namespace backendRef to authentik-server in identity, port 80, and no auth filter. An auth filter there deadlocks the sign-in. Put the <app>-authentik ExtensionRef filter 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.
  7. 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-outpost blueprint errors on the missing metaapplyblueprint, 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()"
  1. 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.