feat(services): multi-domain routing, split-horizon and OIDC hardening

Bundle of cross-role changes for the gymb services deployment:

- Traefik routers: OR-combine opnform/homarr/bookstack Host rules with new
  *_extra_domains (internal *.int.* FQDNs for a DMZ reverseproxy), and emit
  tls.certresolver only when traefik_cert_mode == acme (drawio, homarr,
  opnform, send).
- Split-horizon: bookstack_extra_hosts / opnform_extra_hosts add container
  /etc/hosts overrides so containers reach the IdP public FQDN over the LAN.
- bookstack: assert the OIDC issuer resolves concretely (reject "//v2.0"),
  allowing non-Entra IdPs that override bookstack_oidc_issuer.
- homarr: derive the bcrypt salt from the password digest so the admin hash
  is idempotent — no spurious template changes / container restarts.
- opnform: PATCH an existing OIDC connection instead of skipping (applies
  corrected inventory on re-run); add OIDC_FORCE_LOGIN (enabled only after
  bootstrap) and an optional direct-SSO ingress entrypoint.

Docs: READMEs and meta/argument_specs.yml updated for all new variables.
This commit is contained in:
Simon Bärlocher 2026-05-27 16:18:29 +02:00
parent 1dcff92240
commit 19864d79b2
No known key found for this signature in database
GPG key ID: 63DE20495932047A
17 changed files with 309 additions and 37 deletions

View file

@ -16,6 +16,15 @@ opnform_redis_data_dir: "{{ opnform_docker_volume_dir }}/redis"
# Service configuration
opnform_domain: "forms.local.test"
# Additional hostnames the opnform router answers on (e.g. an internal
# *.int.* FQDN so a DMZ reverseproxy can hit a backend hostname covered
# by the cert).
opnform_extra_domains: []
# Container-level /etc/hosts overrides for the API containers — needed in
# split-horizon setups where the OpnForm API must reach the IdP's public
# FQDN (used in the OIDC discovery/iss claim) over the LAN rather than
# hairpinning through a DMZ that has no NAT loopback to its own public IP.
opnform_extra_hosts: []
opnform_base_url: "https://forms.local.test"
# Images
@ -92,6 +101,12 @@ opnform_oidc_slug: "oidc"
# with @example.com emails are redirected to the IdP). Required when
# opnform_oidc_enabled is true.
opnform_oidc_domain: ""
# When true, sets OIDC_FORCE_LOGIN on the api: password-based login is
# disabled entirely and every user must authenticate via OIDC. Only
# rendered when opnform_oidc_enabled is also true. Make sure all real
# users have addresses under opnform_oidc_domain before enabling — there
# is no password fallback once this is on.
opnform_oidc_force_login: false
opnform_oidc_scopes:
- openid
- profile
@ -104,6 +119,17 @@ opnform_oidc_admin_group: "opnform-admins"
# var. Each item: {idp_group: "<group name>", role: "owner|admin|editor|member"}
opnform_oidc_group_role_mappings: []
# Direct-SSO entrypoint. OpnForm has no built-in way to skip the email
# login form and jump straight to the IdP (verified: config/oidc.php only
# exposes force_login; the login form always routes by email domain). When
# this is enabled the ingress serves a tiny page at opnform_oidc_sso_path
# that calls OpnForm's /api/auth/{slug}/redirect endpoint (which performs
# no domain check) and forwards the browser to the returned authorize URL
# — nonce/state included. Link users to https://<domain><sso_path> instead
# of /login. Requires opnform_oidc_enabled.
opnform_oidc_sso_entrypoint: false
opnform_oidc_sso_path: "/sso"
# Traefik configuration
opnform_traefik_network: "proxy"
opnform_use_ssl: true