From 2104e5fe7da83bed0c79f1ba10b09ee9fcf62418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20B=C3=A4rlocher?= Date: Wed, 20 May 2026 22:44:41 +0200 Subject: [PATCH] feat: drop blanket recreates, ACME-DNS knobs, notify_push override - Drop `recreate: always` from collabora/drawio/homarr/opencloud/traefik handlers and the authentik_outpost_ldap start task. `up -d` with `state: present` already recreates exactly the services whose compose definition changed; the blanket recreate was forcing restarts even when nothing relevant moved. - Rewrite the `*_domains` Traefik Host loop to the `Host(\`a\`) || Host(\`b\`)` form across authentik/collabora/garage/nextcloud so the rule still matches when traefik can't normalize the comma-form into the same canonical shape. - Traefik: add `traefik_acme_tcp_only` (sets LEGO_EXPERIMENTAL_DNS_TCP_ONLY) and `traefik_acme_disable_ans_checks` (disables lego's authoritative-NS propagation check) for environments where the DNS path between the traefik container and the zone's nameservers is constrained. - Traefik DMZ collector: two-step merge so a `traefik_dmz_exposed_services` entry that sets its own `backend_host` wins over the host fallback; lets a route target an internal FQDN covered by the backend cert's SANs instead of the raw IP. - Nextcloud: add `nextcloud_notify_push_domain` override for the `occ notify_push:setup` call so the setup check can hit an internal FQDN instead of hairpinning through the DMZ. Push router now matches every entry in `nextcloud_domains`. - Nextcloud: also %2F-escape slashes in the postgres user/password inside the notify_push DATABASE_URL. --- roles/authentik/templates/docker-compose.yml.j2 | 2 +- roles/collabora/handlers/main.yml | 2 +- roles/collabora/templates/docker-compose.yml.j2 | 2 +- roles/drawio/handlers/main.yml | 2 +- roles/garage/templates/docker-compose.yml.j2 | 2 +- roles/homarr/handlers/main.yml | 2 +- roles/nextcloud/defaults/main.yml | 6 ++++++ roles/nextcloud/tasks/notify_push.yml | 2 +- roles/nextcloud/templates/docker-compose.yml.j2 | 6 +++--- roles/opencloud/handlers/main.yml | 2 +- roles/traefik/defaults/main.yml | 12 ++++++++++++ roles/traefik/handlers/main.yml | 2 +- roles/traefik/tasks/main.yml | 13 ++++++++++++- roles/traefik/templates/docker-compose.yml.j2 | 3 +++ roles/traefik/templates/traefik.yml.j2 | 4 ++++ 15 files changed, 49 insertions(+), 13 deletions(-) diff --git a/roles/authentik/templates/docker-compose.yml.j2 b/roles/authentik/templates/docker-compose.yml.j2 index f4a1f95..f0193ec 100644 --- a/roles/authentik/templates/docker-compose.yml.j2 +++ b/roles/authentik/templates/docker-compose.yml.j2 @@ -48,7 +48,7 @@ services: labels: - traefik.enable=true - traefik.docker.network={{ authentik_traefik_network }} - - traefik.http.routers.{{ authentik_service_name }}.rule=Host({% for d in authentik_domains %}`{{ d }}`{% if not loop.last %}, {% endif %}{% endfor %}) + - traefik.http.routers.{{ authentik_service_name }}.rule={% for d in authentik_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor +%} {% if authentik_use_ssl %} - traefik.http.routers.{{ authentik_service_name }}.entrypoints=websecure - traefik.http.routers.{{ authentik_service_name }}.tls=true diff --git a/roles/collabora/handlers/main.yml b/roles/collabora/handlers/main.yml index bfd2b02..3364bcc 100644 --- a/roles/collabora/handlers/main.yml +++ b/roles/collabora/handlers/main.yml @@ -5,4 +5,4 @@ - name: restart collabora community.docker.docker_compose_v2: project_src: "{{ collabora_docker_compose_dir }}" - state: restarted \ No newline at end of file + state: present \ No newline at end of file diff --git a/roles/collabora/templates/docker-compose.yml.j2 b/roles/collabora/templates/docker-compose.yml.j2 index af6ecfc..353a08b 100644 --- a/roles/collabora/templates/docker-compose.yml.j2 +++ b/roles/collabora/templates/docker-compose.yml.j2 @@ -20,7 +20,7 @@ services: labels: - traefik.enable=true - traefik.docker.network={{ collabora_traefik_network }} - - traefik.http.routers.{{ collabora_service_name }}.rule=Host({% for d in collabora_domains %}`{{ d }}`{% if not loop.last %}, {% endif %}{% endfor %}) + - traefik.http.routers.{{ collabora_service_name }}.rule={% for d in collabora_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor +%} - traefik.http.services.{{ collabora_service_name }}.loadbalancer.server.port={{ collabora_port }} {% if collabora_use_ssl %} - traefik.http.routers.{{ collabora_service_name }}.entrypoints=websecure diff --git a/roles/drawio/handlers/main.yml b/roles/drawio/handlers/main.yml index f1ef0da..b68633d 100644 --- a/roles/drawio/handlers/main.yml +++ b/roles/drawio/handlers/main.yml @@ -5,4 +5,4 @@ - name: restart drawio community.docker.docker_compose_v2: project_src: "{{ drawio_docker_compose_dir }}" - state: restarted \ No newline at end of file + state: present \ No newline at end of file diff --git a/roles/garage/templates/docker-compose.yml.j2 b/roles/garage/templates/docker-compose.yml.j2 index 0427fb2..d344e5f 100644 --- a/roles/garage/templates/docker-compose.yml.j2 +++ b/roles/garage/templates/docker-compose.yml.j2 @@ -14,7 +14,7 @@ services: - traefik.enable=true - traefik.docker.network={{ garage_traefik_network }} # S3 API endpoint - - traefik.http.routers.{{ garage_service_name }}.rule=Host({% for d in garage_s3_domains %}`{{ d }}`{% if not loop.last %}, {% endif %}{% endfor %}) + - traefik.http.routers.{{ garage_service_name }}.rule={% for d in garage_s3_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor +%} {% if garage_use_ssl %} - traefik.http.routers.{{ garage_service_name }}.entrypoints=websecure - traefik.http.routers.{{ garage_service_name }}.tls=true diff --git a/roles/homarr/handlers/main.yml b/roles/homarr/handlers/main.yml index dedb949..da74ed4 100644 --- a/roles/homarr/handlers/main.yml +++ b/roles/homarr/handlers/main.yml @@ -5,4 +5,4 @@ - name: restart homarr community.docker.docker_compose_v2: project_src: "{{ homarr_docker_compose_dir }}" - state: restarted \ No newline at end of file + state: present \ No newline at end of file diff --git a/roles/nextcloud/defaults/main.yml b/roles/nextcloud/defaults/main.yml index c289d96..a5decb1 100644 --- a/roles/nextcloud/defaults/main.yml +++ b/roles/nextcloud/defaults/main.yml @@ -65,6 +65,12 @@ nextcloud_trusted_proxies: "172.16.0.0/12" # File locking and real-time push notifications nextcloud_enable_notify_push: false nextcloud_notify_push_image: "icewind1991/notify_push:1.3.1" +# Domain used when calling `occ notify_push:setup`. Defaults to the +# first nextcloud_domains entry (the canonical public name). Override +# with an internal FQDN to avoid hairpinning the setup check through +# the DMZ; the FQDN must also be in nextcloud_domains so the push +# router matches it. +# nextcloud_notify_push_domain: "cloud.int.example.com" # Nextcloud Talk: register external HPB signaling + TURN + STUN # Set to true to run tasks/talk.yml after Nextcloud is up. diff --git a/roles/nextcloud/tasks/notify_push.yml b/roles/nextcloud/tasks/notify_push.yml index ccf2b72..1497c68 100644 --- a/roles/nextcloud/tasks/notify_push.yml +++ b/roles/nextcloud/tasks/notify_push.yml @@ -5,4 +5,4 @@ - name: Configure notify_push base endpoint community.docker.docker_container_exec: container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1" - command: php /var/www/html/occ notify_push:setup https://{{ nextcloud_domains[0] }}/push \ No newline at end of file + command: php /var/www/html/occ notify_push:setup https://{{ nextcloud_notify_push_domain | default(nextcloud_domains[0]) }}/push \ No newline at end of file diff --git a/roles/nextcloud/templates/docker-compose.yml.j2 b/roles/nextcloud/templates/docker-compose.yml.j2 index 0e05090..365a766 100644 --- a/roles/nextcloud/templates/docker-compose.yml.j2 +++ b/roles/nextcloud/templates/docker-compose.yml.j2 @@ -35,7 +35,7 @@ services: labels: - traefik.enable=true - traefik.docker.network={{ nextcloud_traefik_network }} - - traefik.http.routers.{{ nextcloud_service_name }}.rule=Host({% for d in nextcloud_domains %}`{{ d }}`{% if not loop.last %}, {% endif %}{% endfor %}) + - traefik.http.routers.{{ nextcloud_service_name }}.rule={% for d in nextcloud_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor +%} {% if nextcloud_use_ssl %} - traefik.http.routers.{{ nextcloud_service_name }}.entrypoints=websecure - traefik.http.routers.{{ nextcloud_service_name }}.tls=true @@ -136,7 +136,7 @@ services: environment: PORT: "7867" REDIS_URL: "redis://redis:6379" - DATABASE_URL: "postgres://{{ nextcloud_postgres_user | urlencode }}:{{ nextcloud_postgres_password | urlencode }}@db:5432/{{ nextcloud_postgres_db }}" + DATABASE_URL: "postgres://{{ nextcloud_postgres_user | urlencode | replace('/', '%2F') }}:{{ nextcloud_postgres_password | urlencode | replace('/', '%2F') }}@db:5432/{{ nextcloud_postgres_db }}" DATABASE_PREFIX: "oc_" NEXTCLOUD_URL: "http://nginx" networks: @@ -145,7 +145,7 @@ services: labels: - traefik.enable=true - traefik.docker.network={{ nextcloud_traefik_network }} - - traefik.http.routers.{{ nextcloud_service_name }}-push.rule=Host(`{{ nextcloud_domains[0] }}`) && PathPrefix(`/push`) + - traefik.http.routers.{{ nextcloud_service_name }}-push.rule=({% for d in nextcloud_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor %}) && PathPrefix(`/push`) - traefik.http.services.{{ nextcloud_service_name }}-push.loadbalancer.server.port=7867 {% if nextcloud_use_ssl %} - traefik.http.routers.{{ nextcloud_service_name }}-push.entrypoints=websecure diff --git a/roles/opencloud/handlers/main.yml b/roles/opencloud/handlers/main.yml index 95b6986..7cbc094 100644 --- a/roles/opencloud/handlers/main.yml +++ b/roles/opencloud/handlers/main.yml @@ -5,4 +5,4 @@ - name: restart opencloud community.docker.docker_compose_v2: project_src: "{{ opencloud_docker_compose_dir }}" - state: restarted \ No newline at end of file + state: present \ No newline at end of file diff --git a/roles/traefik/defaults/main.yml b/roles/traefik/defaults/main.yml index 3e43412..eea7391 100644 --- a/roles/traefik/defaults/main.yml +++ b/roles/traefik/defaults/main.yml @@ -33,6 +33,18 @@ traefik_acme_tsig_secret: "" # TSIG secret traefik_acme_propagation_timeout: "120" traefik_acme_polling_interval: "2" traefik_acme_ttl: "60" +# Force lego's DNS lookups (SOA resolution, propagation checks) onto +# TCP instead of UDP. Useful when container egress can reach the +# nameserver on TCP/53 but UDP/53 is blocked or unreliable. Sets the +# upstream env var LEGO_EXPERIMENTAL_DNS_TCP_ONLY=true on the +# traefik container. +traefik_acme_tcp_only: false +# Disable lego's propagation check against the zone's authoritative +# nameservers. Use when the SOA-listed NS hostname resolves to an +# address that isn't reachable from this traefik host (e.g. a DMZ +# box that can only see the internal NS IP, not the public one). +# lego still polls via the configured `resolvers:` list. +traefik_acme_disable_ans_checks: false # Self-signed certificate configuration (for vagrant/testing) traefik_selfsigned_cert_dir: "{{ docker_volume_dir }}/certs" diff --git a/roles/traefik/handlers/main.yml b/roles/traefik/handlers/main.yml index 4abd95a..8af7aac 100644 --- a/roles/traefik/handlers/main.yml +++ b/roles/traefik/handlers/main.yml @@ -5,4 +5,4 @@ - name: restart traefik community.docker.docker_compose_v2: project_src: "{{ docker_compose_dir }}" - state: restarted + state: present diff --git a/roles/traefik/tasks/main.yml b/roles/traefik/tasks/main.yml index 159ba8f..8934037 100644 --- a/roles/traefik/tasks/main.yml +++ b/roles/traefik/tasks/main.yml @@ -9,7 +9,18 @@ - name: Build service registry from backend servers (DMZ mode) set_fact: - proxied_services: "{{ proxied_services | default([]) + hostvars[item].traefik_dmz_exposed_services | default([]) | map('combine', {'backend_host': hostvars[item].ansible_host | default(item)}) | list }}" + # Two-step merge so a service entry's own `backend_host` wins: + # entries that set it pass through unchanged, entries that don't + # get the backend host's ansible_host as fallback. The override + # lets a route target an internal FQDN covered by the backend + # cert's SANs instead of the raw IP (which would fail backend + # TLS verification at the proxy hop). + proxied_services: >- + {{ + proxied_services | default([]) + + (hostvars[item].traefik_dmz_exposed_services | default([]) | selectattr('backend_host', 'defined') | list) + + (hostvars[item].traefik_dmz_exposed_services | default([]) | rejectattr('backend_host', 'defined') | map('combine', {'backend_host': hostvars[item].ansible_host | default(item)}) | list) + }} loop: "{{ _backend_servers | default([]) }}" when: traefik_mode == 'dmz' diff --git a/roles/traefik/templates/docker-compose.yml.j2 b/roles/traefik/templates/docker-compose.yml.j2 index e45578b..6dbb9ec 100644 --- a/roles/traefik/templates/docker-compose.yml.j2 +++ b/roles/traefik/templates/docker-compose.yml.j2 @@ -12,6 +12,9 @@ services: RFC2136_PROPAGATION_TIMEOUT: "{{ traefik_acme_propagation_timeout }}" RFC2136_POLLING_INTERVAL: "{{ traefik_acme_polling_interval }}" RFC2136_TTL: "{{ traefik_acme_ttl }}" +{% if traefik_acme_tcp_only | default(false) %} + LEGO_EXPERIMENTAL_DNS_TCP_ONLY: "true" +{% endif %} {% endif %} ports: - "80:80" diff --git a/roles/traefik/templates/traefik.yml.j2 b/roles/traefik/templates/traefik.yml.j2 index 64bf351..1900bbb 100644 --- a/roles/traefik/templates/traefik.yml.j2 +++ b/roles/traefik/templates/traefik.yml.j2 @@ -48,6 +48,10 @@ certificatesResolvers: provider: rfc2136 resolvers: - "{{ traefik_acme_dns_nameserver }}" +{% if traefik_acme_disable_ans_checks | default(false) %} + propagation: + disableANSChecks: true +{% endif %} {% endif %} {% if traefik_use_ssl %}