refactor(dns): unify Cloudflare + Hetzner handling across roles

- replaced CERTBOT_DNS_API_TOKEN with CLOUDFLARE_API_TOKEN everywhere
- introduced generic sys-dns-cloudflare-records role for managing DNS records
- added sys-dns-hetzner-rdns role with both Cloud (hcloud) and Robot API flavors
- updated Mailu role to:
  - generate DKIM before DNS setup
  - delegate DNS + rDNS records to the new generic roles
- removed legacy per-role Cloudflare vars (MAILU_CLOUDFLARE_API_TOKEN)
- extended group vars with HOSTING_PROVIDER for rDNS flavor decision
- added hetzner.hcloud collection to requirements

This consolidates DNS management into reusable roles,
supports both Cloudflare and Hetzner providers,
and standardizes variable naming across the project.
This commit is contained in:
2025-08-16 21:43:01 +02:00
parent 838a55ea94
commit 2620ee088e
28 changed files with 437 additions and 159 deletions

View File

@@ -25,7 +25,7 @@
meta: flush_handlers
- name: "Create Mailu accounts"
include_tasks: 02_create-mailu-user.yml
include_tasks: 02_create-user.yml
vars:
MAILU_DOCKER_DIR: "{{ docker_compose.directories.instance }}"
mailu_api_base_url: "http://127.0.0.1:8080/api/v1"
@@ -45,6 +45,8 @@
loop_var: item
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: Generate DKIM public key
include_tasks: 04_generate-and-read-dkim.yml
- name: Set Mailu DNS records
include_tasks: 04_set-mailu-dns-records.yml
when: DNS_PROVIDER == 'cloudflare'
include_tasks: 05_dns-records.yml

View File

@@ -25,5 +25,5 @@
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "Create Mailu API Token for {{ mailu_user_name }}"
include_tasks: 03_create-mailu-token.yml
include_tasks: 03_create-token.yml
when: "{{ 'mail-bot' in item.value.roles }}"

View File

@@ -1,125 +0,0 @@
- name: Generate DKIM public key
include_tasks: 05_generate-and-read-dkim.yml
# A/AAAA record for the mail host in the **Hostname Zone**
- name: "Set A record for Mailu host"
community.general.cloudflare_dns:
api_token: "{{ MAILU_CLOUDFLARE_API_TOKEN }}"
zone: "{{ MAILU_HOSTNAME_DNS_ZONE }}"
type: A
name: "{{ MAILU_HOSTNAME }}" # Fully Qualified Domain Name of the mail host
content: "{{ MAILU_IP4_PUBLIC }}"
proxied: false
ttl: 1
state: present
async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "Set AAAA record for Mailu host"
community.general.cloudflare_dns:
api_token: "{{ MAILU_CLOUDFLARE_API_TOKEN }}"
zone: "{{ MAILU_HOSTNAME_DNS_ZONE }}"
type: AAAA
name: "{{ MAILU_HOSTNAME }}"
content: "{{ MAILU_IP6_PUBLIC }}"
proxied: false
ttl: 1
state: present
when: MAILU_IP6_PUBLIC is defined and MAILU_IP6_PUBLIC | length > 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 | bool }}"
# Autoconfig CNAME record in the **Mail Domain Zone**
- name: "Set CNAME record for autoconfig"
community.general.cloudflare_dns:
api_token: "{{ MAILU_CLOUDFLARE_API_TOKEN }}"
zone: "{{ MAILU_DOMAIN_DNS_ZONE }}"
type: CNAME
name: "autoconfig.{{ MAILU_DOMAIN_DNS_ZONE }}"
value: "{{ MAILU_HOSTNAME }}" # Points to the Mailu host FQDN
proxied: false
ttl: 1
state: present
async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
# MX record in the **Mail Domain Zone**
- name: "Set MX record"
community.general.cloudflare_dns:
api_token: "{{ MAILU_CLOUDFLARE_API_TOKEN }}"
zone: "{{ MAILU_DOMAIN_DNS_ZONE }}"
type: MX
name: "{{ MAILU_DOMAIN }}" # Root mail domain
value: "{{ MAILU_HOSTNAME }}" # Points to the Mailu host
priority: 10
ttl: 1
state: present
async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
# SRV records in the **Mail Domain Zone**
- name: "Set SRV records"
community.general.cloudflare_dns:
api_token: "{{ MAILU_CLOUDFLARE_API_TOKEN }}"
zone: "{{ MAILU_DOMAIN_DNS_ZONE }}"
type: SRV
service: "_{{ item.key }}"
proto: "_tcp"
priority: "{{ item.value.priority }}"
weight: "{{ item.value.weight }}"
port: "{{ item.value.port }}"
value: "{{ MAILU_HOSTNAME }}" # Target = Mailu host FQDN
ttl: 1
state: present
name: "{{ MAILU_DOMAIN }}"
loop: "{{ MAILU_DNS_SRV_RECORDS | dict2items }}"
ignore_errors: true
async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
# SPF TXT record in the **Mail Domain Zone**
- name: "Set SPF TXT record"
community.general.cloudflare_dns:
api_token: "{{ MAILU_CLOUDFLARE_API_TOKEN }}"
zone: "{{ MAILU_DOMAIN_DNS_ZONE }}"
type: TXT
name: "{{ MAILU_DOMAIN }}"
value: "v=spf1 mx a:{{ MAILU_HOSTNAME }} ~all"
ttl: 1
state: present
async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
# DMARC TXT record in the **Mail Domain Zone**
- name: "Set DMARC TXT record"
community.general.cloudflare_dns:
api_token: "{{ MAILU_CLOUDFLARE_API_TOKEN }}"
zone: "{{ MAILU_DOMAIN_DNS_ZONE }}"
type: TXT
name: "_dmarc.{{ MAILU_DOMAIN_DNS_ZONE }}"
value: "v=DMARC1; p=reject; ruf=mailto:{{ MAILU_DMARC_RUF }}; adkim=s; aspf=s"
ttl: 1
state: present
async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
# DKIM TXT record in the **Mail Domain Zone**
- name: "Set DKIM TXT record"
community.general.cloudflare_dns:
api_token: "{{ MAILU_CLOUDFLARE_API_TOKEN }}"
zone: "{{ MAILU_DOMAIN_DNS_ZONE }}"
type: TXT
name: "dkim._domainkey.{{ MAILU_DOMAIN_DNS_ZONE }}"
value: "{{ mailu_dkim_public_key }}"
ttl: 1
state: present
async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"

