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

@ -46,6 +46,7 @@ See `defaults/main.yml` for the full list. Most useful overrides:
| Variable | Default | Purpose |
|---|---|---|
| `homarr_domain` | `homarr.local.test` | Traefik Host rule |
| `homarr_extra_domains` | `[]` | Extra Host-rule hostnames (OR-combined), e.g. internal `*.int.*` FQDN |
| `homarr_base_url` | `https://home.local.test` | NEXTAUTH_URL / BASE_URL |
| `homarr_auth_providers` | `credentials` | `credentials`, `oidc`, or both |
| `homarr_oidc_issuer` | empty | Identity provider issuer URL |

View file

@ -15,6 +15,10 @@ homarr_db: "{{ homarr_appdata_dir }}/db/db.sqlite"
# Service configuration
homarr_domain: "homarr.local.test"
# Additional hostnames the homarr router answers on (e.g. an internal
# *.int.* FQDN so a DMZ reverseproxy can hit a backend hostname covered
# by the cert).
homarr_extra_domains: []
homarr_image: "ghcr.io/homarr-labs/homarr:latest"
homarr_port: 7575
homarr_use_docker: false

View file

@ -112,19 +112,17 @@
# =====================================================================
- name: Generate bcrypt hash for admin password
ansible.builtin.shell:
cmd: python3 -c "import bcrypt, sys; print(bcrypt.hashpw(sys.stdin.read().encode(), bcrypt.gensalt(rounds=10)).decode())"
stdin: "{{ homarr_admin_password }}"
stdin_add_newline: false
delegate_to: localhost
become: false
register: bcrypt_result
changed_when: false
no_log: true
- name: Set bcrypt hash fact
ansible.builtin.set_fact:
homarr_bcrypt_hash: "{{ bcrypt_result.stdout }}"
# Deterministic salt derived from the password's SHA-256 digest so the
# hash stays stable across runs (idempotent — no spurious template
# changes / container restarts when the password is unchanged). The
# bcrypt salt alphabet is [./A-Za-z0-9]; the digest's hex chars are
# a strict subset, so we just take the first 22.
homarr_bcrypt_hash: >-
{{ homarr_admin_password
| password_hash('bcrypt', rounds=10,
salt=(homarr_admin_password
| hash('sha256'))[:22]) }}
no_log: true
# =====================================================================
@ -161,4 +159,4 @@
register: seed_result
changed_when: seed_result.rc == 0
when: admin_exists.stdout == ""
notify: restart homarr
notify: restart homarr

View file

@ -29,10 +29,13 @@ services:
labels:
- traefik.enable=true
- traefik.docker.network={{ homarr_traefik_network }}
- traefik.http.routers.homarr.rule=Host(`{{ homarr_domain }}`)
- traefik.http.routers.homarr.rule={% set _all_domains = [homarr_domain] + (homarr_extra_domains | default([])) %}{% for d in _all_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor +%}
{% if homarr_use_ssl %}
- traefik.http.routers.homarr.entrypoints=websecure
- traefik.http.routers.homarr.tls=true
{% if traefik_cert_mode | default('selfsigned') == 'acme' %}
- traefik.http.routers.homarr.tls.certresolver={{ traefik_ssl_cert_resolver | default('dns') }}
{% endif %}
{% else %}
- traefik.http.routers.homarr.entrypoints=web
{% endif %}