refactor(demo-gymburgdorf): drop DNS workarounds now backend can reach DMZ

The firewall now permits the backend subnet (172.16.19.0/24) to reach the
DMZ reverseproxy on 443, so server-to-server calls can use the public
FQDN over the reverseproxy instead of the previous workarounds:

- Remove the `*_extra_hosts` pins from nextcloud, opnform, bookstack and
  homarr; their OIDC discovery/issuer now uses the public auth.gymb.* FQDN
  (verified: discovery returns 200 with the public iss over the reverseproxy).
- Switch nextcloud OIDC discovery_url from auth.int.* to auth.gymb.*.
- Drop authentik_host_rewrite_domains and authentik_outpost_domains; point
  the LDAP outpost (both host_vars) at the public auth.gymb.* FQDN.
- Repoint the garage-webui ForwardAuth from the never-functional dedicated
  outpost FQDN to auth.gymb.*. The embedded outpost only serves the auth
  path when Host is a configured authentik_domain and matches the protected
  app via X-Forwarded-Host, so auth.gymb.* stays pinned to the application
  host on storage to bypass the reverseproxy's X-Forwarded-Host stripping.

The *.int.* names and their cert SANs are kept: the DMZ verifies the
backend cert (acme cert_mode) and connects by name, so they remain
structural to the DMZ->backend hop. The console.s3 DMZ route limitation
is pre-existing and tracked separately.
This commit is contained in:
Simon Bärlocher 2026-06-05 13:49:30 +02:00
parent 2206b809e7
commit 2942dabc54
No known key found for this signature in database
GPG key ID: 63DE20495932047A
8 changed files with 58 additions and 110 deletions

View file

@ -10,26 +10,9 @@ _authentik: "{{ lookup('community.hashi_vault.hashi_vault', vault_mount + '/data
authentik_domains:
- "auth.gymb.souveredu.ch"
# Internal FQDN for server-to-server calls (Nextcloud OIDC discovery,
# token, userinfo; LDAP outpost configuration pull). Traefik rewrites
# the Host header to `authentik_domains[0]` on these routers so authentik
# still emits issuer URLs against the public hostname — that keeps the
# iss claim matching what the browser sees while the traffic itself
# stays inside the LAN (the DMZ has no hairpin-NAT for the public IP).
authentik_host_rewrite_domains:
- "auth.int.gymb.souveredu.ch"
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
@ -41,9 +24,10 @@ authentik_ldap_outpost:
name: "ldap-outpost"
token: "{{ _authentik.ldap_outpost_token }}"
config:
# Outpost pulls config from authentik over the internal FQDN — keeps
# the round-trip in the LAN with a valid cert.
authentik_host: "https://auth.int.gymb.souveredu.ch/"
# Outpost pulls config from authentik over the public FQDN, which
# resolves (internal DNS view) to the DMZ reverseproxy the backend
# subnet can reach — keeps the round-trip in the LAN with a valid cert.
authentik_host: "https://auth.gymb.souveredu.ch/"
log_level: "info"
# Proxy providers (ForwardAuth) — gate downstream services behind

View file

@ -3,5 +3,8 @@
# authenticate against the authentik server it talks to.
_authentik: "{{ lookup('community.hashi_vault.hashi_vault', vault_mount + '/data/authentik', url=vault_addr) }}"
authentik_outpost_ldap_host: "https://auth.int.gymb.souveredu.ch"
# Public FQDN resolves (internal DNS view) to the DMZ reverseproxy the
# backend subnet can reach, so the outpost->authentik round-trip stays in
# the LAN with a valid cert and matches the iss claim authentik emits.
authentik_outpost_ldap_host: "https://auth.gymb.souveredu.ch"
authentik_outpost_ldap_token: "{{ _authentik.ldap_outpost_token }}"

View file

@ -20,11 +20,11 @@ bookstack_admin_email: "admin@gymb.souveredu.ch"
bookstack_admin_name: "BookStack Admin"
# OIDC against Authentik. BookStack compares OIDC_ISSUER strictly against
# the `iss` claim in the discovery response. Authentik emits the public
# auth.gymb.* hostname there (host-rewrite middleware ensures the claim
# matches what browsers see during login), so the issuer URL must use the
# public FQDN. Pinning auth.gymb.* in /etc/hosts below keeps the actual
# server-to-server traffic on the LAN.
# the `iss` claim in the discovery response, and Authentik emits the
# public auth.gymb.* hostname there, so the issuer must use the public
# FQDN. That FQDN resolves (internal DNS view) to the DMZ reverseproxy,
# which the backend subnet can reach, so the server-to-server calls stay
# in the LAN.
bookstack_oidc_enabled: true
bookstack_oidc_name: "Authentik"
bookstack_oidc_issuer: "https://auth.gymb.souveredu.ch/application/o/bookstack/"
@ -34,10 +34,3 @@ bookstack_oidc_additional_scopes: "openid profile email"
bookstack_oidc_user_to_groups: true
bookstack_oidc_groups_claim: "groups"
bookstack_oidc_auto_initiate: false
# Pin auth.gymb.* to the application host so server-to-server OIDC calls
# (discovery, token, userinfo, jwks) stay in the LAN and reach authentik
# directly without hairpinning through the DMZ (which has no NAT loop
# back to its own public IP).
bookstack_extra_hosts:
- "auth.gymb.souveredu.ch:172.16.19.101"

View file

