From 7d150fa021521a7b299fc947c8549c9ef843870c Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Fri, 19 Sep 2025 11:22:51 +0200 Subject: [PATCH] =?UTF-8?q?DNS=20&=20certs=20refactor:=20-=20Switch=20cert?= =?UTF-8?q?bot=20flag=20from=20MODE=5FTEST=20=E2=86=92=20MODE=5FDUMMY=20in?= =?UTF-8?q?=20dedicated=20certs=20-=20Add=20sys-svc-dns=20defaults=20for?= =?UTF-8?q?=20CLOUDFLARE=5FNAMESERVERS=20-=20Introduce=2002=5Fnameservers.?= =?UTF-8?q?yml=20for=20NS=20cleanup=20+=20enforce,=20adjust=20task=20order?= =?UTF-8?q?ing=20(apex=20now=2003=5Fapex.yml)=20-=20Enforce=20quoting=20fo?= =?UTF-8?q?r=20Bluesky=20and=20Mailu=20TXT=20records=20-=20Add=20cleanup?= =?UTF-8?q?=20of=20MX/TXT/DMARC/DKIM=20in=20Mailu=20role=20-=20Normalize?= =?UTF-8?q?=20no=5Flog=20handling=20in=20Nextcloud=20plugin=20-=20Simplify?= =?UTF-8?q?=20async=20conditionals=20in=20Collabora=20role=20Conversation:?= =?UTF-8?q?=20https://chatgpt.com/share/68cd20d8-9ba8-800f-b070-f7294f072c?= =?UTF-8?q?40?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sys-svc-certs/tasks/flavors/dedicated.yml | 2 +- roles/sys-svc-dns/defaults/main.yml | 1 + roles/sys-svc-dns/tasks/01_core.yml | 9 ++- roles/sys-svc-dns/tasks/02_nameservers.yml | 60 +++++++++++++++++++ .../tasks/{02_apex.yml => 03_apex.yml} | 0 roles/web-app-bluesky/tasks/03_dns.yml | 2 +- roles/web-app-mailu/tasks/05_dns-records.yml | 58 ++++++++++++++---- .../tasks/_plugin_b_enable_and_configure.yml | 2 +- roles/web-svc-collabora/tasks/01_core.yml | 4 +- 9 files changed, 121 insertions(+), 17 deletions(-) create mode 100644 roles/sys-svc-dns/defaults/main.yml create mode 100644 roles/sys-svc-dns/tasks/02_nameservers.yml rename roles/sys-svc-dns/tasks/{02_apex.yml => 03_apex.yml} (100%) diff --git a/roles/sys-svc-certs/tasks/flavors/dedicated.yml b/roles/sys-svc-certs/tasks/flavors/dedicated.yml index d3c0ee10..5787f21e 100644 --- a/roles/sys-svc-certs/tasks/flavors/dedicated.yml +++ b/roles/sys-svc-certs/tasks/flavors/dedicated.yml @@ -24,7 +24,7 @@ {% else %} -d {{ domain }} {% endif %} - {{ '--test-cert' if MODE_TEST | bool else '' }} + {{ '--test-cert' if MODE_DUMMY | bool else '' }} register: certbot_result changed_when: "'Certificate not yet due for renewal' not in certbot_result.stdout" when: not cert_check.exists \ No newline at end of file diff --git a/roles/sys-svc-dns/defaults/main.yml b/roles/sys-svc-dns/defaults/main.yml new file mode 100644 index 00000000..34cd87e0 --- /dev/null +++ b/roles/sys-svc-dns/defaults/main.yml @@ -0,0 +1 @@ +CLOUDFLARE_NAMESERVERS: [] # Cloudflare Nameservers for NS records \ No newline at end of file diff --git a/roles/sys-svc-dns/tasks/01_core.yml b/roles/sys-svc-dns/tasks/01_core.yml index d562a9f0..53841094 100644 --- a/roles/sys-svc-dns/tasks/01_core.yml +++ b/roles/sys-svc-dns/tasks/01_core.yml @@ -5,8 +5,15 @@ The variable "CLOUDFLARE_API_TOKEN" must be defined and cannot be empty! when: (CLOUDFLARE_API_TOKEN | default('') | trim) == '' +- name: "Setup Nameservers" + include_tasks: 02_nameservers.yml + loop: "{{ SYS_SVC_DNS_BASE_DOMAINS | list }}" + loop_control: + loop_var: base_domain + label: "{{ base_domain }}" + - name: "Apply apex A/AAAA for base domains" - include_tasks: 02_apex.yml + include_tasks: 03_apex.yml loop: "{{ SYS_SVC_DNS_BASE_DOMAINS | list }}" loop_control: loop_var: base_domain diff --git a/roles/sys-svc-dns/tasks/02_nameservers.yml b/roles/sys-svc-dns/tasks/02_nameservers.yml new file mode 100644 index 00000000..e97a8da1 --- /dev/null +++ b/roles/sys-svc-dns/tasks/02_nameservers.yml @@ -0,0 +1,60 @@ +# Ensure CLOUDFLARE_NAMESERVERS is provided +- name: "Assert CLOUDFLARE_NAMESERVERS is not empty" + ansible.builtin.fail: + msg: > + CLOUDFLARE_NAMESERVERS must be a non-empty list of nameserver hostnames, + e.g. ['bob.ns.cloudflare.com', 'dina.ns.cloudflare.com']. + when: (CLOUDFLARE_NAMESERVERS | length) == 0 + +- block: + # Gather current NS records for this base domain + - name: "NS | Fetch existing NS records for {{ base_domain }}" + community.general.cloudflare_dns_info: + api_token: "{{ CLOUDFLARE_API_TOKEN }}" + zone: "{{ base_domain }}" + register: _cf_ns_info + no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}" + + # Build a list with ONLY the NS records (Cloudflare returns a mixed list) + - name: "NS | Build deletion list (all NS at apex and subdomains)" + set_fact: + _cf_ns_to_delete: >- + {{ + _cf_ns_info.records + | selectattr('type', 'equalto', 'NS') + | list + }} + + # Delete all existing NS records (exact matches) + - name: "NS | Delete existing NS records" + community.general.cloudflare_dns: + api_token: "{{ CLOUDFLARE_API_TOKEN }}" + zone: "{{ base_domain }}" + type: NS + name: "{{ item.name }}" + value: "{{ item.value | default(item.content) }}" + state: absent + ttl: 1 + loop: "{{ _cf_ns_to_delete }}" + loop_control: + label: "NS {{ item.name }} -> {{ item.value | default(item.content) }}" + no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}" + async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}" + poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}" + when: MODE_CLEANUP | bool + +# Create enforced NS set at the zone apex (@) +- name: "NS | Create NS apex set for {{ base_domain }}" + community.general.cloudflare_dns: + api_token: "{{ CLOUDFLARE_API_TOKEN }}" + zone: "{{ base_domain }}" + type: NS + name: "@" + value: "{{ item }}" + ttl: 1 + state: present + loop: "{{ CLOUDFLARE_NAMESERVERS }}" + loop_control: + label: "@ -> {{ item }}" + async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}" + poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}" diff --git a/roles/sys-svc-dns/tasks/02_apex.yml b/roles/sys-svc-dns/tasks/03_apex.yml similarity index 100% rename from roles/sys-svc-dns/tasks/02_apex.yml rename to roles/sys-svc-dns/tasks/03_apex.yml diff --git a/roles/web-app-bluesky/tasks/03_dns.yml b/roles/web-app-bluesky/tasks/03_dns.yml index 11927916..39b17264 100644 --- a/roles/web-app-bluesky/tasks/03_dns.yml +++ b/roles/web-app-bluesky/tasks/03_dns.yml @@ -39,7 +39,7 @@ - type: TXT zone: "{{ PRIMARY_DOMAIN | to_zone }}" name: "_atproto.{{ PRIMARY_DOMAIN }}" - value: "did=did:web:{{ BLUESKY_API_DOMAIN }}" + value: '"did=did:web:{{ BLUESKY_API_DOMAIN }}"' # 3) Web UI host (only if enabled) - type: A diff --git a/roles/web-app-mailu/tasks/05_dns-records.yml b/roles/web-app-mailu/tasks/05_dns-records.yml index 2684af52..9c027d44 100644 --- a/roles/web-app-mailu/tasks/05_dns-records.yml +++ b/roles/web-app-mailu/tasks/05_dns-records.yml @@ -1,22 +1,58 @@ +- block: + - name: "CLEANUP | Gather existing apex TXT/MX (and DMARC/DKIM) records" + community.general.cloudflare_dns_info: + api_token: "{{ CLOUDFLARE_API_TOKEN }}" + zone: "{{ MAILU_DOMAIN_DNS_ZONE }}" + register: cf_info + + - name: "CLEANUP | Build deletion list" + set_fact: + cf_records_to_delete: >- + {{ + cf_info.records + | selectattr('type','in',['MX','TXT']) + | selectattr('name','in', [ + MAILU_DOMAIN, + '_dmarc.' ~ MAILU_DOMAIN_DNS_ZONE, + 'dkim._domainkey.' ~ MAILU_DOMAIN_DNS_ZONE + ]) + | list + }} + + - name: "CLEANUP | Remove matched records (exact Value match)" + community.general.cloudflare_dns: + api_token: "{{ CLOUDFLARE_API_TOKEN }}" + zone: "{{ MAILU_DOMAIN_DNS_ZONE }}" + type: "{{ item.type }}" + name: "{{ item.name }}" + value: "{{ item.value | default(item.content) }}" + state: absent + loop: "{{ cf_records_to_delete }}" + loop_control: + label: "{{ item.type }} {{ item.name }} -> {{ item.value | default(item.content) }}" + when: + - DNS_PROVIDER | lower == 'cloudflare' + - MODE_CLEANUP | bool + - name: "DNS (Cloudflare) for Mailu" include_role: name: sys-dns-cloudflare-records when: DNS_PROVIDER | lower == 'cloudflare' vars: - cloudflare_async_enabled: "{{ ASYNC_ENABLED | default(false) | bool }}" - cloudflare_async_time: "{{ ASYNC_TIME | default(45) }}" - cloudflare_async_poll: "{{ ASYNC_POLL | default(5) }}" - cloudflare_no_log: "{{ MASK_CREDENTIALS_IN_LOGS | default(true) | bool }}" + cloudflare_async_enabled: "{{ ASYNC_ENABLED | bool }}" + cloudflare_async_time: "{{ ASYNC_TIME }}" + cloudflare_async_poll: "{{ ASYNC_POLL }}" + cloudflare_no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}" cloudflare_records: - { type: A, zone: "{{ MAILU_HOSTNAME_DNS_ZONE }}", name: "{{ MAILU_HOSTNAME }}", content: "{{ MAILU_IP4_PUBLIC }}", proxied: false } # - { type: AAAA, zone: "{{ MAILU_HOSTNAME_DNS_ZONE }}", name: "{{ MAILU_HOSTNAME }}", content: "{{ MAILU_IP6_PUBLIC }}", proxied: false } - - { type: CNAME, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "autoconfig.{{ MAILU_DOMAIN_DNS_ZONE }}", value: "{{ MAILU_HOSTNAME }}" } - - { type: MX, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "{{ MAILU_DOMAIN }}", value: "{{ MAILU_HOSTNAME }}", priority: 10 } - - { type: TXT, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "{{ MAILU_DOMAIN }}", value: "v=spf1 mx a:{{ MAILU_HOSTNAME }} ~all" } - - { type: TXT, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "_dmarc.{{ MAILU_DOMAIN_DNS_ZONE }}", value: "v=DMARC1; p=reject; ruf=mailto:{{ MAILU_DMARC_RUF }}; adkim=s; aspf=s" } - - { type: TXT, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "dkim._domainkey.{{ MAILU_DOMAIN_DNS_ZONE }}", value: "{{ mailu_dkim_public_key }}" } - - { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", service: "_submission", proto: "_tcp", name: "{{ MAILU_DOMAIN }}", priority: 20, weight: 1, port: 587, value: "{{ MAILU_HOSTNAME }}" } - - { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", service: "_submissions", proto: "_tcp", name: "{{ MAILU_DOMAIN }}", priority: 20, weight: 1, port: 465, value: "{{ MAILU_HOSTNAME }}" } + - { type: CNAME, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "autoconfig.{{ MAILU_DOMAIN_DNS_ZONE }}", value: "{{ MAILU_HOSTNAME }}" } + - { type: MX, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "{{ MAILU_DOMAIN }}", value: "{{ MAILU_HOSTNAME }}", priority: 10 } + - { type: TXT, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "{{ MAILU_DOMAIN }}", value: '"v=spf1 mx a:{{ MAILU_HOSTNAME }} ~all"' } + - { type: TXT, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "_dmarc.{{ MAILU_DOMAIN_DNS_ZONE }}", value: '"v=DMARC1; p=reject; ruf=mailto:{{ MAILU_DMARC_RUF }}; adkim=s; aspf=s"' } + - { type: TXT, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "dkim._domainkey.{{ MAILU_DOMAIN_DNS_ZONE }}", value: '"{{ mailu_dkim_public_key }}"' } + - { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", service: "_submission", proto: "_tcp",name: "{{ MAILU_DOMAIN }}", priority: 20, weight: 1, port: 587, value: "{{ MAILU_HOSTNAME }}" } + - { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", service: "_submissions", proto: "_tcp",name: "{{ MAILU_DOMAIN }}", priority: 20, weight: 1, port: 465, value: "{{ MAILU_HOSTNAME }}" } - { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", service: "_imaps", proto: "_tcp", name: "{{ MAILU_DOMAIN }}", priority: 20, weight: 1, port: 993, value: "{{ MAILU_HOSTNAME }}" } - { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", service: "_imap", proto: "_tcp", name: "{{ MAILU_DOMAIN }}", priority: 20, weight: 1, port: 143, value: "{{ MAILU_HOSTNAME }}" } - { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", service: "_pop3s", proto: "_tcp", name: "{{ MAILU_DOMAIN }}", priority: 20, weight: 1, port: 995, value: "{{ MAILU_HOSTNAME }}" } diff --git a/roles/web-app-nextcloud/tasks/_plugin_b_enable_and_configure.yml b/roles/web-app-nextcloud/tasks/_plugin_b_enable_and_configure.yml index a693973e..9a5d122e 100644 --- a/roles/web-app-nextcloud/tasks/_plugin_b_enable_and_configure.yml +++ b/roles/web-app-nextcloud/tasks/_plugin_b_enable_and_configure.yml @@ -34,7 +34,7 @@ failed_when: not ASYNC_ENABLED and config_set_shell.rc != 0 async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}" poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}" - no_log: "{{ MASK_CREDENTIALS_IN_LOGS | default(true) | bool }}" + no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}" - name: Check if {{ plugin_task_path }} exists stat: diff --git a/roles/web-svc-collabora/tasks/01_core.yml b/roles/web-svc-collabora/tasks/01_core.yml index 4fa614af..3618269d 100644 --- a/roles/web-svc-collabora/tasks/01_core.yml +++ b/roles/web-svc-collabora/tasks/01_core.yml @@ -18,8 +18,8 @@ (not ASYNC_ENABLED | bool ) and ('updated' in (collabora_fonts.stdout | default(''))) - async: "{{ ASYNC_TIME if (ASYNC_ENABLED | default(false) | bool) else omit }}" - poll: "{{ ASYNC_POLL if (ASYNC_ENABLED | default(false) | bool) else omit }}" + async: "{{ ASYNC_TIME if (ASYNC_ENABLED | bool) else omit }}" + poll: "{{ ASYNC_POLL if (ASYNC_ENABLED | bool) else omit }}" when: MODE_UPDATE | bool - name: Allow Nextcloud host IP for Collabora preview conversion