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 bb64ccf71e
commit 518d80ec71
No known key found for this signature in database
GPG key ID: 63DE20495932047A
17 changed files with 309 additions and 37 deletions

View file

@ -6,6 +6,12 @@ services:
image: {{ opnform_api_image }}
container_name: opnform-api
restart: unless-stopped
{% if opnform_extra_hosts | length > 0 %}
extra_hosts:
{% for host in opnform_extra_hosts %}
- "{{ host }}"
{% endfor %}
{% endif %}
volumes:
- {{ opnform_storage_dir }}:/usr/share/nginx/html/storage:rw
environment: &api-env
@ -14,6 +20,9 @@ services:
APP_URL: "{{ opnform_base_url }}"
APP_DEBUG: "false"
SELF_HOSTED: "true"
{% if opnform_oidc_enabled and (_opnform_force_login_effective | default(false)) %}
OIDC_FORCE_LOGIN: "true"
{% endif %}
LOG_CHANNEL: errorlog
LOG_LEVEL: info
@ -173,10 +182,13 @@ services:
labels:
- traefik.enable=true
- traefik.docker.network={{ opnform_traefik_network }}
- traefik.http.routers.{{ opnform_service_name }}.rule=Host(`{{ opnform_domain }}`)
- traefik.http.routers.{{ opnform_service_name }}.rule={% set _all_domains = [opnform_domain] + (opnform_extra_domains | default([])) %}{% for d in _all_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor +%}
{% if opnform_use_ssl %}
- traefik.http.routers.{{ opnform_service_name }}.entrypoints=websecure
- traefik.http.routers.{{ opnform_service_name }}.tls=true
{% if traefik_cert_mode | default('selfsigned') == 'acme' %}
- traefik.http.routers.{{ opnform_service_name }}.tls.certresolver={{ traefik_ssl_cert_resolver | default('dns') }}
{% endif %}
{% else %}
- traefik.http.routers.{{ opnform_service_name }}.entrypoints=web
{% endif %}