chore(demo-gymburgdorf): finish ACME, LDAP, DMZ routing for live inventory

- ACME via DNS-01 against internal NS (172.16.9.169) with TCP-only +
  disableANSChecks so the DMZ traefik can issue LE certs without
  reaching public NS IPs.
- Migrate single-domain vars to `*_domains` lists (authentik, nextcloud,
  collabora, garage_s3) so public + *.int.* SANs share one cert and
  server-to-server traffic stays in the LAN.
- Wire `traefik_dmz_exposed_services` per backend host (application,
  storage) with explicit `backend_host` overrides pointing at internal
  FQDNs — DMZ traefik now validates upstream certs against SAN names.
- Nextcloud notify_push setup on internal FQDN to avoid DMZ hairpin;
  collabora WOPI / authentik LDAP outpost wired to *.int.* equivalents.
This commit is contained in:
Simon Bärlocher 2026-05-20 23:26:18 +02:00
parent 82f0db8fe3
commit c67e9aac43
No known key found for this signature in database
GPG key ID: 63DE20495932047A
13 changed files with 552 additions and 170 deletions

View file

@ -2,6 +2,7 @@ _acme_tsig: "{{ lookup('community.hashi_vault.hashi_vault', vault_mount + '/data
traefik_use_ssl: true
traefik_cert_mode: "acme"
traefik_ssl_email: "hostmaster@digitalboard.ch"
traefik_log_level: DEBUG
traefik_network: proxy
@ -11,3 +12,9 @@ traefik_acme_tsig_algorithm: "hmac-sha256"
traefik_acme_tsig_key: "{{ _acme_tsig.tsig_key }}"
traefik_acme_tsig_secret: "{{ _acme_tsig.tsig_secret }}"
# UDP/53 egress from the traefik container reaches ns1.digitalboard.ch
# unreliably (i/o timeouts on lego's recursive SOA pre-check), while
# TCP/53 to the same nameserver is open. Force lego to do its DNS
# lookups over TCP so the DNS-01 challenge can proceed.
traefik_acme_tcp_only: true

View file

@ -5,7 +5,13 @@
# nextcloud_oidc_secret
_authentik: "{{ lookup('community.hashi_vault.hashi_vault', vault_mount + '/data/authentik', url=vault_addr) }}"
authentik_domain: "auth.gymb.souveredu.ch"
# First entry is the canonical public FQDN. Additional entries cover
# internal *.int.* names so server-to-server traffic (e.g. the LDAP
# outpost) hits authentik on a name with a valid internal cert and
# skips the DMZ hop.
authentik_domains:
- "auth.gymb.souveredu.ch"
- "auth.int.gymb.souveredu.ch"
authentik_secret_key: "{{ _authentik.secret_key }}"
authentik_postgres_password: "{{ _authentik.postgres_password }}"
@ -20,7 +26,9 @@ authentik_ldap_outpost:
name: "ldap-outpost"
token: "{{ _authentik.ldap_outpost_token }}"
config:
authentik_host: "https://auth.gymb.souveredu.ch/"
# 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/"
log_level: "info"
# OIDC clients

View file

@ -3,5 +3,5 @@
# 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.gymb.souveredu.ch"
authentik_outpost_ldap_host: "https://auth.int.gymb.souveredu.ch"
authentik_outpost_ldap_token: "{{ _authentik.ldap_outpost_token }}"

View file

@ -1,8 +1,16 @@
---
collabora_domain: "office.gymb.souveredu.ch"
# First entry is the canonical public FQDN. Additional entries cover
# internal *.int.* names so nextcloud's WOPI discovery hits collabora
# in the LAN with a valid internal cert.
collabora_domains:
- "office.gymb.souveredu.ch"
- "office.int.gymb.souveredu.ch"
# Hosts allowed to issue WOPI calls. Both names are listed so collabora
# accepts the callback from nextcloud regardless of which FQDN it uses.
collabora_allowed_domains:
- "cloud.gymb.souveredu.ch"
- "cloud.int.gymb.souveredu.ch"
collabora_frame_ancestors:
- "cloud.gymb.souveredu.ch"

