diff --git a/inventories/production/group_vars/all/public_dns.yml b/inventories/production/group_vars/all/public_dns.yml index e03ea43..b260f5b 100644 --- a/inventories/production/group_vars/all/public_dns.yml +++ b/inventories/production/group_vars/all/public_dns.yml @@ -4,9 +4,10 @@ # vault.gandi.pat. See docs/decisions/007-network.md and the M1 spec. public_dns__domain: wingu.me -# Present — anti-spoof baseline for a no-mail domain (overwrites Gandi's seeded mail set). +# Present — anti-spoof baseline for a no-mail domain. No null-MX: Gandi LiveDNS rejects +# the RFC-7505 "0 ." form, so the MX is simply REMOVED (below) — no MX + no apex A means +# no mail delivery, and SPF -all + DMARC reject prevent spoofing. public_dns__records: - - {record: "@", type: MX, values: ["0 ."], ttl: 3600} - {record: "@", type: TXT, values: ['"v=spf1 -all"'], ttl: 3600} - {record: _dmarc, type: TXT, values: ['"v=DMARC1; p=reject;"'], ttl: 3600} # Service records appear as public-tier needs arise (askari A in M4). @@ -15,6 +16,7 @@ public_dns__records: # Absent — Gandi's auto-seeded defaults we don't want (purged once, idempotent thereafter). public_dns__absent: - {record: "@", type: A} # Gandi parking IP + - {record: "@", type: MX} # Gandi mail MX (no mail on wingu.me; null-MX unsupported) - {record: www, type: CNAME} # Gandi web-redirect - {record: webmail, type: CNAME} # Gandi webmail - {record: gm1._domainkey, type: CNAME} # Gandi DKIM diff --git a/roles/public_dns/molecule/default/converge.yml b/roles/public_dns/molecule/default/converge.yml index 7a4c873..43fe109 100644 --- a/roles/public_dns/molecule/default/converge.yml +++ b/roles/public_dns/molecule/default/converge.yml @@ -6,9 +6,9 @@ public_dns__apply: false # never call the Gandi API from a container public_dns__domain: example.test public_dns__records: - - {record: "@", type: MX, values: ["0 ."], ttl: 3600} - {record: "@", type: TXT, values: ['"v=spf1 -all"'], ttl: 3600} public_dns__absent: - {record: www, type: CNAME} + - {record: "@", type: MX} roles: - role: public_dns diff --git a/roles/public_dns/tasks/main.yml b/roles/public_dns/tasks/main.yml index c1f3e2f..521a4b1 100644 --- a/roles/public_dns/tasks/main.yml +++ b/roles/public_dns/tasks/main.yml @@ -3,9 +3,10 @@ ansible.builtin.assert: that: - public_dns__domain | length > 0 - - public_dns__records | selectattr('type', 'equalto', 'MX') | list | length > 0 + - public_dns__records | selectattr('record', 'equalto', '@') + | selectattr('type', 'equalto', 'TXT') | list | length > 0 fail_msg: >- - public_dns__domain must be set and a null-MX anti-spoof record declared in + public_dns__domain must be set and an SPF record (@/TXT) declared in public_dns__records (group_vars/all/public_dns.yml). run_once: true diff --git a/tests/test_public_dns.py b/tests/test_public_dns.py index 0d498af..16fc0fc 100644 --- a/tests/test_public_dns.py +++ b/tests/test_public_dns.py @@ -8,8 +8,10 @@ _DATA = ( ) # Gandi auto-seeds these on a fresh .me zone; boma purges them (verified 2026-06-14). +# Includes the @ MX: Gandi rejects the RFC-7505 null-MX "0 .", so we remove the MX +# entirely (no MX + no apex A = no mail) rather than declare a null-MX present. GANDI_DEFAULTS_ABSENT = { - ("@", "A"), ("www", "CNAME"), ("webmail", "CNAME"), + ("@", "A"), ("@", "MX"), ("www", "CNAME"), ("webmail", "CNAME"), ("gm1._domainkey", "CNAME"), ("gm2._domainkey", "CNAME"), ("gm3._domainkey", "CNAME"), ("_imap._tcp", "SRV"), ("_imaps._tcp", "SRV"), ("_pop3._tcp", "SRV"), ("_pop3s._tcp", "SRV"), ("_submission._tcp", "SRV"), @@ -32,9 +34,9 @@ def test_present_records_well_formed(): def test_anti_spoof_baseline_present(): recs = {(r["record"], r["type"]): r["values"] for r in _load()["public_dns__records"]} - assert recs[("@", "MX")] == ["0 ."] # null MX assert recs[("@", "TXT")] == ['"v=spf1 -all"'] # SPF deny-all assert recs[("_dmarc", "TXT")] == ['"v=DMARC1; p=reject;"'] + assert ("@", "MX") not in recs # no MX (Gandi rejects null-MX; removed instead) def test_gandi_defaults_marked_absent():