| name | authentik-auth |
| description | Add or change Authentik SSO for a dapper-cluster app (forward-auth proxy or native OIDC), managed declaratively via Envoy Gateway annotations + Authentik blueprints. Use when an app needs login protection, you're adding a new gated app, or fixing/auditing the forward-auth or OIDC wiring. |
| allowed-tools | Read, Edit, Write, Bash, Grep, Glob |
Authentik auth (forward-auth + OIDC)
Two auth models. Pick by whether the app can do OIDC itself:
| Forward-auth (proxy provider) | Native OIDC |
|---|
| Enforcer | Envoy SecurityPolicy → Authentik outpost (app is dumb) | the app (it's an OAuth client) |
| Route annotation | authentik.home.arpa/forward-auth: "true" | none (plain route) |
| Authentik object | proxy provider + app, assigned to embedded outpost | OAuth2 provider + app |
| Secrets in app | none | client_id + client_secret |
| Examples | all *arr, firefly (header variant) | actual, open-webui, proxmox |
Everything is GitOps. Route gating = an HTTPRoute annotation (Kyverno generates the rest). Authentik
side = a blueprint in kubernetes/apps/security/authentik/app/blueprints.yaml (one ConfigMap key
forward-auth.yaml, delivered to the worker via the chart's blueprints.configMaps).
A. Add a forward-auth app (the common case)
1. Gate the route — one annotation
On the app's route.<key>.annotations (app-template) or the standalone HTTPRoute:
route:
app:
annotations:
authentik.home.arpa/forward-auth: "true"
hostnames: ["newapp.${SECRET_DOMAIN}"]
parentRefs: [{ name: internal, namespace: network }]
Kyverno (kyverno/policies/httproute-authentik-forward-auth.yaml) auto-generates the SecurityPolicy
(ext-auth → outpost) and a per-namespace ReferenceGrant (works in any namespace, no edits). Done.
2. Define the Authentik provider + app — in blueprints.yaml, key forward-auth.yaml
Add a block under entries: (everything else is inherited from the &proxy anchor):
- model: authentik_providers_proxy.proxyprovider
identifiers: { name: Provider for Newapp }
id: newapp-provider
attrs: { <<: *proxy, external_host: "https://newapp.${SECRET_DOMAIN}" }
- model: authentik_core.application
identifiers: { slug: newapp }
attrs: { name: Newapp, group: media_internal, provider: !KeyOf newapp-provider,
meta_icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/newapp.svg",
meta_launch_url: "https://newapp.${SECRET_DOMAIN}" }
id: must be unique; !KeyOf <id> binds the app to its provider.
- Add
skip_path_regex: "^/api([/?].*)?$" to the provider attrs only if the app needs an
unauthenticated API bypass (clients hitting /api). Omit it to require auth on everything.
group = portal bucket (e.g. media_internal, home). meta_icon uses
cdn.jsdelivr.net/gh/selfhst/icons/ (bare selfh.st URLs 404).
identifiers matching an existing object adopts it in place (no duplicate); a new slug creates it.
3. Assign it to the outpost — one line
In the same file, the authentik_outposts.outpost entry has a providers: list. Add:
- !KeyOf newapp-provider
This is what actually makes the outpost gate the host. Skipping it = the app shows a 500/“provider
not found” on login. (The outpost entry also disables the k8s Ingress — leave that alone.)
4. Validate, push, apply
kustomize build kubernetes/apps/security/authentik/app/ >/dev/null && echo OK
git add kubernetes/apps/security/authentik/app/blueprints.yaml && git commit && git push
flux reconcile source git flux-system && flux reconcile kustomization authentik -n security
5. Make Authentik discover it (the mount lags ~60–90s)
The ConfigMap mount updates, then Authentik discovers on a schedule. To force it:
pod=$(kubectl get pods -n security -l app.kubernetes.io/instance=authentik,app.kubernetes.io/component=worker \
--field-selector status.phase=Running -o name | head -1)
kubectl exec -n security "$pod" -c worker -- ak shell -c \
"from authentik.blueprints.v1.tasks import blueprints_discovery; blueprints_discovery.send()"
Do not spam discovery/apply — concurrent outpost updates can hit a Postgres deadlock (see Gotchas).
6. Verify
TOKEN=$(kubectl get secret authentik-secret -n security -o jsonpath='{.data.AUTHENTIK_BOOTSTRAP_TOKEN}' | base64 -d)
API=https://sso.${SECRET_DOMAIN}/api/v3
auth=(-H "Authorization: Bearer $TOKEN" -H "Accept: application/json")
curl -sS "${auth[@]}" "$API/managed/blueprints/?search=forward-auth-apps" | jq '.results[0].status'
curl -sI https://newapp.${SECRET_DOMAIN}
B. Add a native-OIDC app
The less common path: the app is itself an OAuth client and enforces auth, so it
gets a plain route (no forward-auth annotation). Used by actual, open-webui,
proxmox. OIDC apps live in a separate blueprint (app/blueprints-oidc.yaml,
ConfigMap authentik-oidc).
Full recipe — adopting an existing app vs. creating a new one, the !Env creds
pattern, and the blueprint YAML — see references/oidc.md.
Non-web-app consumers (Proxmox VE and similar appliances) follow the same Authentik
blueprint side, but the consumer is configured outside Git (PVE realm lives in
/etc/pve/domains.cfg, set via ssh+pveum, not an app HR). Recipe + the
issuer-trailing-slash / group-naming / headless-validation gotchas are in
references/oidc.md → "Non-web-app OIDC consumers".
Gotchas
- Outpost-update deadlock (transient): changing the outpost
providers: list triggers an Authentik
permission rebuild that can hit DeadlockDetected under concurrency → blueprint status=error. Just
re-apply once on a quiet window — it succeeds: POST $API/managed/blueprints/<pk>/apply/. Don't fire
multiple discoveries at once.
- Forgot the
!KeyOf line: provider/app exist but the outpost won't gate the host. Add it.
- Icons & app config are live-only unless in a blueprint; manage via the blueprint, not the UI.
hass/vault have leftover proxy providers not on the outpost (they use OIDC/native) — don't add
them to forward-auth.
- The blueprint adopts by
identifiers and never deletes objects you remove from the file — to
retire an app, remove its entries AND DELETE the live provider+app via the API (apps first).
blueprints.yaml was bootstrapped once from live state by a throwaway script; it is hand-maintained
now (no codegen in the Flux flow).
- OIDC issuer trailing slash: strict OIDC consumers (e.g. Proxmox) exact-match the configured
issuer against Authentik's discovery
issuer, which is published with a trailing slash
(.../application/o/<slug>/). Drop it and the consumer rejects every token. See oidc.md.
- Groups need no extra scope mapping: Authentik's default
profile scope already emits a groups
claim (names) and preferred_username; with include_claims_in_id_token they're in the ID token.
- Flaky OIDC login (timeout or 401) is usually transient Authentik slowness, not config — single
authentik-server replica stalls; the 401 is the ~60s auth code expiring mid-stall. See memory
project-authentik-stability.