View file

@ -4,16 +4,31 @@
_nextcloud: "{{ lookup('community.hashi_vault.hashi_vault', vault_mount + '/data/nextcloud', url=vault_addr) }}"
_authentik: "{{ lookup('community.hashi_vault.hashi_vault', vault_mount + '/data/authentik', url=vault_addr) }}"
nextcloud_domain: "cloud.gymb.souveredu.ch"
# First entry is the canonical public FQDN (used for OVERWRITEHOST and
# OIDC redirects). Additional entries cover internal *.int.* names so
# collabora's WOPI callbacks hit nextcloud on a name with a valid
# internal cert instead of routing through the DMZ.
nextcloud_domains:
- "cloud.gymb.souveredu.ch"
- "cloud.int.gymb.souveredu.ch"
nextcloud_postgres_password: "{{ _nextcloud.postgres_password }}"
nextcloud_admin_user: admin
nextcloud_admin_password: "{{ _nextcloud.admin_password }}"
nextcloud_enable_notify_push: true
# Use the internal FQDN for the notify_push setup check so curl from the
# nextcloud container hits the local traefik directly instead of
# hairpinning through the DMZ reverseproxy.
nextcloud_notify_push_domain: "cloud.int.gymb.souveredu.ch"
# Collabora integration
# wopi_url (server-to-server: nextcloud calls collabora for discovery /
# capabilities) goes to the internal FQDN so the call stays in the LAN.
# public_wopi_url is what the browser loads the office iframe from — that
# stays on the public name reachable through the DMZ.
nextcloud_enable_collabora: true
nextcloud_collabora_domain: "office.gymb.souveredu.ch"
nextcloud_collabora_domain: "office.int.gymb.souveredu.ch"
nextcloud_collabora_public_domain: "office.gymb.souveredu.ch"
# Draw.io integration
nextcloud_enable_drawio: true
@ -30,12 +45,14 @@ nextcloud_apps_to_install:
- files_lock
- notify_push
# S3 primary storage via Garage
# S3 primary storage via Garage — server-to-server, so use the internal FQDN.
# Resolves through the internal DNS to the storage host and presents a valid
# cert from the local traefik on storage.
nextcloud_use_s3_storage: true
nextcloud_s3_key: "{{ lookup('digitalboard.core.garage_credentials', 'nextcloud', host='storage')['key_id'] }}"
nextcloud_s3_secret: "{{ lookup('digitalboard.core.garage_credentials', 'nextcloud', host='storage')['secret_key'] }}"
nextcloud_s3_bucket: "nextcloud"
nextcloud_s3_host: "{{ hostvars['storage']['garage_s3_domain'] }}"
nextcloud_s3_host: "s3.int.gymb.souveredu.ch"
nextcloud_s3_port: 443
nextcloud_s3_ssl: true
nextcloud_s3_usepath_style: true
@ -81,6 +98,11 @@ nextcloud_oidc_providers:
display_name: "Login with Authentik"
client_id: nextcloud
client_secret: "{{ _authentik.nextcloud_oidc_secret }}"
# Stays on the public FQDN: user_oidc validates the iss claim against
# the discovery host, and authentik returns iss based on the request
# host — using auth.int.* would break the iss match with what the
# browser sees (auth.gymb.*). Routed via the DMZ for now; revisit if
# this becomes a bottleneck.
discovery_url: "https://auth.gymb.souveredu.ch/application/o/nextcloud/.well-known/openid-configuration"
scope: "openid email profile"
unique_uid: true

View file

