From 96ded68ef49c1dbf281dd2da0908a88e92750486 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Fri, 19 Sep 2025 15:29:11 +0200 Subject: [PATCH] Refactor DNS handling and add solo record support - Added 'solo' flag support for A/AAAA, CNAME/MX/TXT, and SRV records in sys-dns-cloudflare-records. - Simplified sys-svc-dns: removed NS management tasks and CLOUDFLARE_NAMESERVERS default. - Renamed 03_apex.yml back to 02_apex.yml, adjusted AAAA task name. - Updated web-app-bluesky DNS tasks: marked critical records with 'solo'. - Updated web-app-mailu DNS tasks: removed cleanup block, enforced 'solo' on all records. - Adjusted constructor stage to call domain_mappings with AUTO_BUILD_ALIASES parameter. Conversation: https://chatgpt.com/share/68cd20d8-9ba8-800f-b070-f7294f072c40 --- .../sys-dns-cloudflare-records/tasks/main.yml | 3 + roles/sys-svc-dns/defaults/main.yml | 1 - roles/sys-svc-dns/tasks/01_core.yml | 9 +-- .../tasks/{03_apex.yml => 02_apex.yml} | 2 +- roles/sys-svc-dns/tasks/02_nameservers.yml | 60 ------------------ roles/web-app-bluesky/tasks/03_dns.yml | 7 +++ roles/web-app-mailu/tasks/05_dns-records.yml | 62 ++++--------------- tasks/stages/01_constructor.yml | 2 +- 8 files changed, 26 insertions(+), 120 deletions(-) delete mode 100644 roles/sys-svc-dns/defaults/main.yml rename roles/sys-svc-dns/tasks/{03_apex.yml => 02_apex.yml} (90%) delete mode 100644 roles/sys-svc-dns/tasks/02_nameservers.yml diff --git a/roles/sys-dns-cloudflare-records/tasks/main.yml b/roles/sys-dns-cloudflare-records/tasks/main.yml index d2135d67..f68d7f2b 100644 --- a/roles/sys-dns-cloudflare-records/tasks/main.yml +++ b/roles/sys-dns-cloudflare-records/tasks/main.yml @@ -17,6 +17,7 @@ proxied: "{{ item.proxied | default(false) }}" ttl: "{{ item.ttl | default(1) }}" state: "{{ item.state | default('present') }}" + solo: "{{ item.solo | default(false) }}" loop: "{{ cloudflare_records | selectattr('type','in',['A','AAAA']) | list }}" loop_control: { label: "{{ item.type }} {{ item.name }} -> {{ item.content }}" } async: "{{ cloudflare_async_enabled | ternary(cloudflare_async_time, omit) }}" @@ -48,6 +49,7 @@ ttl: "{{ item.ttl | default(1) }}" priority: "{{ (item.type == 'MX') | ternary(item.priority | default(10), omit) }}" state: "{{ item.state | default('present') }}" + solo: "{{ item.solo | default(false) }}" loop: "{{ cloudflare_records | selectattr('type','in',['CNAME','MX','TXT']) | list }}" loop_control: { label: "{{ item.type }} {{ item.name }} -> {{ item.value }}" } async: "{{ cloudflare_async_enabled | ternary(cloudflare_async_time, omit) }}" @@ -83,6 +85,7 @@ value: "{{ item.value }}" ttl: "{{ item.ttl | default(1) }}" state: "{{ item.state | default('present') }}" + solo: "{{ item.solo | default(false) }}" loop: "{{ cloudflare_records | selectattr('type','equalto','SRV') | list }}" loop_control: { label: "SRV {{ item.service }}.{{ item.proto }} {{ item.name }} -> {{ item.value }}:{{ item.port }}" } ignore_errors: "{{ item.ignore_errors | default(true) }}" diff --git a/roles/sys-svc-dns/defaults/main.yml b/roles/sys-svc-dns/defaults/main.yml deleted file mode 100644 index 34cd87e0..00000000 --- a/roles/sys-svc-dns/defaults/main.yml +++ /dev/null @@ -1 +0,0 @@ -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 53841094..d562a9f0 100644 --- a/roles/sys-svc-dns/tasks/01_core.yml +++ b/roles/sys-svc-dns/tasks/01_core.yml @@ -5,15 +5,8 @@ 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: 03_apex.yml + include_tasks: 02_apex.yml loop: "{{ SYS_SVC_DNS_BASE_DOMAINS | list }}" loop_control: loop_var: base_domain diff --git a/roles/sys-svc-dns/tasks/03_apex.yml b/roles/sys-svc-dns/tasks/02_apex.yml similarity index 90% rename from roles/sys-svc-dns/tasks/03_apex.yml rename to roles/sys-svc-dns/tasks/02_apex.yml index ab11ec66..606431c7 100644 --- a/roles/sys-svc-dns/tasks/03_apex.yml +++ b/roles/sys-svc-dns/tasks/02_apex.yml @@ -12,7 +12,7 @@ async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}" poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}" -- name: "Ensure AAAA @ for {{ base_domain }} (if IPv6 is global)" +- name: "Ensure AAAA @ for {{ base_domain }} with '{{ networks.internet.ip6 | default('None') }}" community.general.cloudflare_dns: api_token: "{{ CLOUDFLARE_API_TOKEN }}" zone: "{{ base_domain }}" diff --git a/roles/sys-svc-dns/tasks/02_nameservers.yml b/roles/sys-svc-dns/tasks/02_nameservers.yml deleted file mode 100644 index e97a8da1..00000000 --- a/roles/sys-svc-dns/tasks/02_nameservers.yml +++ /dev/null @@ -1,60 +0,0 @@ -# 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/web-app-bluesky/tasks/03_dns.yml b/roles/web-app-bluesky/tasks/03_dns.yml index 39b17264..17d8669a 100644 --- a/roles/web-app-bluesky/tasks/03_dns.yml +++ b/roles/web-app-bluesky/tasks/03_dns.yml @@ -26,6 +26,7 @@ zone: "{{ BLUESKY_API_DOMAIN | to_zone }}" name: "{{ BLUESKY_API_DOMAIN }}" content: "{{ networks.internet.ip4 }}" + solo: true proxied: false - type: AAAA @@ -33,6 +34,7 @@ name: "{{ BLUESKY_API_DOMAIN }}" content: "{{ networks.internet.ip6 | default('') }}" proxied: false + solo: true state: "{{ (networks.internet.ip6 is defined and (networks.internet.ip6 | string) | length > 0) | ternary('present','absent') }}" # 2) Handle verification for primary handle (Apex) @@ -40,9 +42,11 @@ zone: "{{ PRIMARY_DOMAIN | to_zone }}" name: "_atproto.{{ PRIMARY_DOMAIN }}" value: '"did=did:web:{{ BLUESKY_API_DOMAIN }}"' + solo: true # 3) Web UI host (only if enabled) - type: A + solo: true zone: "{{ BLUESKY_WEB_DOMAIN | to_zone }}" name: "{{ BLUESKY_WEB_DOMAIN }}" content: "{{ networks.internet.ip4 }}" @@ -50,6 +54,7 @@ state: "{{ (BLUESKY_WEB_ENABLED | bool) | ternary('present','absent') }}" - type: AAAA + solo: true zone: "{{ BLUESKY_WEB_DOMAIN | to_zone }}" name: "{{ BLUESKY_WEB_DOMAIN }}" content: "{{ networks.internet.ip6 | default('') }}" @@ -58,6 +63,7 @@ # 4) Custom AppView host (only if you actually run one and it's not api.bsky.app) - type: A + solo: true zone: "{{ BLUESKY_VIEW_DOMAIN | to_zone }}" name: "{{ BLUESKY_VIEW_DOMAIN }}" content: "{{ networks.internet.ip4 }}" @@ -65,6 +71,7 @@ state: "{{ (BLUESKY_VIEW_ENABLED | bool) and (BLUESKY_VIEW_DOMAIN != 'api.bsky.app') | ternary('present','absent') }}" - type: AAAA + solo: true zone: "{{ BLUESKY_VIEW_DOMAIN | to_zone }}" name: "{{ BLUESKY_VIEW_DOMAIN }}" content: "{{ networks.internet.ip6 | default('') }}" diff --git a/roles/web-app-mailu/tasks/05_dns-records.yml b/roles/web-app-mailu/tasks/05_dns-records.yml index 9c027d44..c7157c18 100644 --- a/roles/web-app-mailu/tasks/05_dns-records.yml +++ b/roles/web-app-mailu/tasks/05_dns-records.yml @@ -1,39 +1,3 @@ -- 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 @@ -44,20 +8,20 @@ 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: A, zone: "{{ MAILU_HOSTNAME_DNS_ZONE }}", name: "{{ MAILU_HOSTNAME }}", solo: true, 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: 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 }}" } - - { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", service: "_pop3", proto: "_tcp", name: "{{ MAILU_DOMAIN }}", priority: 20, weight: 1, port: 110, value: "{{ MAILU_HOSTNAME }}" } - - { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", service: "_autodiscover", proto: "_tcp", name: "{{ MAILU_DOMAIN }}", priority: 20, weight: 1, port: 443, value: "{{ MAILU_HOSTNAME }}" } + - { type: CNAME, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "autoconfig.{{ MAILU_DOMAIN_DNS_ZONE }}", solo: true, value: "{{ MAILU_HOSTNAME }}" } + - { type: MX, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "{{ MAILU_DOMAIN }}", solo: true, value: "{{ MAILU_HOSTNAME }}", priority: 10 } + - { type: TXT, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "{{ MAILU_DOMAIN }}", solo: true, value: '"v=spf1 mx a:{{ MAILU_HOSTNAME }} ~all"' } + - { type: TXT, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "_dmarc.{{ MAILU_DOMAIN_DNS_ZONE }}", solo: true, 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 }}", solo: true, value: '"{{ mailu_dkim_public_key }}"' } + - { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "{{ MAILU_DOMAIN }}", solo: true, service: "_submission", proto: "_tcp", priority: 20, weight: 1, port: 587, value: "{{ MAILU_HOSTNAME }}" } + - { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "{{ MAILU_DOMAIN }}", solo: true, service: "_submissions", proto: "_tcp", priority: 20, weight: 1, port: 465, value: "{{ MAILU_HOSTNAME }}" } + - { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "{{ MAILU_DOMAIN }}", solo: true, service: "_imaps", proto: "_tcp", priority: 20, weight: 1, port: 993, value: "{{ MAILU_HOSTNAME }}" } + - { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "{{ MAILU_DOMAIN }}", solo: true, service: "_imap", proto: "_tcp", priority: 20, weight: 1, port: 143, value: "{{ MAILU_HOSTNAME }}" } + - { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "{{ MAILU_DOMAIN }}", solo: true, service: "_pop3s", proto: "_tcp", priority: 20, weight: 1, port: 995, value: "{{ MAILU_HOSTNAME }}" } + - { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "{{ MAILU_DOMAIN }}", solo: true, service: "_pop3", proto: "_tcp", priority: 20, weight: 1, port: 110, value: "{{ MAILU_HOSTNAME }}" } + - { type: SRV, zone: "{{ MAILU_DOMAIN_DNS_ZONE }}", name: "{{ MAILU_DOMAIN }}", solo: true, service: "_autodiscover", proto: "_tcp", priority: 20, weight: 1, port: 443, value: "{{ MAILU_HOSTNAME }}" } - name: "rDNS (Hetzner Cloud) for Mailu" include_role: diff --git a/tasks/stages/01_constructor.yml b/tasks/stages/01_constructor.yml index 68e6f64a..c1726668 100644 --- a/tasks/stages/01_constructor.yml +++ b/tasks/stages/01_constructor.yml @@ -76,7 +76,7 @@ redirect_domain_mappings: >- {{ CURRENT_PLAY_APPLICATIONS | - domain_mappings(PRIMARY_DOMAIN) | + domain_mappings(PRIMARY_DOMAIN, AUTO_BUILD_ALIASES) | merge_mapping(redirect_domain_mappings, 'source') }}