From 8901ce4c63d0b94f1f15dcdb92ab8ce4985a6393 Mon Sep 17 00:00:00 2001 From: Chrison Simtian Date: Sun, 23 Aug 2026 16:52:28 +1200 Subject: [PATCH 1/2] Register Krautwatch as an OIDC client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Krautwatch gained OIDC support (Chrison-dev/Krautwatch#48), so its web UI can stop carrying its own single-admin login. This adds the provider, application and policy bindings, following the Pangolin entry — including its grant_types lesson, which fails identically and just as opaquely when omitted. Admins only: Krautwatch drives the download client and writes the media library, which is machinery rather than something Family or Guests consume. Two redirect URIs, because the UI is LAN-only today and would move to a hostname the day it is fronted; registering both now keeps that a compose edit. Nothing switches over yet — krautwatch-web keeps its local login until the client pair exists in Bitwarden Secrets Manager. --- stacks/Core/authentik.lxc.yaml | 4 ++ .../blueprints/00-homelab-identity.yaml | 66 +++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/stacks/Core/authentik.lxc.yaml b/stacks/Core/authentik.lxc.yaml index 10cc88f..4c4a0d0 100644 --- a/stacks/Core/authentik.lxc.yaml +++ b/stacks/Core/authentik.lxc.yaml @@ -136,3 +136,7 @@ spec: # blueprint file, silently undoing every unrelated change in it. authentik_pulse_client_id: AUTHENTIK_PULSE_CLIENT_ID authentik_pulse_client_secret: AUTHENTIK_PULSE_CLIENT_SECRET + # Krautwatch's web UI (Krautwatch #48). The same pair is handed to the krautwatch-web + # container as Auth__Oidc__ClientId / Auth__Oidc__ClientSecret. + authentik_krautwatch_client_id: AUTHENTIK_KRAUTWATCH_CLIENT_ID + authentik_krautwatch_client_secret: AUTHENTIK_KRAUTWATCH_CLIENT_SECRET diff --git a/stacks/Core/authentik/assets/blueprints/00-homelab-identity.yaml b/stacks/Core/authentik/assets/blueprints/00-homelab-identity.yaml index d3ec735..e1785a6 100644 --- a/stacks/Core/authentik/assets/blueprints/00-homelab-identity.yaml +++ b/stacks/Core/authentik/assets/blueprints/00-homelab-identity.yaml @@ -271,6 +271,72 @@ entries: identifiers: { target: !KeyOf pulse-app, group: !KeyOf grp-monitoring-admins } attrs: { order: 1, enabled: true } + # ── Krautwatch: OIDC provider ───────────────────────────────────────────────────── + # Krautwatch (CT 5114, the Media podman host) can hand its web UI's sign-in to an IdP as of + # v0.7.0. Registered here for the same reason Pangolin is: the client pair is fixed in + # Bitwarden Secrets Manager rather than minted by Authentik and copied out of a UI. + - model: authentik_providers_oauth2.oauth2provider + id: krautwatch-provider + identifiers: { name: Krautwatch } + attrs: + client_type: confidential + client_id: !Env AUTHENTIK_KRAUTWATCH_CLIENT_ID + client_secret: !Env AUTHENTIK_KRAUTWATCH_CLIENT_SECRET + authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]] + # Krautwatch checks the groups claim itself as a second layer, and reads it from the ID + # token rather than depending on a userinfo round trip. + include_claims_in_id_token: true + # Same trap as Pangolin: an empty grant_types allow-list rejects every authorization + # request with a message that says nothing about grant types. + grant_types: + - authorization_code + - refresh_token + property_mappings: + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]] + # Strict, not regex: ASP.NET Core's handler uses one fixed callback path, so there is + # nothing variable to match. Two URIs because the UI is LAN-only today (no hostname, no + # CF Access app) and would move to a name the day it is fronted — registering both now + # means that change is a compose edit rather than an identity change. + redirect_uris: + - matching_mode: strict + url: http://10.10.255.254:5099/signin-oidc + - matching_mode: strict + url: https://krautwatch.chrison.dev/signin-oidc + + # ── Krautwatch: application ─────────────────────────────────────────────────────── + - model: authentik_core.application + id: krautwatch-app + identifiers: { slug: krautwatch } + attrs: + name: Krautwatch + provider: !KeyOf krautwatch-provider + meta_launch_url: http://10.10.255.254:5099 + meta_description: German public-TV indexer and download client for the *arr fleet. + + # ── Who may sign in ─────────────────────────────────────────────────────────────── + # Admins only. Krautwatch drives the download client and rewrites the media library's + # contents; family and guests have Plex for the output and no business with the machinery. + # + # Krautwatch re-checks the groups claim itself, so a binding removed here does not silently + # open the UI. That second check is configured with the group NAMES, not these entry ids: + # + # Auth__Oidc__RequiredGroups=homelab-admins,media-admins + # + # which is why the names have to be kept in step with #497's rename. Krautwatch splits that + # setting on commas only — never on whitespace — so a name containing a space would work + # there, but the convention holds anyway: consumers that split on whitespace exist (Pulse + # does), and a group name is a value every one of them re-parses with its own rules. + - model: authentik_policies.policybinding + identifiers: { target: !KeyOf krautwatch-app, group: !KeyOf grp-homelab-admins } + attrs: { order: 0, enabled: true } + - model: authentik_policies.policybinding + identifiers: { target: !KeyOf krautwatch-app, group: !KeyOf grp-media-admins } + attrs: { order: 1, enabled: true } + # ── Who is actually in a group ──────────────────────────────────────────────────── # Without this the whole model is inert: the four bindings above admit only members of # those groups, and akadmin was in `authentik Admins` — which is Authentik's OWN admin From b4db8630467a74c4fb3578c5e26767f924179d9f Mon Sep 17 00:00:00 2001 From: Chrison Simtian Date: Sun, 23 Aug 2026 19:54:49 +1200 Subject: [PATCH 2/2] Inject Krautwatch's client pair, not just declare it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The blueprint entry was inert as written. authentik.lxc.yaml's `secrets:` block creates the podman secret; injecting it as an environment variable takes a `Secret=…,type=env,target=…` line in the server and worker quadlets, and the worker is what applies blueprints. Without it `!Env` resolves null and authentik rejects the entire file — #497, with the collateral being whatever else happens to be in it. The runner also never saw the pair: _deploy-stack.yml has no secrets.env and falls back to process env, so every secret a stack declares has to be listed explicitly. Same class as #446/#471/#490. Both found in review by the Homelab session. The shape and the quadlet each looked correct in isolation, which is exactly what made #497 hard to see. --- .github/workflows/_deploy-stack.yml | 2 ++ stacks/Core/authentik/quadlets/authentik-server.container | 6 ++++++ stacks/Core/authentik/quadlets/authentik-worker.container | 6 ++++++ 3 files changed, 14 insertions(+) diff --git a/.github/workflows/_deploy-stack.yml b/.github/workflows/_deploy-stack.yml index cde0017..ad2ca03 100644 --- a/.github/workflows/_deploy-stack.yml +++ b/.github/workflows/_deploy-stack.yml @@ -82,6 +82,8 @@ jobs: AUTHENTIK_PANGOLIN_CLIENT_SECRET: ${{ secrets.AUTHENTIK_PANGOLIN_CLIENT_SECRET }} AUTHENTIK_PULSE_CLIENT_ID: ${{ secrets.AUTHENTIK_PULSE_CLIENT_ID }} AUTHENTIK_PULSE_CLIENT_SECRET: ${{ secrets.AUTHENTIK_PULSE_CLIENT_SECRET }} + AUTHENTIK_KRAUTWATCH_CLIENT_ID: ${{ secrets.AUTHENTIK_KRAUTWATCH_CLIENT_ID }} + AUTHENTIK_KRAUTWATCH_CLIENT_SECRET: ${{ secrets.AUTHENTIK_KRAUTWATCH_CLIENT_SECRET }} # monitoring stack (#485). These four were never here because monitoring had no # workflow to need them — every apply ran from a laptop that already had secrets.env, # so nothing ever noticed. Adding deploy-monitoring.yml made the runner the first diff --git a/stacks/Core/authentik/quadlets/authentik-server.container b/stacks/Core/authentik/quadlets/authentik-server.container index f105242..23cb014 100644 --- a/stacks/Core/authentik/quadlets/authentik-server.container +++ b/stacks/Core/authentik/quadlets/authentik-server.container @@ -76,6 +76,12 @@ Secret=authentik_pangolin_client_secret,type=env,target=AUTHENTIK_PANGOLIN_CLIEN # being copied between two UIs. Secret=authentik_pulse_client_id,type=env,target=AUTHENTIK_PULSE_CLIENT_ID Secret=authentik_pulse_client_secret,type=env,target=AUTHENTIK_PULSE_CLIENT_SECRET +# Krautwatch's pair, same reason again: the blueprint declaring its provider is applied in this +# container, so `!Env AUTHENTIK_KRAUTWATCH_CLIENT_ID` reads it from here. Declaring the secret in +# authentik.lxc.yaml only CREATES it — without these two lines it is never injected, `!Env` +# resolves null, and authentik rejects the whole blueprint file (#497). +Secret=authentik_krautwatch_client_id,type=env,target=AUTHENTIK_KRAUTWATCH_CLIENT_ID +Secret=authentik_krautwatch_client_secret,type=env,target=AUTHENTIK_KRAUTWATCH_CLIENT_SECRET # Must match ContainerName= on the postgres unit — resolved by aardvark-dns on the shared # network, not by anything in this file. Environment=AUTHENTIK_POSTGRESQL__HOST=authentik-postgresql diff --git a/stacks/Core/authentik/quadlets/authentik-worker.container b/stacks/Core/authentik/quadlets/authentik-worker.container index 5143bde..327986d 100644 --- a/stacks/Core/authentik/quadlets/authentik-worker.container +++ b/stacks/Core/authentik/quadlets/authentik-worker.container @@ -69,6 +69,12 @@ Secret=authentik_pangolin_client_secret,type=env,target=AUTHENTIK_PANGOLIN_CLIEN # being copied between two UIs. Secret=authentik_pulse_client_id,type=env,target=AUTHENTIK_PULSE_CLIENT_ID Secret=authentik_pulse_client_secret,type=env,target=AUTHENTIK_PULSE_CLIENT_SECRET +# Krautwatch's pair, same reason again: the blueprint declaring its provider is applied in this +# container, so `!Env AUTHENTIK_KRAUTWATCH_CLIENT_ID` reads it from here. Declaring the secret in +# authentik.lxc.yaml only CREATES it — without these two lines it is never injected, `!Env` +# resolves null, and authentik rejects the whole blueprint file (#497). +Secret=authentik_krautwatch_client_id,type=env,target=AUTHENTIK_KRAUTWATCH_CLIENT_ID +Secret=authentik_krautwatch_client_secret,type=env,target=AUTHENTIK_KRAUTWATCH_CLIENT_SECRET Environment=AUTHENTIK_POSTGRESQL__HOST=authentik-postgresql Environment=AUTHENTIK_POSTGRESQL__NAME=authentik Environment=AUTHENTIK_POSTGRESQL__USER=authentik