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

@ -15,6 +15,17 @@ server {
index index.html index.htm index.php;
{% if opnform_oidc_enabled and opnform_oidc_sso_entrypoint %}
# Direct-SSO entrypoint: a tiny page that asks the API for the IdP
# authorize URL (no email/domain check on this endpoint) and forwards
# the browser there. Link users here instead of /login to skip the
# email field entirely. Exact-match so it wins over the `/` prefix.
location = {{ opnform_oidc_sso_path }} {
default_type text/html;
return 200 '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Redirecting to sign-in…</title></head><body style="font-family:sans-serif;text-align:center;padding:3rem;color:#374151"><p id="m">Redirecting to sign-in…</p><script>fetch("/api/auth/{{ opnform_oidc_slug }}/redirect",{method:"POST",headers:{Accept:"application/json"}}).then(function(r){if(!r.ok)throw new Error("HTTP "+r.status);return r.json()}).then(function(d){if(d&&d.redirect_url){window.location.replace(d.redirect_url)}else{throw new Error("no redirect_url")}}).catch(function(e){document.getElementById("m").textContent="Sign-in redirect failed: "+e.message+". Go to the login page instead.";var a=document.createElement("a");a.href="/login";a.textContent="Open login page";a.style.display="block";a.style.marginTop="1rem";document.body.appendChild(a)});</script></body></html>';
}
{% endif %}
location / {
proxy_http_version 1.1;
proxy_pass http://ui:3000;