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:
parent
bb64ccf71e
commit
518d80ec71
17 changed files with 309 additions and 37 deletions
|
|
@ -76,6 +76,15 @@
|
|||
mode: '0644'
|
||||
notify: restart opnform
|
||||
|
||||
# OIDC_FORCE_LOGIN disables OpnForm's password login — including the
|
||||
# password-based admin/OIDC bootstrap this role performs below. So the
|
||||
# first compose render always keeps force-login OFF; it is switched on
|
||||
# only after the bootstrap completes (see step 7). This keeps a first
|
||||
# deploy on a fresh host working even when opnform_oidc_force_login=true.
|
||||
- name: Render compose with force-login disabled during bootstrap
|
||||
ansible.builtin.set_fact:
|
||||
_opnform_force_login_effective: false
|
||||
|
||||
- name: Deploy docker-compose file
|
||||
ansible.builtin.template:
|
||||
src: docker-compose.yml.j2
|
||||
|
|
@ -155,9 +164,12 @@
|
|||
# =====================================================================
|
||||
# 6. OIDC IDENTITY CONNECTION (optional)
|
||||
# =====================================================================
|
||||
# Creates a single OIDC connection on the admin's default workspace.
|
||||
# OpnForm enforces one OIDC connection per workspace, so this block is
|
||||
# idempotent: we GET existing connections first and skip if any exists.
|
||||
# Provisions a single OIDC connection on the admin's default workspace.
|
||||
# OpnForm enforces one OIDC connection per workspace, so we GET the
|
||||
# existing connections first and then either POST a new one or PATCH the
|
||||
# existing one to the desired state. PATCHing (rather than skipping when
|
||||
# one exists) keeps inventory changes — e.g. a corrected issuer — applied
|
||||
# on re-runs instead of leaving stale values in the DB forever.
|
||||
|
||||
- name: Log in as admin to obtain OIDC API token
|
||||
ansible.builtin.uri:
|
||||
|
|
@ -213,15 +225,12 @@
|
|||
}}
|
||||
when: opnform_oidc_enabled | bool
|
||||
|
||||
- name: Create OIDC identity connection
|
||||
ansible.builtin.uri:
|
||||
url: "https://127.0.0.1/api/open/workspaces/{{ opnform_workspaces.json[0].id }}/oidc-connections"
|
||||
method: POST
|
||||
headers:
|
||||
Host: "{{ opnform_domain }}"
|
||||
Authorization: "Bearer {{ opnform_oidc_token.json.token }}"
|
||||
body_format: json
|
||||
body:
|
||||
# Desired connection state shared by both the create (POST) and update
|
||||
# (PATCH) calls below. client_secret is always sent: OpnForm's update
|
||||
# endpoint only persists it when present, and on create it is required.
|
||||
- name: Build desired OIDC connection body
|
||||
ansible.builtin.set_fact:
|
||||
_opnform_oidc_body:
|
||||
name: "{{ opnform_oidc_client_name }}"
|
||||
slug: "{{ opnform_oidc_slug }}"
|
||||
domain: "{{ opnform_oidc_domain }}"
|
||||
|
|
@ -233,6 +242,18 @@
|
|||
options:
|
||||
require_state: true
|
||||
group_role_mappings: "{{ _opnform_oidc_group_role_mappings }}"
|
||||
no_log: true
|
||||
when: opnform_oidc_enabled | bool
|
||||
|
||||
- name: Create OIDC identity connection
|
||||
ansible.builtin.uri:
|
||||
url: "https://127.0.0.1/api/open/workspaces/{{ opnform_workspaces.json[0].id }}/oidc-connections"
|
||||
method: POST
|
||||
headers:
|
||||
Host: "{{ opnform_domain }}"
|
||||
Authorization: "Bearer {{ opnform_oidc_token.json.token }}"
|
||||
body_format: json
|
||||
body: "{{ _opnform_oidc_body }}"
|
||||
status_code: [201]
|
||||
validate_certs: false
|
||||
no_log: true
|
||||
|
|
@ -240,6 +261,58 @@
|
|||
- opnform_oidc_enabled | bool
|
||||
- opnform_existing_oidc.json | length == 0
|
||||
|
||||
# An OIDC connection already exists: PATCH it to the desired state so
|
||||
# inventory changes (e.g. a corrected issuer) are applied. OpnForm allows
|
||||
# exactly one connection per workspace, so the first entry is ours.
|
||||
- name: Update existing OIDC identity connection
|
||||
ansible.builtin.uri:
|
||||
url: >-
|
||||
https://127.0.0.1/api/open/workspaces/{{ opnform_workspaces.json[0].id }}/oidc-connections/{{ opnform_existing_oidc.json[0].id }}
|
||||
method: PATCH
|
||||
headers:
|
||||
Host: "{{ opnform_domain }}"
|
||||
Authorization: "Bearer {{ opnform_oidc_token.json.token }}"
|
||||
body_format: json
|
||||
body: "{{ _opnform_oidc_body }}"
|
||||
status_code: [200]
|
||||
validate_certs: false
|
||||
no_log: true
|
||||
when:
|
||||
- opnform_oidc_enabled | bool
|
||||
- opnform_existing_oidc.json | length > 0
|
||||
|
||||
# =====================================================================
|
||||
# 7. ENABLE FORCE LOGIN (optional, must run last)
|
||||
# =====================================================================
|
||||
# OIDC_FORCE_LOGIN disables password login — including the password-based
|
||||
# admin/OIDC bootstrap above — so it is switched on only now, after the
|
||||
# connection is provisioned. OpnForm itself only enforces force-login when
|
||||
# an enabled OIDC connection exists, so the order matters: connection
|
||||
# first, force-login second.
|
||||
- name: Enable force login now that the OIDC connection exists
|
||||
when:
|
||||
- opnform_oidc_enabled | bool
|
||||
- opnform_oidc_force_login | bool
|
||||
block:
|
||||
- name: Re-render compose with force-login enabled
|
||||
ansible.builtin.set_fact:
|
||||
_opnform_force_login_effective: true
|
||||
|
||||
- name: Deploy docker-compose file with force-login enabled
|
||||
ansible.builtin.template:
|
||||
src: docker-compose.yml.j2
|
||||
dest: "{{ opnform_docker_compose_dir }}/docker-compose.yml"
|
||||
mode: '0644'
|
||||
register: _opnform_force_login_compose
|
||||
|
||||
- name: Apply force-login by recreating the api containers
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ opnform_docker_compose_dir }}"
|
||||
state: present
|
||||
wait: true
|
||||
wait_timeout: 180
|
||||
when: _opnform_force_login_compose is changed
|
||||
|
||||
- name: Display deployment info
|
||||
ansible.builtin.debug:
|
||||
msg: |-
|
||||
|
|
@ -260,6 +333,10 @@
|
|||
(slug: {{ opnform_oidc_slug }}, domain: {{ opnform_oidc_domain }})
|
||||
Users with @{{ opnform_oidc_domain }} addresses will be
|
||||
redirected to {{ opnform_oidc_issuer }} on login.
|
||||
{% if opnform_oidc_sso_entrypoint %}
|
||||
Direct-SSO entrypoint: {{ opnform_base_url }}{{ opnform_oidc_sso_path }}
|
||||
(link users here to skip the email login form)
|
||||
{% endif %}
|
||||
{% else %}
|
||||
OIDC: disabled (set opnform_oidc_enabled=true to auto-configure)
|
||||
{% endif %}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue