From 2206b809e7cd18f2553c085533f4790aa9157ea3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20B=C3=A4rlocher?= Date: Thu, 4 Jun 2026 11:07:48 +0200 Subject: [PATCH] fix(demo-gymburgdorf): route cross-host ForwardAuth via dedicated outpost FQDN Storage Traefik calling the public auth.gymb.* FQDN hit Authentik's ASGI handler, which 404s the /outpost.goauthentik.io/auth/traefik path. Add a dedicated outpost.auth.int.gymb.* FQDN outside authentik_domains so the request falls through to the embedded outpost, pinned to the application host via traefik_extra_hosts to stay on the LAN. - authentik: add authentik_outpost_domains; allow users group on drawio proxy so the Nextcloud drawio iframe works for non-admins - garage: point webui ForwardAuth at the new outpost FQDN - homarr: use public OIDC issuer to match the iss claim, enable auto-login, pin auth FQDN to LAN via extra_hosts - opnform: intercept / and /login for SSO, keep break-glass bypass - drawio: align comments with admins+users allow-list --- .../host_vars/application/authentik.yml | 15 ++++++++++++ .../host_vars/application/drawio.yml | 5 ++-- .../host_vars/application/homarr.yml | 24 +++++++++++++++---- .../host_vars/application/opnform.yml | 6 +++-- .../host_vars/storage/garage.yml | 14 +++++++---- .../host_vars/storage/traefik.yml | 9 ++++++- 6 files changed, 59 insertions(+), 14 deletions(-) diff --git a/inventories/demo-gymburgdorf/host_vars/application/authentik.yml b/inventories/demo-gymburgdorf/host_vars/application/authentik.yml index 55d6a3a..62ffa06 100644 --- a/inventories/demo-gymburgdorf/host_vars/application/authentik.yml +++ b/inventories/demo-gymburgdorf/host_vars/application/authentik.yml @@ -21,6 +21,15 @@ authentik_host_rewrite_domains: authentik_secret_key: "{{ _authentik.secret_key }}" authentik_postgres_password: "{{ _authentik.postgres_password }}" +# Dedicated FQDN for cross-host ForwardAuth (storage Traefik calling +# /outpost.goauthentik.io/auth/traefik). Routing through the public +# auth.gymb.* FQDN doesn't work — Authentik sees Host: auth.gymb.* and +# routes to ASGI which 404s the outpost path. This FQDN sits outside +# authentik_domains so the same request falls through to the embedded +# outpost handler (which matches the protected app via X-Forwarded-Host). +authentik_outpost_domains: + - "outpost.auth.int.gymb.souveredu.ch" + # LDAP outpost (provider for nextcloud) authentik_ldap_apps: - slug: ldap @@ -47,8 +56,14 @@ authentik_proxy_apps: name: Drawio external_host: "https://draw.gymb.souveredu.ch" internal_host: "http://drawio:8080" + # drawio is embedded in Nextcloud as an iframe (nextcloud_enable_drawio). + # Every authenticated Nextcloud user must therefore pass the ForwardAuth + # gate, otherwise the editor loads a 403 inside the iframe. Allow both + # standard groups; tightening this back to admins-only would break the + # Nextcloud integration for regular users. allowed_groups: - admins + - users flows: authentication_slug: default-authentication-flow authorization_slug: default-provider-authorization-implicit-consent diff --git a/inventories/demo-gymburgdorf/host_vars/application/drawio.yml b/inventories/demo-gymburgdorf/host_vars/application/drawio.yml index 2d7f6b6..7e424ac 100644 --- a/inventories/demo-gymburgdorf/host_vars/application/drawio.yml +++ b/inventories/demo-gymburgdorf/host_vars/application/drawio.yml @@ -8,8 +8,9 @@ drawio_domain: "draw.gymb.souveredu.ch" drawio_extra_domains: - "draw.int.gymb.souveredu.ch" -# Gate drawio behind the authentik embedded outpost (admins-only — -# enforced by the policy-binding on the authentik proxy application). +# Gate drawio behind the authentik embedded outpost. The allow-list is +# managed on the authentik proxy application (admins + users) so the +# Nextcloud drawio iframe works for every authenticated user. # ForwardAuth talks to the embedded outpost on the authentik server's # in-network address. Going via the public FQDN routes through a second # traefik hop that strips/rewrites X-Forwarded-Host, which breaks diff --git a/inventories/demo-gymburgdorf/host_vars/application/homarr.yml b/inventories/demo-gymburgdorf/host_vars/application/homarr.yml index dc3ba2e..96b2b7d 100644 --- a/inventories/demo-gymburgdorf/host_vars/application/homarr.yml +++ b/inventories/demo-gymburgdorf/host_vars/application/homarr.yml @@ -14,16 +14,32 @@ homarr_admin_email: "admin@gymb.souveredu.ch" homarr_admin_password: "{{ _homarr.admin_password }}" # OIDC against Authentik. credentials provider stays enabled as a -# break-glass account. +# break-glass account — reach it via /auth/login/credentials when +# AUTH_OIDC_AUTO_LOGIN bypasses the normal /login page. +# +# Issuer must match the `iss` claim authentik emits, which is always the +# public FQDN (authentik's host-rewrite middleware aligns the claim with +# what browsers see). Homarr (oauth4webapi) does a strict 1:1 comparison +# between the discovery response's issuer and this URL — using the +# internal FQDN here fails with OAUTH_JSON_ATTRIBUTE_COMPARISON_FAILED. +# The extra_hosts pin below keeps the actual discovery/token/userinfo +# traffic on the LAN. homarr_auth_providers: "credentials,oidc" -homarr_oidc_issuer: "https://auth.int.gymb.souveredu.ch/application/o/homarr/" +homarr_oidc_issuer: "https://auth.gymb.souveredu.ch/application/o/homarr/" homarr_oidc_client_id: "homarr" homarr_oidc_client_secret: "{{ _homarr.oidc_client_secret }}" homarr_oidc_client_name: "Authentik" homarr_oidc_scopes: "openid profile email groups" homarr_oidc_groups_attribute: "groups" -homarr_oidc_admin_group: "homarr-admins" -homarr_oidc_auto_login: "false" +homarr_oidc_auto_login: "true" + +# Pin the public authentik FQDN to the application host so OIDC +# discovery (and downstream token/userinfo) calls from the homarr +# container stay in the LAN. Without this, fetch() to auth.gymb.* would +# hit the public IP and time out in the DMZ (no hairpin-NAT). Same +# pattern as nextcloud_extra_hosts. +homarr_extra_hosts: + - "auth.gymb.souveredu.ch:172.16.19.101" # Default board with shortcuts to the other gymburgdorf services. Width # values describe horizontal grid cells (1-10 desktop / 6 tablet / 2 diff --git a/inventories/demo-gymburgdorf/host_vars/application/opnform.yml b/inventories/demo-gymburgdorf/host_vars/application/opnform.yml index 1526ac0..6bf41dc 100644 --- a/inventories/demo-gymburgdorf/host_vars/application/opnform.yml +++ b/inventories/demo-gymburgdorf/host_vars/application/opnform.yml @@ -48,8 +48,10 @@ opnform_oidc_admin_group: "opnform-admins" # opnform_oidc_domain above), so no password fallback is needed. opnform_oidc_force_login: true -# Serve a /sso page that jumps straight to Authentik without the email -# login form. Link users to https://forms.gymb.souveredu.ch/sso. +# `/` and `/login` are intercepted and jump straight to Authentik. +# Public form deep-links (`/forms/`, `/admin/...`) keep working. +# Break-glass: /login?bypass=1 reaches the email form when the IdP is +# down. opnform_oidc_sso_entrypoint: true # Pin auth.gymb.* to the application host so server-to-server OIDC diff --git a/inventories/demo-gymburgdorf/host_vars/storage/garage.yml b/inventories/demo-gymburgdorf/host_vars/storage/garage.yml index 37f4179..ae23880 100644 --- a/inventories/demo-gymburgdorf/host_vars/storage/garage.yml +++ b/inventories/demo-gymburgdorf/host_vars/storage/garage.yml @@ -15,12 +15,16 @@ garage_webui_enabled: true # Gate the WebUI behind authentik (admins-only, via policy-binding on the # authentik proxy app). Replaces the htpasswd Basic-Auth — AUTH_USER_PASS # is dropped from the compose env when this is true. The forwardauth URL -# resolves to the application-host traefik (network alias -# `auth.gymb.souveredu.ch` -> authentik-server-1 in the proxy network on -# the application host), but THIS host (storage) is in a different LAN, -# so traefik here reaches it via the public name through the DMZ proxy. +# uses a dedicated outpost-only FQDN that's deliberately outside +# authentik_domains so Authentik routes it to the embedded outpost (not +# ASGI). The public auth.gymb.* FQDN would 404 here — Authentik routes +# any Host matching an auth-domain to ASGI which doesn't serve the outpost +# path. The outpost itself then matches the protected app via +# X-Forwarded-Host (Traefik forwards it via trustForwardHeader=true). +# The FQDN is pinned to the application host via traefik_extra_hosts so +# the request stays in the LAN. garage_webui_authentik_forward_auth: true -garage_webui_authentik_forward_auth_url: "https://auth.gymb.souveredu.ch/outpost.goauthentik.io/auth/traefik" +garage_webui_authentik_forward_auth_url: "https://outpost.auth.int.gymb.souveredu.ch/outpost.goauthentik.io/auth/traefik" # Kept for completeness — only used when authentik ForwardAuth is off. garage_webui_username: "admin" garage_webui_password: "{{ _garage.webui_password | default('disabled') }}" diff --git a/inventories/demo-gymburgdorf/host_vars/storage/traefik.yml b/inventories/demo-gymburgdorf/host_vars/storage/traefik.yml index e704bc3..36530ef 100644 --- a/inventories/demo-gymburgdorf/host_vars/storage/traefik.yml +++ b/inventories/demo-gymburgdorf/host_vars/storage/traefik.yml @@ -1,11 +1,18 @@ --- # Local traefik needs to reach authentik for the ForwardAuth subrequest # the garage-webui router fires. The public IP is unreachable from this -# subnet (no DMZ hairpin), so point auth.gymb.* directly at the +# subnet (no DMZ hairpin), so pin both auth FQDNs directly at the # application host where authentik runs. Without this the forwardauth # middleware would time out and every garage-console request would 502. +# - auth.gymb.* covers any future server-to-server traffic on the public +# FQDN. +# - outpost.auth.int.gymb.* is the dedicated outpost endpoint actually +# used by the ForwardAuth middleware (see garage.yml). It exists only +# to skip Authentik's ASGI handler, which 404s the outpost path when +# Host is one of the configured authentik_domains. traefik_extra_hosts: - "auth.gymb.souveredu.ch:172.16.19.101" + - "outpost.auth.int.gymb.souveredu.ch:172.16.19.101" # Services hosted on `storage` that the DMZ reverseproxy should forward # public traffic to. See application/traefik.yml for the mechanism.