Files
axion1337.chat-gitops/apps/authentik/authentik-blueprints.yaml
T
Thore Cimbal f0174decc1 fix(authentik): give the identification stage its required fields
The actual root cause of the failing blueprint, visible only after the KeyOf references were gone: the entry set recovery_flow alone, but the serializer validates the whole object and rejected it with 'When no user fields are selected, at least one source must be selected'.

All other fields are listed on purpose even though they match the model defaults - whatever the serializer does not receive is reset to its default, so a blueprint naming a single field can silently flip unrelated settings. Values are taken from the live database.

Refs axion1337.chat/axion1337.chat-gitops#60
2026-08-07 12:00:00 +00:00

425 lines
20 KiB
YAML

apiVersion: v1
kind: ConfigMap
metadata:
name: authentik-blueprints
namespace: authentik
data:
matrix-invitation-flow.yaml: |
# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json
version: 1
metadata:
name: matrix-invitation-flow
labels:
blueprints.goauthentik.io/instantiate: "true"
entries:
# Reaffirm the flow itself (already created manually; matched by slug)
- model: authentik_flows.flow
state: present
identifiers:
slug: matrix-invitation
id: matrix_invitation_flow
attrs:
name: matrix-invitation
title: matrix-invitation
designation: enrollment
# The prompt stage had accumulated 16 unrelated system validation_policies
# (e.g. default-user-settings-authorization, default-oobe-password-usable)
# from manual UI setup, likely a "select all" slip in the policy picker.
# These crash on an anonymous enrollment context ('AnonymousUser' object
# has no attribute 'group_attributes', etc). A prompt stage needs none here.
- model: authentik_stages_prompt.promptstage
state: present
identifiers:
name: matrix-invitation-prompt
attrs:
validation_policies: []
# Correct stage chain, mirroring the working matrix-enrollment flow:
# Invite -> Prompt (username/email/password) -> Write -> Password -> Login
# Root cause of the original bug: only Invite+Prompt were bound, both at
# order=0, so the flow never wrote the user to the DB or logged them in.
- model: authentik_flows.flowstagebinding
state: present
identifiers:
target: !KeyOf matrix_invitation_flow
order: 0
attrs:
stage: !Find [authentik_stages_invitation.invitationstage, [name, matrix-enrollment-invitation]]
- model: authentik_flows.flowstagebinding
state: present
identifiers:
target: !KeyOf matrix_invitation_flow
order: 1
attrs:
stage: !Find [authentik_stages_prompt.promptstage, [name, matrix-invitation-prompt]]
- model: authentik_flows.flowstagebinding
state: present
identifiers:
target: !KeyOf matrix_invitation_flow
order: 2
attrs:
stage: !Find [authentik_stages_user_write.userwritestage, [name, default-source-enrollment-write]]
- model: authentik_flows.flowstagebinding
state: present
identifiers:
target: !KeyOf matrix_invitation_flow
order: 3
attrs:
stage: !Find [authentik_stages_password.passwordstage, [name, default-authentication-password]]
- model: authentik_flows.flowstagebinding
state: present
identifiers:
target: !KeyOf matrix_invitation_flow
order: 4
attrs:
stage: !Find [authentik_stages_user_login.userloginstage, [name, default-source-enrollment-login]]
# Without an explicit destination, the flow falls back to Authentik's own
# /if/user/ dashboard, which refuses type=external users ("Die Oberflaeche
# kann nur von internen Nutzern geoeffnet werden") - exactly the user type
# these Matrix-only accounts correctly have. Send them to Element instead.
- model: authentik_stages_redirect.redirectstage
state: present
identifiers:
name: matrix-invitation-redirect
id: matrix_invitation_redirect_stage
attrs:
mode: static
target_static: https://axion1337.chat
- model: authentik_flows.flowstagebinding
state: present
identifiers:
target: !KeyOf matrix_invitation_flow
order: 5
attrs:
stage: !KeyOf matrix_invitation_redirect_stage
matrix-recovery-flow.yaml: |
# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json
version: 1
metadata:
name: matrix-recovery-flow
labels:
blueprints.goauthentik.io/instantiate: "true"
entries:
# matrix-recovery existed but had zero stage bindings (dead flow), and the
# real login flow (default-authentication-flow, used by the MAS OAuth2
# provider's authentication_flow) didn't link to it at all - no "Forgot
# password?" link was ever shown. Reuses the same default-recovery-*
# stages the built-in default-recovery-flow already uses successfully,
# plus our own redirect stage instead of falling back to the authentik
# dashboard (blocked for type=external Matrix users).
# ⚠️ Hier steckten ZWEI Fehler uebereinander (gitops#60).
#
# 1. !KeyOf koppelt jede Bindung daran, dass DIESER Eintrag im selben Lauf
# eine Model-Instanz erzeugt. Scheitert er, faellt der ganze Blueprint.
# Schlimmer: beim Protokollieren des Fehlers ruft Authentik str() auf das
# !KeyOf-Objekt, dessen __repr__ gegen ein LEERES Blueprint aufloest und
# dabei dieselbe Ausnahme wirft - die echte Fehlermeldung wird dadurch
# ueberdeckt. Man sieht nur noch "KeyOf: failed to find entry".
# Deshalb jetzt !Find gegen die Datenbank, wie in den Blueprints darunter
# ohnehin ueblich. Der Flow existiert, das ist stabiler und entkoppelt.
#
# 2. name und title MUESSEN gesetzt sein. Der FlowSerializer verlangt beide
# (keine Model-Defaults). Fehlen sie, scheitert die Validierung, der Eintrag
# bekommt keine Model-Instanz - und JEDES nachfolgende `!KeyOf
# matrix_recovery_flow` laeuft ins Leere. Genau das war der Zustand bis
# 2026-08-07: der Blueprint wurde bei jedem Lauf verworfen (gitops#60).
# Gegenprobe im selben ConfigMap: matrix-invitation-flow setzt beide und
# laeuft durch.
#
# Die Werte sind der Stand aus der Datenbank, damit sich an der angezeigten
# Seite nichts aendert. (Der Tippfehler "mail-adress" ist so gewachsen und
# bleibt bewusst stehen - Textaenderung waere eine eigene Entscheidung.)
- model: authentik_flows.flow
state: present
identifiers:
slug: matrix-recovery
attrs:
name: "Welcome to aXion1337! Please provide a username or mail-adress."
title: "Welcome to aXion1337! Please provide a username or mail-adress."
designation: recovery
- model: authentik_flows.flowstagebinding
state: present
identifiers:
target: !Find [authentik_flows.flow, [slug, matrix-recovery]]
order: 10
attrs:
stage: !Find [authentik_stages_identification.identificationstage, [name, default-recovery-identification]]
- model: authentik_flows.flowstagebinding
state: present
identifiers:
target: !Find [authentik_flows.flow, [slug, matrix-recovery]]
order: 20
attrs:
stage: !Find [authentik_stages_email.emailstage, [name, default-recovery-email]]
- model: authentik_flows.flowstagebinding
state: present
identifiers:
target: !Find [authentik_flows.flow, [slug, matrix-recovery]]
order: 30
attrs:
stage: !Find [authentik_stages_prompt.promptstage, [name, "Change your password"]]
- model: authentik_flows.flowstagebinding
state: present
identifiers:
target: !Find [authentik_flows.flow, [slug, matrix-recovery]]
order: 40
attrs:
stage: !Find [authentik_stages_user_write.userwritestage, [name, default-recovery-user-write]]
- model: authentik_flows.flowstagebinding
state: present
identifiers:
target: !Find [authentik_flows.flow, [slug, matrix-recovery]]
order: 100
attrs:
stage: !Find [authentik_stages_user_login.userloginstage, [name, default-recovery-user-login]]
- model: authentik_flows.flowstagebinding
state: present
identifiers:
target: !Find [authentik_flows.flow, [slug, matrix-recovery]]
order: 110
attrs:
stage: !Find [authentik_stages_redirect.redirectstage, [name, matrix-invitation-redirect]]
# Wire the "Forgot password?" link on the real login flow used by MAS.
#
# ⚠️ Hier reicht recovery_flow allein NICHT. Der Serializer validiert das
# ganze Objekt, nicht nur die angegebenen Felder - ohne user_fields faellt
# er mit "When no user fields are selected, at least one source must be
# selected". Das war die eigentliche Ursache von gitops#60; sichtbar wurde
# sie erst, nachdem die !KeyOf-Verweise weg waren (die haben die Meldung
# ueberdeckt).
#
# Die uebrigen Felder stehen bewusst mit drin, obwohl sie den Model-Defaults
# entsprechen: Was der Serializer nicht bekommt, setzt er auf den Default
# zurueck. Ein Blueprint, der nur ein Feld nennt, kann so still andere
# Einstellungen kippen. Werte sind der Stand aus der Datenbank.
- model: authentik_stages_identification.identificationstage
state: present
identifiers:
name: default-authentication-identification
attrs:
recovery_flow: !Find [authentik_flows.flow, [slug, matrix-recovery]]
user_fields:
- email
- username
case_insensitive_matching: true
show_matched_user: true
pretend_user_exists: true
show_source_labels: false
enable_remember_me: false
matrix-mfa-setup-redirect.yaml: |
# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json
version: 1
metadata:
name: matrix-mfa-setup-redirect
labels:
blueprints.goauthentik.io/instantiate: "true"
entries:
# 2FA is optional (default-authentication-mfa-validation has
# not_configured_action=skip - login never blocks on missing MFA).
# Users who want to opt in use these built-in single-stage setup flows
# directly (unreachable via /if/user/, which is blocked for type=external
# Matrix accounts). Without a stage after the setup itself, completion
# fell back to the same blocked /if/user/ dashboard - append our redirect.
- model: authentik_flows.flowstagebinding
state: present
identifiers:
target: !Find [authentik_flows.flow, [slug, default-authenticator-totp-setup]]
order: 10
attrs:
stage: !Find [authentik_stages_redirect.redirectstage, [name, matrix-invitation-redirect]]
- model: authentik_flows.flowstagebinding
state: present
identifiers:
target: !Find [authentik_flows.flow, [slug, default-authenticator-webauthn-setup]]
order: 10
attrs:
stage: !Find [authentik_stages_redirect.redirectstage, [name, matrix-invitation-redirect]]
admin-mfa-enforcement.yaml: |
# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json
version: 1
metadata:
name: admin-mfa-enforcement
labels:
blueprints.goauthentik.io/instantiate: "true"
entries:
# MFA-Pflicht fuer Admins, ohne sie fuer alle anderen zu erzwingen.
#
# Warum eine ZWEITE Validate-Stage statt not_configured_action am
# bestehenden default-authentication-mfa-validation umzustellen: dieses Feld
# haengt an der Stage, nicht an der Bindung - eine Umstellung wuerde alle
# Mitglieder treffen. Und die Standard-Stage anzufassen hiesse, ein Objekt
# aus Authentiks eigenem Blueprint zu veraendern.
#
# Diese Loesung fasst KEIN Authentik-Standardobjekt an. Sie haengt eine
# eigene Stage hinter die vorhandene und bindet sie an die Admin-Gruppe.
- model: authentik_stages_authenticator_validate.authenticatorvalidatestage
state: present
identifiers:
name: admin-mfa-validation
id: admin_mfa_stage
attrs:
# configure statt deny: Wer keinen Faktor hat, wird beim Login durch die
# Einrichtung GEFUEHRT. deny wuerde ihn aussperren - und bei einem
# Admin-Konto gibt es dann keinen Weg zurueck ausser ueber den Cluster.
not_configured_action: configure
configuration_stages:
- !Find [authentik_stages_authenticator_totp.authenticatortotpstage, [name, default-authenticator-totp-setup]]
- !Find [authentik_stages_authenticator_webauthn.authenticatorwebauthnstage, [name, default-authenticator-webauthn-setup]]
# Verhindert die doppelte Abfrage: Die Standard-Stage auf Ordnung 30
# validiert bereits, wer einen Faktor hat. Diese hier laeuft danach und
# ueberspringt sich, wenn das Geraet gerade eben benutzt wurde. Uebrig
# bleibt genau der Fall, um den es geht - Admin ohne zweiten Faktor.
last_auth_threshold: hours=1
# Ordnung 31: direkt hinter der Standard-MFA-Stage (30), vor dem Login (100).
- model: authentik_flows.flowstagebinding
state: present
identifiers:
target: !Find [authentik_flows.flow, [slug, default-authentication-flow]]
order: 31
id: admin_mfa_binding
attrs:
stage: !KeyOf admin_mfa_stage
# Eine PolicyBinding mit gesetztem "group" prueft Gruppenmitgliedschaft
# (PolicyResult(group.is_member(user))). Ohne diese Bindung wuerde die
# Stage fuer ALLE gelten - dann waere 2FA fuer die ganze Instanz Pflicht.
- model: authentik_policies.policybinding
state: present
identifiers:
target: !KeyOf admin_mfa_binding
order: 0
attrs:
group: !Find [authentik_core.group, [name, "authentik Admins"]]
matrix-brand-default-app.yaml: |
# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json
version: 1
metadata:
name: matrix-brand-default-app
labels:
blueprints.goauthentik.io/instantiate: "true"
entries:
# Root cause behind several dead ends: an authenticated user hitting "/"
# with no other destination (e.g. after logging in mid-way through the
# TOTP/WebAuthn setup flows) falls back to Brand.default_application: if
# unset, that's /if/user/, which type=external Matrix accounts can't
# open. Only affects the bare "/" fallback - explicit URLs like
# /if/admin/ are unaffected, so internal/staff access is unchanged.
- model: authentik_brands.brand
state: present
identifiers:
domain: authentik-default
attrs:
default_application: !Find [authentik_core.application, [slug, matrix]]
# ThreadNet-Branding: Wer sich anmeldet, soll nicht auf einer Seite
# landen, die sich "authentik" nennt und ein fremdes Logo zeigt.
# Assets werden bewusst NICHT in Authentik hochgeladen, sondern von
# der Client-Auslieferung referenziert - sie liegen dort ohnehin und
# ein zweites Mal gepflegt zu werden ist genau die Quelle, aus der
# spaeter Abweichungen entstehen. Ein ConfigMap-Mount scheidet aus:
# die drei Dateien sind zusammen ~775 KB, base64-kodiert reissen sie
# das 1-MiB-Limit einer ConfigMap.
#
# Preis dieser Wahl: faellt Element Web aus, zeigt die Anmeldeseite
# Platzhalter statt Logo und Hintergrund. Kosmetisch, nicht
# funktional - anmelden kann man sich weiterhin.
#
# ⚠️ alpenglow.jpg existiert erst nach dem naechsten Client-Deploy.
# Wird diese Brand vorher ausgerollt, ist der Hintergrund 404 und
# Authentik zeigt Grau.
branding_title: ThreadNet
# Vorerst Authentiks eigenes Logo. Unser 512-px-PNG rendert in der
# Anmeldemaske in Naturgroesse und damit viel zu gross: Authentiks
# Default ist ein SVG, das sich seiner Box anpasst, ein PNG nicht.
#
# Explizit auf den Default gesetzt statt die Zeile zu loeschen - ein
# Blueprint mit state: present setzt weggelassene Felder NICHT zurueck,
# der alte Wert bliebe in der Datenbank stehen.
#
# ⚠️ SOLANGE DIESE ZEILE HIER STEHT, GEWINNT SIE. Wer das Logo in der
# Authentik-Oberflaeche auswaehlt oder hochlaedt, sieht es bis zur
# naechsten Blueprint-Reconciliation - danach steht wieder der Wert von
# hier. Ein neues Logo gehoert deshalb in diese Zeile, nicht in die UI.
#
# Was schon oeffentlich erreichbar ist und hier eingetragen werden kann:
# https://axion1337.chat/vector-icons/{24,120,144,152,180,512,1024}.png
# Alles quadratische Bildmarken. Fuer diesen Slot passt eher eine
# Wortmarke im Querformat - die gibt es noch nicht. Die vorhandene
# threadnet-logo-wortmarke.png liegt im wiki-Repo (Gruppe homelab) und
# ist von Hetzner aus NICHT erreichbar; sie muesste erst mit dem Client
# ausgeliefert werden, damit Authentik sie laden kann.
branding_logo: /static/dist/assets/icons/icon_left_brand.svg
branding_favicon: https://axion1337.chat/vector-icons/favicon.ico
branding_default_flow_background: https://axion1337.chat/themes/element/img/backgrounds/alpenglow.jpg
matrix-oidc-provider.yaml: |
# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json
version: 1
metadata:
name: matrix-oidc-provider
labels:
blueprints.goauthentik.io/instantiate: "true"
entries:
# The OIDC Provider + Application linking Authentik to MAS was originally
# clicked together by hand in the UI and existed nowhere as code (issue
# #36): losing the Authentik DB would have meant re-creating this from
# scratch, including a new client_secret that MAS would then no longer
# match. client_secret is read from AUTHENTIK_MAS_OIDC_CLIENT_SECRET
# (see authentik.yaml HelmRelease values) rather than inlined here,
# since this ConfigMap itself is not SOPS-encrypted - the actual value
# lives in the authentik-credentials Secret instead.
- model: authentik_providers_oauth2.oauth2provider
state: present
identifiers:
name: Matrix Authentication Service
id: matrix_mas_provider
attrs:
client_type: confidential
client_id: dHbTAgAgXvjh3VALh220mB3dxcVXAifiXU2ZO3U6
client_secret: !Env AUTHENTIK_MAS_OIDC_CLIENT_SECRET
# Path includes MAS's own upstream-provider ID, not Authentik's -
# must match MAS's config exactly or the OIDC callback breaks.
redirect_uris:
- matching_mode: strict
url: https://account.axion1337.chat/upstream/callback/01KQDJTR1ZVTG8JQ220F5BNBFZ
# Stable across username renames - this is what keeps
# upstream_oauth_links rows valid after e.g. the elbojoloco rename.
sub_mode: hashed_user_id
include_claims_in_id_token: true
access_code_validity: minutes=1
access_token_validity: minutes=5
signing_key: !Find [authentik_crypto.certificatekeypair, [name, "authentik Self-signed Certificate"]]
authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]]
property_mappings:
- !Find [authentik_core.propertymapping, [managed, "goauthentik.io/providers/oauth2/scope-openid"]]
- !Find [authentik_core.propertymapping, [managed, "goauthentik.io/providers/oauth2/scope-email"]]
- !Find [authentik_core.propertymapping, [managed, "goauthentik.io/providers/oauth2/scope-profile"]]
- model: authentik_core.application
state: present
identifiers:
slug: matrix
attrs:
name: aXion1337.chat Accountverwaltung
provider: !KeyOf matrix_mas_provider
meta_description: Matrixclient tailored for aXionCommunity
meta_publisher: aXionGaming
policy_engine_mode: any
open_in_new_tab: false