From 3ff1b472d7be541e5fc0ea5bdbaa41d3466638ea Mon Sep 17 00:00:00 2001 From: Konstantin Renner Date: Tue, 7 Jul 2026 08:54:30 +0200 Subject: [PATCH 1/2] =?UTF-8?q?authentik:=20Passkey-Step-up-Flow=20f=C3=BC?= =?UTF-8?q?r=20Home=20Assistant=20und=20NetBird?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neuer Authorization-Flow `passkey-stepup-authorization` mit einer WebAuthn-only Authenticator-Validation-Stage, per Blueprint auf die OAuth2-Provider von Home Assistant und NetBird gebunden. Login in diese Apps erfordert damit einen Passkey — auch mit bestehender SSO-Session aus einem Passwort-Login. - not_configured_action: configure — g.renner (HA-Zugriff, kein Passkey enrolled) wird beim ersten Login zur Enrollment geführt statt ausgesperrt - last_auth_threshold hours=8 — kein Doppel-Touch direkt nach Passkey-Login; NetBirds /silent-auth (prompt=none) bleibt innerhalb des Fensters funktionsfähig - Provider-Pflichtfelder (invalidation_flow, redirect_uris, client_type) spiegeln den Live-Stand vom 2026-07-07; ab jetzt besitzt Git diese Felder Co-Authored-By: Claude Fable 5 --- .../apps/authentik/blueprints-configmap.yaml | 102 ++++++++++++++++++ kubernetes/apps/authentik/helmrelease.yaml | 8 +- 2 files changed, 106 insertions(+), 4 deletions(-) diff --git a/kubernetes/apps/authentik/blueprints-configmap.yaml b/kubernetes/apps/authentik/blueprints-configmap.yaml index a5c8f713..b333c8c1 100644 --- a/kubernetes/apps/authentik/blueprints-configmap.yaml +++ b/kubernetes/apps/authentik/blueprints-configmap.yaml @@ -612,3 +612,105 @@ data: identifiers: name: Caddy docs.renner.uno state: absent + + # Passkey (WebAuthn) step-up for selected apps. A dedicated + # AUTHORIZATION flow — unlike a per-provider authentication flow, the + # authorization flow runs on EVERY interactive authorize, so an + # existing password-only SSO session from another app cannot slip + # into the step-up apps without presenting a passkey. + passkey-stepup.yaml: | + version: 1 + metadata: + name: Passkey step-up authorization (homelab) + entries: + - id: passkey-stepup-flow + model: authentik_flows.flow + identifiers: + slug: passkey-stepup-authorization + state: present + attrs: + name: Passkey step-up authorization + title: "Redirecting to %(app)s" + designation: authorization + # Unauthenticated users are bounced through the default + # authentication flow first, then land back here for the + # WebAuthn validation — same as the built-in implicit-consent + # flow this replaces. + authentication: require_authenticated + + - id: passkey-stepup-webauthn-validation + model: authentik_stages_authenticator_validate.authenticatorvalidatestage + identifiers: + name: passkey-stepup-webauthn-validation + state: present + attrs: + device_classes: + - webauthn + # `configure`, not `deny`: g.renner has Home Assistant access + # but no enrolled WebAuthn device (live check 2026-07-07), so + # a hard deny would lock her out with no self-service path. + # `configure` injects the default WebAuthn enrollment stage + # inline — first login without a passkey becomes "enroll one + # now, then validate it". Still passkey-only: there is no way + # through this stage without a WebAuthn credential. + not_configured_action: configure + configuration_stages: + - !Find [authentik_stages_authenticator_webauthn.authenticatorwebauthnstage, [name, default-authenticator-webauthn-setup]] + # Skip re-validation if a passkey was already presented within + # the window. Non-zero for two reasons: (1) no double-touch + # when the login itself used a passkey seconds earlier; + # (2) NetBird's dashboard silently re-authorizes via + # /silent-auth (prompt=none) on token expiry — a flow that + # always demands interaction would fail those and force a + # visible re-login every access-token lifetime. + last_auth_threshold: hours=8 + webauthn_user_verification: preferred + + - model: authentik_flows.flowstagebinding + identifiers: + target: !KeyOf passkey-stepup-flow + stage: !KeyOf passkey-stepup-webauthn-validation + order: 10 + state: present + + # Re-point the (previously UI-managed) OAuth2 providers at the + # step-up flow. Blueprint apply validates the full serializer, so + # the required fields (invalidation_flow, redirect_uris) mirror + # the live values as of 2026-07-07 — git owns them from here on; + # UI edits to these fields will be reverted on the next apply + # cycle. + - model: authentik_providers_oauth2.oauth2provider + identifiers: + name: Homeassistant + state: present + attrs: + authorization_flow: !KeyOf passkey-stepup-flow + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + client_type: confidential + redirect_uris: + - url: https://home.renner.uno/auth/openid/callback + matching_mode: strict + - url: http://homeassistant.srv.renner.uno:8123/auth/openid/callback + matching_mode: strict + + - model: authentik_providers_oauth2.oauth2provider + identifiers: + name: Provider for Netbird + state: present + attrs: + authorization_flow: !KeyOf passkey-stepup-flow + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + client_type: public + redirect_uris: + - url: http://localhost:53000 + matching_mode: strict + - url: https://netbird.renner.uno/ + matching_mode: strict + - url: https://netbird.renner.uno/auth + matching_mode: strict + - url: https://netbird.renner.uno/silent-auth + matching_mode: strict + - url: https://netbird.renner.uno/peers + matching_mode: strict + - url: 'https://netbird\.renner\.uno(/.*)?' + matching_mode: regex diff --git a/kubernetes/apps/authentik/helmrelease.yaml b/kubernetes/apps/authentik/helmrelease.yaml index e5f4f22e..0c23b743 100644 --- a/kubernetes/apps/authentik/helmrelease.yaml +++ b/kubernetes/apps/authentik/helmrelease.yaml @@ -160,10 +160,10 @@ spec: redis: enabled: false - # Mount the K8s-outpost-integration blueprint so the worker re-points - # every Outpost at the in-cluster Kubernetes service connection on - # boot and drops the legacy Docker connection inherited from the - # Proxmox deployment. See blueprints-configmap.yaml. + # Mount the homelab blueprints (K8s outpost integration, passkey + # step-up authorization) — every key of the ConfigMap is applied by + # the worker on boot and on each apply cycle. See + # blueprints-configmap.yaml. blueprints: configMaps: - authentik-blueprints-k8s-outposts -- 2.54.0 From e9e5d8a82c7a94335caa9562216df985ebe51b08 Mon Sep 17 00:00:00 2001 From: Konstantin Renner Date: Tue, 7 Jul 2026 09:08:54 +0200 Subject: [PATCH 2/2] =?UTF-8?q?authentik:=20Passwordless-Passkey-Login=20f?= =?UTF-8?q?=C3=BCr=20Home=20Assistant=20und=20NetBird?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ergänzt den Step-up um den eigentlichen Kern: einen passwortlosen Authentication-Flow (Identification ohne Password-Stage → WebAuthn → Login), per authentication_flow an beide Provider gebunden. Frische Logins in HA/NetBird zeigen damit kein Passwortfeld mehr; der Authentik-eigene Login (Default-Brand-Flow) bleibt unverändert. Sicherheitsdetail: die Validation-Stage im Authentication-Flow ist eine SEPARATE Stage mit not_configured_action: deny — `configure` wäre hier eine Account-Übernahme (User ist nur identifiziert, nicht authentifiziert). Inline-Enrollment bleibt exklusiv im Authorization- Step-up mit require_authenticated. Co-Authored-By: Claude Fable 5 --- .../apps/authentik/blueprints-configmap.yaml | 85 +++++++++++++++++-- 1 file changed, 79 insertions(+), 6 deletions(-) diff --git a/kubernetes/apps/authentik/blueprints-configmap.yaml b/kubernetes/apps/authentik/blueprints-configmap.yaml index b333c8c1..5a49c80b 100644 --- a/kubernetes/apps/authentik/blueprints-configmap.yaml +++ b/kubernetes/apps/authentik/blueprints-configmap.yaml @@ -613,11 +613,16 @@ data: name: Caddy docs.renner.uno state: absent - # Passkey (WebAuthn) step-up for selected apps. A dedicated - # AUTHORIZATION flow — unlike a per-provider authentication flow, the - # authorization flow runs on EVERY interactive authorize, so an - # existing password-only SSO session from another app cannot slip - # into the step-up apps without presenting a passkey. + # Passkey-only (WebAuthn) login for selected apps, in two parts: + # + # 1. A passwordless AUTHENTICATION flow (username → passkey, no + # password prompt) bound per-provider, so fresh logins to these + # apps never see a password field. auth.renner.uno's own login + # (default brand flow) is untouched. + # 2. A step-up AUTHORIZATION flow — unlike the authentication flow, + # this runs on EVERY interactive authorize, so an existing + # password-only SSO session from another app cannot slip into + # these apps without presenting a passkey. passkey-stepup.yaml: | version: 1 metadata: @@ -673,7 +678,73 @@ data: order: 10 state: present - # Re-point the (previously UI-managed) OAuth2 providers at the + # Passwordless authentication flow: identification (username or + # email, NO password stage) → WebAuthn validation → login. + - id: passkey-passwordless-flow + model: authentik_flows.flow + identifiers: + slug: passkey-passwordless-authentication + state: present + attrs: + name: Passkey passwordless authentication + title: "Sign in with your passkey" + designation: authentication + + - id: passkey-passwordless-identification + model: authentik_stages_identification.identificationstage + identifiers: + name: passkey-passwordless-identification + state: present + attrs: + user_fields: + - username + - email + + # SEPARATE stage from the step-up one, and deliberately `deny`: + # at this point in an authentication flow the user has only been + # *identified* (typed a username), not authenticated — `configure` + # here would let anyone who knows a passkey-less username enroll + # their own passkey and take over that account. Inline enrollment + # for passkey-less users happens exclusively in the authorization + # step-up stage above, where require_authenticated guarantees a + # fully authenticated session. A passkey-less user therefore + # bootstraps via password login on auth.renner.uno, then gets the + # enrollment prompt on first app access. + - id: passkey-passwordless-webauthn-validation + model: authentik_stages_authenticator_validate.authenticatorvalidatestage + identifiers: + name: passkey-passwordless-webauthn-validation + state: present + attrs: + device_classes: + - webauthn + not_configured_action: deny + last_auth_threshold: seconds=0 + webauthn_user_verification: preferred + + - model: authentik_flows.flowstagebinding + identifiers: + target: !KeyOf passkey-passwordless-flow + stage: !KeyOf passkey-passwordless-identification + order: 10 + state: present + + - model: authentik_flows.flowstagebinding + identifiers: + target: !KeyOf passkey-passwordless-flow + stage: !KeyOf passkey-passwordless-webauthn-validation + order: 20 + state: present + + - model: authentik_flows.flowstagebinding + identifiers: + target: !KeyOf passkey-passwordless-flow + stage: !Find [authentik_stages_user_login.userloginstage, [name, default-authentication-login]] + order: 30 + state: present + + # Re-point the (previously UI-managed) OAuth2 providers: fresh + # logins use the passwordless flow, every authorize passes the # step-up flow. Blueprint apply validates the full serializer, so # the required fields (invalidation_flow, redirect_uris) mirror # the live values as of 2026-07-07 — git owns them from here on; @@ -684,6 +755,7 @@ data: name: Homeassistant state: present attrs: + authentication_flow: !KeyOf passkey-passwordless-flow authorization_flow: !KeyOf passkey-stepup-flow invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] client_type: confidential @@ -698,6 +770,7 @@ data: name: Provider for Netbird state: present attrs: + authentication_flow: !KeyOf passkey-passwordless-flow authorization_flow: !KeyOf passkey-stepup-flow invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] client_type: public -- 2.54.0