View File

@@ -0,0 +1,33 @@
- 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_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: 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 }}" }
- name: "rDNS (Hetzner Cloud) for Mailu"
include_role:
name: sys-dns-hetzner-rdns
when: HOSTING_PROVIDER | lower == 'hetzner'
vars:
rdns_records:
- { resource: "server", ip_address: "{{ MAILU_IP4_PUBLIC }}", dns_ptr: "{{ MAILU_HOSTNAME }}" }
# - { resource: "server", ip_address: "{{ MAILU_IP6_PUBLIC | default('') }}", dns_ptr: "{{ MAILU_HOSTNAME }}" }

View File

@@ -43,7 +43,6 @@ MAILU_SUBNET: "{{ networks.local['web-app-mailu'].subnet
## Credentials
MAILU_SECRET_KEY: "{{ applications | get_app_conf(application_id,'credentials.secret_key') }}"
MAILU_CLOUDFLARE_API_TOKEN: "{{ CERTBOT_DNS_API_TOKEN }}"
MAILU_API_TOKEN: "{{ applications | get_app_conf(application_id, 'credentials.api_token') }}"
## OIDC
@@ -55,16 +54,7 @@ MAILU_OIDC_ENABLE_USER_CREATION: "{{ applications | get_app_conf(applicatio
# @see https://github.com/heviat/Mailu-OIDC/tree/2024.06
MAILU_DOCKER_FLAVOR: "{{ 'ghcr.io/heviat' if MAILU_OIDC_ENABLED | bool else 'ghcr.io/mailu' }}"
MAILU_DMARC_RUF: "{{ applications | get_app_conf(application_id, 'users.administrator.email') }}"
MAILU_DMARC_RUF: "{{ applications | get_app_conf(application_id, 'users.administrator.email') }}"
MAILU_DKIM_KEY_FILE: "{{ MAILU_DOMAIN }}.dkim.key"
MAILU_DKIM_KEY_PATH: "/dkim/{{ MAILU_DKIM_KEY_FILE }}"
MAILU_DNS_SRV_RECORDS:
submission: { port: 587, priority: 20, weight: 1 }
submissions: { port: 465, priority: 20, weight: 1 }
imaps: { port: 993, priority: 20, weight: 1 }
imap: { port: 143, priority: 20, weight: 1 }
pop3s: { port: 995, priority: 20, weight: 1 }
pop3: { port: 110, priority: 20, weight: 1 }
autodiscover: { port: 443, priority: 20, weight: 1 }
MAILU_DKIM_KEY_FILE: "{{ MAILU_DOMAIN }}.dkim.key"
MAILU_DKIM_KEY_PATH: "/dkim/{{ MAILU_DKIM_KEY_FILE }}"