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
This commit is contained in:
Simon Bärlocher 2026-06-04 11:07:48 +02:00
parent 2ba0c07cd3
commit 2206b809e7
No known key found for this signature in database
GPG key ID: 63DE20495932047A
6 changed files with 59 additions and 14 deletions

View file

@ -21,6 +21,15 @@ authentik_host_rewrite_domains:
authentik_secret_key: "{{ _authentik.secret_key }}" authentik_secret_key: "{{ _authentik.secret_key }}"
authentik_postgres_password: "{{ _authentik.postgres_password }}" 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) # LDAP outpost (provider for nextcloud)
authentik_ldap_apps: authentik_ldap_apps:
- slug: ldap - slug: ldap
@ -47,8 +56,14 @@ authentik_proxy_apps:
name: Drawio name: Drawio
external_host: "https://draw.gymb.souveredu.ch" external_host: "https://draw.gymb.souveredu.ch"
internal_host: "http://drawio:8080" 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: allowed_groups:
- admins - admins
- users
flows: flows:
authentication_slug: default-authentication-flow authentication_slug: default-authentication-flow
authorization_slug: default-provider-authorization-implicit-consent authorization_slug: default-provider-authorization-implicit-consent

View file

@ -8,8 +8,9 @@ drawio_domain: "draw.gymb.souveredu.ch"
drawio_extra_domains: drawio_extra_domains:
- "draw.int.gymb.souveredu.ch" - "draw.int.gymb.souveredu.ch"
# Gate drawio behind the authentik embedded outpost (admins-only — # Gate drawio behind the authentik embedded outpost. The allow-list is
# enforced by the policy-binding on the authentik proxy application). # 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 # ForwardAuth talks to the embedded outpost on the authentik server's
# in-network address. Going via the public FQDN routes through a second # in-network address. Going via the public FQDN routes through a second
# traefik hop that strips/rewrites X-Forwarded-Host, which breaks # traefik hop that strips/rewrites X-Forwarded-Host, which breaks

View file

@ -14,16 +14,32 @@ homarr_admin_email: "admin@gymb.souveredu.ch"
homarr_admin_password: "{{ _homarr.admin_password }}" homarr_admin_password: "{{ _homarr.admin_password }}"
# OIDC against Authentik. credentials provider stays enabled as a # 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_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_id: "homarr"
homarr_oidc_client_secret: "{{ _homarr.oidc_client_secret }}" homarr_oidc_client_secret: "{{ _homarr.oidc_client_secret }}"
homarr_oidc_client_name: "Authentik" homarr_oidc_client_name: "Authentik"
homarr_oidc_scopes: "openid profile email groups" homarr_oidc_scopes: "openid profile email groups"
homarr_oidc_groups_attribute: "groups" homarr_oidc_groups_attribute: "groups"
homarr_oidc_admin_group: "homarr-admins" homarr_oidc_auto_login: "true"
homarr_oidc_auto_login: "false"
# 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 # Default board with shortcuts to the other gymburgdorf services. Width
# values describe horizontal grid cells (1-10 desktop / 6 tablet / 2 # values describe horizontal grid cells (1-10 desktop / 6 tablet / 2

View file

@ -48,8 +48,10 @@ opnform_oidc_admin_group: "opnform-admins"
# opnform_oidc_domain above), so no password fallback is needed. # opnform_oidc_domain above), so no password fallback is needed.
opnform_oidc_force_login: true opnform_oidc_force_login: true
# Serve a /sso page that jumps straight to Authentik without the email # `/` and `/login` are intercepted and jump straight to Authentik.
# login form. Link users to https://forms.gymb.souveredu.ch/sso. # Public form deep-links (`/forms/<slug>`, `/admin/...`) keep working.
# Break-glass: /login?bypass=1 reaches the email form when the IdP is
# down.
opnform_oidc_sso_entrypoint: true opnform_oidc_sso_entrypoint: true
# Pin auth.gymb.* to the application host so server-to-server OIDC # Pin auth.gymb.* to the application host so server-to-server OIDC

View file

@ -15,12 +15,16 @@ garage_webui_enabled: true
# Gate the WebUI behind authentik (admins-only, via policy-binding on the # Gate the WebUI behind authentik (admins-only, via policy-binding on the
# authentik proxy app). Replaces the htpasswd Basic-Auth — AUTH_USER_PASS # authentik proxy app). Replaces the htpasswd Basic-Auth — AUTH_USER_PASS
# is dropped from the compose env when this is true. The forwardauth URL # is dropped from the compose env when this is true. The forwardauth URL
# resolves to the application-host traefik (network alias # uses a dedicated outpost-only FQDN that's deliberately outside
# `auth.gymb.souveredu.ch` -> authentik-server-1 in the proxy network on # authentik_domains so Authentik routes it to the embedded outpost (not
# the application host), but THIS host (storage) is in a different LAN, # ASGI). The public auth.gymb.* FQDN would 404 here — Authentik routes
# so traefik here reaches it via the public name through the DMZ proxy. # 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: 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. # Kept for completeness — only used when authentik ForwardAuth is off.
garage_webui_username: "admin" garage_webui_username: "admin"
garage_webui_password: "{{ _garage.webui_password | default('disabled') }}" garage_webui_password: "{{ _garage.webui_password | default('disabled') }}"

View file

@ -1,11 +1,18 @@
--- ---
# Local traefik needs to reach authentik for the ForwardAuth subrequest # Local traefik needs to reach authentik for the ForwardAuth subrequest
# the garage-webui router fires. The public IP is unreachable from this # 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 # application host where authentik runs. Without this the forwardauth
# middleware would time out and every garage-console request would 502. # 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: traefik_extra_hosts:
- "auth.gymb.souveredu.ch:172.16.19.101" - "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 # Services hosted on `storage` that the DMZ reverseproxy should forward
# public traffic to. See application/traefik.yml for the mechanism. # public traffic to. See application/traefik.yml for the mechanism.