@ -0,0 +1,29 @@
---
# Services hosted on `application` that the DMZ reverseproxy should
# forward public traffic to. The DMZ traefik picks this up via
# hostvars[backend].traefik_dmz_exposed_services and renders a router +
# service for each entry into /config/services.yml.
traefik_dmz_exposed_services:
- name: authentik
domain: auth.gymb.souveredu.ch
backend_host: auth.int.gymb.souveredu.ch
port: 443
protocol: https
- name: nextcloud
domain: cloud.gymb.souveredu.ch
backend_host: cloud.int.gymb.souveredu.ch
port: 443
protocol: https
- name: collabora
domain: office.gymb.souveredu.ch
backend_host: office.int.gymb.souveredu.ch
port: 443
protocol: https
- name: drawio
domain: draw.gymb.souveredu.ch
# No internal FQDN/cert for drawio yet — proxy by IP. Combined
# with serversTransport `insecureSkipVerify` (handled by the
# selfsigned-mode branch in the template), or accept the route's
# 500 until the cert is wired up.
port: 443
protocol: https

View file

@ -1,28 +1,22 @@
---
traefik_mode: dmz
traefik_dmz_exposed_services:
- name: authentik
domain: auth.gymb.souveredu.ch
port: 443
protocol: https
- name: nextcloud
domain: cloud.gymb.souveredu.ch
port: 443
protocol: https
- name: collabora
domain: office.gymb.souveredu.ch
port: 443
protocol: https
- name: drawio
domain: draw.gymb.souveredu.ch
port: 443
protocol: https
- name: garage-webui
domain: console.s3.gymb.souveredu.ch
port: 443
protocol: https
- name: garage-s3
domain: s3.gymb.souveredu.ch
port: 443
protocol: https
# The DMZ traefik discovers which services to expose by reading
# traefik_dmz_exposed_services from each backend host's host_vars
# (application/traefik.yml, storage/traefik.yml). See the role's
# tasks/main.yml — set_fact "Build service registry from backend
# servers (DMZ mode)".
# From the DMZ network the public ns1 IP (193.43.183.169) is not
# reachable on port 53, but the internal address (172.16.9.169) is.
# Override the group-level traefik_acme_dns_nameserver from bao so
# lego's RFC2136 updates land at the internal interface. The TSIG
# key/secret are the same; only the transport target changes.
traefik_acme_dns_nameserver: "172.16.9.169"
# Lego's propagation check normally polls the NS hostnames listed in
# the zone's SOA (ns1.digitalboard.ch.) — which resolves to the
# public IP that's unreachable from this DMZ host. Skip that check;
# lego still polls via the resolver above before asking LE to
# validate.
traefik_acme_disable_ans_checks: true

View file

@ -3,7 +3,12 @@
# rpc_secret, admin_token, metrics_token, webui_password
_garage: "{{ lookup('community.hashi_vault.hashi_vault', vault_mount + '/data/garage', url=vault_addr) }}"
garage_s3_domain: "s3.gymb.souveredu.ch"
# First entry is the canonical public S3 FQDN. Additional entries
# cover internal *.int.* names so server-to-server S3 traffic (e.g.
# nextcloud → garage) stays in the LAN.
garage_s3_domains:
- "s3.gymb.souveredu.ch"
- "s3.int.gymb.souveredu.ch"
garage_webui_domain: "console.s3.gymb.souveredu.ch"
garage_use_ssl: true
garage_webui_enabled: true

View file

@ -0,0 +1,16 @@
---
# Services hosted on `storage` that the DMZ reverseproxy should forward
# public traffic to. See application/traefik.yml for the mechanism.
traefik_dmz_exposed_services:
- name: garage-s3
domain: s3.gymb.souveredu.ch
backend_host: s3.int.gymb.souveredu.ch
port: 443
protocol: https
- name: garage-webui
domain: console.s3.gymb.souveredu.ch
# No internal FQDN/cert SAN for console.s3 yet — would need an
# extra_domain on garage-webui. Until then this route will 500
# against the storage backend (cert mismatch on raw IP).
port: 443
protocol: https