From e32c2202eaa27be8fb8a56da95262e3218f4eab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20B=C3=A4rlocher?= Date: Fri, 5 Jun 2026 14:27:37 +0200 Subject: [PATCH] feat(demo-gymburgdorf): gate garage-webui via storage-local proxy outpost Finish the garage-webui SSO that the DNS cleanup left broken. Two coupled fixes, both relying on roles added in digitalboard.core: - console.s3 now has an internal name. garage_webui_domains lists both the public and console.s3.int.* FQDN, and the DMZ garage-webui route sets backend_host: console.s3.int.* so the DMZ->storage hop verifies the backend cert against a matching SAN instead of failing on a raw IP (500). - garage-webui ForwardAuth moves off the embedded outpost (cross-host hop mangled X-Forwarded-Host -> 404) onto a dedicated proxy outpost deployed on storage (authentik_outpost_proxy). The garage Traefik middleware now talks to it over the local docker network, and the storage-proxy-outpost is registered in authentik with the garage-webui provider. The stale auth.gymb pin on storage is dropped; the outpost reaches authentik via the reverseproxy (firewall now permits backend -> DMZ). Requires a new bao secret key: /data/authentik proxy_outpost_token. --- .../host_vars/application/authentik.yml | 20 +++++++++++---- .../storage/authentik_outpost_proxy.yml | 16 ++++++++++++ .../host_vars/storage/garage.yml | 25 +++++++++++-------- .../host_vars/storage/traefik.yml | 23 ++++++----------- inventories/demo-gymburgdorf/hosts.yml | 8 ++++++ playbooks/site.yml | 6 +++++ 6 files changed, 67 insertions(+), 31 deletions(-) create mode 100644 inventories/demo-gymburgdorf/host_vars/storage/authentik_outpost_proxy.yml diff --git a/inventories/demo-gymburgdorf/host_vars/application/authentik.yml b/inventories/demo-gymburgdorf/host_vars/application/authentik.yml index bb4d9a0..d5a354f 100644 --- a/inventories/demo-gymburgdorf/host_vars/application/authentik.yml +++ b/inventories/demo-gymburgdorf/host_vars/application/authentik.yml @@ -1,7 +1,7 @@ --- # Bao secret expected at /data/authentik with keys: # secret_key, postgres_password, admin_password, -# ldap_outpost_token, +# ldap_outpost_token, proxy_outpost_token, # nextcloud_oidc_secret, # opnform_oidc_secret, homarr_oidc_secret, bookstack_oidc_secret _authentik: "{{ lookup('community.hashi_vault.hashi_vault', vault_mount + '/data/authentik', url=vault_addr) }}" @@ -63,15 +63,25 @@ authentik_proxy_apps: authorization_slug: default-provider-authorization-implicit-consent invalidation_slug: default-provider-invalidation-flow -# Bind both proxy providers to authentik's built-in embedded outpost so -# we don't have to deploy a separate proxy outpost container. The -# embedded outpost listens on the same host:9000 as the authentik server -# and exposes /outpost.goauthentik.io/auth/traefik for ForwardAuth. +# Outpost bindings. drawio runs on this (application) host, so its +# ForwardAuth can use the embedded outpost (same host:9000 as the +# authentik server). garage-webui runs on the storage host; a cross-host +# ForwardAuth to the embedded outpost arrives with a mangled +# X-Forwarded-Host (an extra reverseproxy hop appends :443 / a comma list) +# and the outpost then fails to match the provider's external_host (404). +# So garage-webui is bound to a dedicated proxy outpost deployed on +# storage (role digitalboard.core.authentik_outpost_proxy), whose +# ForwardAuth endpoint the local storage Traefik reaches over the docker +# network — no extra hop, clean X-Forwarded-Host. authentik_proxy_outposts: - name: "authentik Embedded Outpost" type: proxy providers: - drawio + - name: "storage-proxy-outpost" + type: proxy + token: "{{ _authentik.proxy_outpost_token }}" + providers: - garage-webui # OIDC clients diff --git a/inventories/demo-gymburgdorf/host_vars/storage/authentik_outpost_proxy.yml b/inventories/demo-gymburgdorf/host_vars/storage/authentik_outpost_proxy.yml new file mode 100644 index 0000000..74a3209 --- /dev/null +++ b/inventories/demo-gymburgdorf/host_vars/storage/authentik_outpost_proxy.yml @@ -0,0 +1,16 @@ +--- +# Proxy (ForwardAuth) outpost co-located with garage on storage. The +# garage-webui Traefik router's ForwardAuth middleware points at this +# container over the local `proxy` docker network, so the auth subrequest +# never leaves storage — the embedded outpost on the application host +# can't be used here because the cross-host hop mangles X-Forwarded-Host. +_authentik: "{{ lookup('community.hashi_vault.hashi_vault', vault_mount + '/data/authentik', url=vault_addr) }}" + +# Outpost pulls its config from authentik over the public FQDN (resolves +# via the internal DNS view to the DMZ reverseproxy the storage subnet can +# reach). Token must match the storage-proxy-outpost registered in +# authentik (see application/authentik.yml authentik_proxy_outposts). +authentik_outpost_proxy_host: "https://auth.gymb.souveredu.ch" +authentik_outpost_proxy_token: "{{ _authentik.proxy_outpost_token }}" +authentik_outpost_proxy_insecure: "true" +authentik_outpost_proxy_network: "proxy" diff --git a/inventories/demo-gymburgdorf/host_vars/storage/garage.yml b/inventories/demo-gymburgdorf/host_vars/storage/garage.yml index c868053..aa0b8f1 100644 --- a/inventories/demo-gymburgdorf/host_vars/storage/garage.yml +++ b/inventories/demo-gymburgdorf/host_vars/storage/garage.yml @@ -9,24 +9,27 @@ _garage: "{{ lookup('community.hashi_vault.hashi_vault', vault_mount + '/data/ga garage_s3_domains: - "s3.gymb.souveredu.ch" - "s3.int.gymb.souveredu.ch" -garage_webui_domain: "console.s3.gymb.souveredu.ch" +# First entry is the canonical public console FQDN. The *.int.* entry +# gives the DMZ a backend_host whose cert SAN matches (storage/traefik.yml), +# so the DMZ->storage hop verifies TLS instead of failing on a raw IP. +garage_webui_domains: + - "console.s3.gymb.souveredu.ch" + - "console.s3.int.gymb.souveredu.ch" garage_use_ssl: true 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 embedded outpost only serves the /outpost.goauthentik.io/auth path -# when the request reaches authentik with Host set to a configured -# authentik_domain (auth.gymb.*); it then matches the protected app -# (console.s3.*) via X-Forwarded-Host. So the ForwardAuth must hit the -# app-host's `authentik` router (Host(auth.gymb), passHostHeader=true) — -# NOT the DMZ reverseproxy (strips X-Forwarded-Host) and NOT the auth.int -# rewrite router (overwrites X-Forwarded-Host). auth.gymb.* is therefore -# pinned to the application host in storage/traefik.yml so this single hop -# preserves the forwarded headers and stays in the LAN. +# ForwardAuth talks to the storage-local proxy outpost over the docker +# network (role digitalboard.core.authentik_outpost_proxy). The embedded +# outpost on the application host can't be used cross-host: the extra +# reverseproxy hop mangles X-Forwarded-Host (appends :443 / a comma list) +# and the outpost then 404s instead of matching console.s3.* against the +# provider's external_host. A co-located outpost keeps the subrequest on +# the local network with a clean X-Forwarded-Host. 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: "http://authentik-outpost-proxy-proxy-1:9000/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 f33c1df..6c60ffc 100644 --- a/inventories/demo-gymburgdorf/host_vars/storage/traefik.yml +++ b/inventories/demo-gymburgdorf/host_vars/storage/traefik.yml @@ -1,15 +1,10 @@ --- -# The garage-webui ForwardAuth middleware fires a subrequest to authentik -# (see garage.yml). It must reach the app-host's `authentik` router -# directly — going via the DMZ reverseproxy strips X-Forwarded-Host, which -# breaks the embedded outpost's app matching. The internal DNS view points -# auth.gymb.* at the reverseproxy, so pin it to the application host here -# to force the single direct hop that preserves the forwarded headers. -traefik_extra_hosts: - - "auth.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. +# The garage-webui ForwardAuth no longer needs an auth FQDN pinned here — +# it talks to the storage-local proxy outpost over the docker network +# (see garage.yml), and the outpost reaches authentik via the public FQDN +# over the reverseproxy (firewall now permits backend -> DMZ). traefik_dmz_exposed_services: - name: garage-s3 domain: s3.gymb.souveredu.ch @@ -18,11 +13,9 @@ traefik_dmz_exposed_services: protocol: https - name: garage-webui domain: console.s3.gymb.souveredu.ch - # Pre-existing limitation, orthogonal to the DNS cleanup: the DMZ - # verifies the storage backend cert (acme cert_mode), but with no - # backend_host set it connects by IP, which the storage cert has no - # SAN for, so this route fails the TLS verify. Fixing it needs the - # garage role to expose a console extra_domain (e.g. console.s3.int.*) - # for the backend_host + cert SAN — tracked separately. + # Internal name so the DMZ verifies the storage backend cert against a + # matching SAN (acme cert_mode, no insecureSkipVerify). garage now + # serves this SAN via garage_webui_domains (see garage.yml). + backend_host: console.s3.int.gymb.souveredu.ch port: 443 protocol: https diff --git a/inventories/demo-gymburgdorf/hosts.yml b/inventories/demo-gymburgdorf/hosts.yml index 66261bc..4e4f5c2 100644 --- a/inventories/demo-gymburgdorf/hosts.yml +++ b/inventories/demo-gymburgdorf/hosts.yml @@ -48,6 +48,14 @@ all: hosts: application: + # Proxy (ForwardAuth) outpost co-located with garage on storage, so + # the garage-webui ForwardAuth subrequest stays on the local docker + # network instead of crossing an extra reverseproxy hop (which mangles + # X-Forwarded-Host and breaks the embedded outpost's app matching). + authentik_outpost_proxy_servers: + hosts: + storage: + send_servers: hosts: application: diff --git a/playbooks/site.yml b/playbooks/site.yml index 2e70255..1676774 100644 --- a/playbooks/site.yml +++ b/playbooks/site.yml @@ -70,6 +70,12 @@ roles: - digitalboard.core.authentik_outpost_ldap +- name: Deploy authentik proxy outpost + hosts: authentik_outpost_proxy_servers + become: yes + roles: + - digitalboard.core.authentik_outpost_proxy + - name: Deploy nextcloud service hosts: nextcloud_servers become: yes