@ -18,12 +18,11 @@ homarr_admin_password: "{{ _homarr.admin_password }}"
# 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.
# public FQDN. Homarr (oauth4webapi) does a strict 1:1 comparison between
# the discovery response's issuer and this URL — an internal FQDN here
# fails with OAUTH_JSON_ATTRIBUTE_COMPARISON_FAILED. The public FQDN
# resolves (internal DNS view) to the DMZ reverseproxy, which the backend
# subnet can reach, so discovery/token/userinfo stay in the LAN.
homarr_auth_providers: "credentials,oidc"
homarr_oidc_issuer: "https://auth.gymb.souveredu.ch/application/o/homarr/"
homarr_oidc_client_id: "homarr"
@ -33,14 +32,6 @@ homarr_oidc_scopes: "openid profile email groups"
homarr_oidc_groups_attribute: "groups"
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
# mobile, packed left-to-right).

View file

@ -62,25 +62,16 @@ nextcloud_s3_port: 443
nextcloud_s3_ssl: true
nextcloud_s3_usepath_style: true
# OIDC server-to-server discovery / token / userinfo goes to
# auth.int.gymb.souveredu.ch (LAN, RFC1918). Nextcloud's DnsPinMiddleware
# would otherwise block that as "local server access".
# OIDC discovery/token/userinfo and the S3 backend both target FQDNs
# that resolve to RFC1918 addresses (auth.gymb.* via the reverseproxy,
# s3.int.* directly), which Nextcloud's DnsPinMiddleware would otherwise
# block as "local server access".
nextcloud_allow_local_remote_servers: true
# Share the LDAP docker network with the authentik LDAP outpost
nextcloud_extra_networks:
- ldap
# Pin the public authentik FQDN to the application host so server-to-server
# OIDC traffic (token, userinfo, jwks — endpoints the discovery doc lists
# under auth.gymb.* even when discovery itself is fetched via auth.int.*)
# stays in the LAN. Without this, curl in the PHP container would hit the
# public IP and time out in the DMZ (no hairpin-NAT). The DnsPin middleware
# only honours /etc/hosts when allow_local_remote_servers is enabled, so
# that flag (set above) is what makes this entry effective.
nextcloud_extra_hosts:
- "auth.gymb.souveredu.ch:172.16.19.101"
# LDAP backend (Authentik LDAP outpost)
nextcloud_ldap_enabled: true
nextcloud_ldap_config:
@ -118,13 +109,12 @@ nextcloud_oidc_providers:
display_name: "Login with Authentik"
client_id: nextcloud
client_secret: "{{ _authentik.nextcloud_oidc_secret }}"
# Discovery via the internal FQDN (LAN-only) — the DMZ has no
# hairpin-NAT for the public IP, so server-to-server calls to
# auth.gymb.* would time out. The traefik router for auth.int.*
# rewrites the Host header to auth.gymb.souveredu.ch before the
# request reaches authentik, so the iss claim authentik emits still
# matches the public hostname the browser sees during login.
discovery_url: "https://auth.int.gymb.souveredu.ch/application/o/nextcloud/.well-known/openid-configuration"
# Discovery via the public FQDN. It resolves (internal DNS view) to
# the DMZ reverseproxy, which the backend subnet can now reach, so
# the server-to-server call stays in the LAN while the iss claim and
# all listed endpoints match the public hostname the browser sees
# during login — no host-rewrite or /etc/hosts pin needed.
discovery_url: "https://auth.gymb.souveredu.ch/application/o/nextcloud/.well-known/openid-configuration"
scope: "openid email profile"
unique_uid: true
mapping:

View file

@ -23,18 +23,13 @@ opnform_admin_name: "OpnForm Admin"
opnform_admin_email: "admin@gymb.souveredu.ch"
opnform_admin_password: "{{ _opnform.admin_password }}"
# OIDC against Authentik. Discovery via the internal FQDN keeps
# server-to-server traffic in the LAN; Authentik's host-rewrite router
# rewrites the Host header to auth.gymb.* before the request reaches
# authentik so the iss claim still matches the public hostname browsers
# see during login.
# OIDC against Authentik. The public auth.gymb.* FQDN resolves (internal
# DNS view) to the DMZ reverseproxy, which the backend subnet can reach,
# so OpnForm's discovery/token/userinfo calls stay in the LAN while the
# iss claim matches the public hostname browsers see during login.
opnform_oidc_enabled: true
# Issuer must use the public auth.gymb.* FQDN: OpnForm does OIDC
# discovery and then validates the token's `iss` claim against this
# value. Authentik emits the public hostname in `iss` (its host-rewrite
# middleware keeps the claim aligned with what browsers see), so an
# internal-FQDN issuer here would fail iss validation. The extra_hosts
# pin below keeps the actual discovery/token/userinfo traffic on the LAN.
# Issuer must use the public FQDN: OpnForm validates the token's `iss`
# claim against this value, and Authentik emits the public hostname there.
opnform_oidc_issuer: "https://auth.gymb.souveredu.ch/application/o/opnform/"
opnform_oidc_client_id: "opnform"
opnform_oidc_client_secret: "{{ _opnform.oidc_client_secret }}"
@ -53,10 +48,3 @@ opnform_oidc_force_login: true
# 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
# calls (token, userinfo, jwks — endpoints discovery returns under the
# public hostname even when discovery itself is fetched via auth.int.*)
# stay in the LAN.
opnform_extra_hosts:
- "auth.gymb.souveredu.ch:172.16.19.101"