server-dmarc-hint: the server's own name gets a DMARC record

Owner, 2026-10-18: "Оба предложения делаем"; the mockup is 8b2dc8f. The
security audit's S3 left a product side: the panel showed the DKIM and SPF
records of the server's own name, not a DMARC one, and without it a
receiver does not refuse mail forged in that name - a false password-reset
message among it.

dnscheck.ServerMail checks a third record (Mail.DMARC, checkServerDMARC):
the sending-domain DMARC check held to more - missing is an error, as a
missing DKIM or SPF record of this name is (the page draws it as a
warning: notifications still go), p=none a warning ("weak": it refuses
nothing), quarantine or reject ok - and Overall includes it. The record to
publish is ServerMailDMARC, v=DMARC1; p=reject: only the panel sends as
the name, signed with its key, from the address its SPF names, so reject
costs nothing. Settings › Notification mail draws it after SPF
(view.ServerMailDMARC, folded when published), Health's card has the row,
Re-check DNS re-reads it with the others.

Tests: TestServerMailDMARC (missing, p=none, quarantine, reject); the
Settings and Health tests publish and expect three records. Help word for
word with the mockup; guide § Notifications; roadmap; CHANGELOG.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-18 10:05:39 +03:00
co-authored by Claude Opus 5.5
parent 8b2dc8f40f
commit 554e007f22
11 changed files with 135 additions and 28 deletions
+2
View File
@@ -139,6 +139,7 @@ func healthFixture(refresh bool) *Health {
h.WithNotificationMail("postmaster@mail.example.org", []MailRow{
NewMailRow("DKIM", mailFixtureDKIMHost, DNSCheck{Status: "ok"}),
NewMailRow("SPF", "mail.example.org", DNSCheck{Status: "error"}),
NewMailRow("DMARC", "_dmarc.mail.example.org", DNSCheck{Status: "warn"}),
}, "warn")
return h
}
@@ -171,6 +172,7 @@ func settingsFixture() *Settings {
WithMail([]Record{
ServerMailDKIM("sp2026", mailFixtureDKIMHost, key, DNSCheck{Status: "ok"}),
ServerMailSPF("mail.example.org", "v=spf1 a -all", DNSCheck{Status: "error", Detail: "No SPF record is published for mail.example.org."}),
ServerMailDMARC("_dmarc.mail.example.org", "v=DMARC1; p=reject", DNSCheck{Status: "warn", Detail: "No DMARC record at _dmarc.mail.example.org."}),
}, "3 min ago").
WithTimeZone("Europe/Moscow", msk, time.Date(2026, 10, 17, 12, 0, 0, 0, time.UTC))
}
+1 -1
View File
@@ -299,7 +299,7 @@ var serverTopics = []HelpTopic{
Br(), Br(),
"Each user turns notifications on or off under Account and needs an e-mail there; Server › Settings turns them off for everyone. They are the panel's own mail: not rows of the Outbound log, and not counted against any limit.",
Br(), Br(),
"For that mail to arrive, the server's own name needs two DNS records of its own, as each of your domains does: a DKIM key, which the panel generates and signs notifications with, and an SPF record, ", Code("v=spf1 a -all"), ". Server › Settings gives both with the values to publish and whether they are in place, and its Re-check DNS checks them again once they are published; Server › Health shows them too. Without them notifications are still sent, and a receiver may file them as spam. Mail of your domains does not depend on either."),
"For that mail to arrive, the server's own name needs DNS records of its own, as each of your domains does: a DKIM key, which the panel generates and signs notifications with, an SPF record, ", Code("v=spf1 a -all"), ", and a DMARC record, ", Code("v=DMARC1; p=reject"), ", which has receivers refuse mail forged in the server's name - a false password-reset message among it; only the panel sends as this name, signed, so reject is safe. Server › Settings gives all three with the values to publish and whether they are in place, and its Re-check DNS checks them again once they are published; Server › Health shows them too. Without them notifications are still sent, and a receiver may file them as spam. Mail of your domains depends on none of them."),
}},
}
+15
View File
@@ -213,6 +213,21 @@ func ServerMailSPF(host, value string, c DNSCheck) Record {
return r
}
// ServerMailDMARC is the DMARC record of the server's own name
// (server-dmarc-hint). While it is not in place the note under the value says
// what it is for and why its strict policy is safe.
func ServerMailDMARC(host, value string, c DNSCheck) Record {
r := Record{Name: "DMARC", Note: Plain("For the server's own name, not for your domains"),
Host: host, Type: "TXT", Value: value}
var standing Text
if c.Status != "ok" {
standing = Plain("Without it a receiver does not refuse mail forged in the server's name - a false password-reset message included. Only the panel sends as this name, signed, so reject is safe.")
}
applyCheck(&r, c, standing, nil)
r.Status = mailTag(c)
return r
}
// DMARCRecordInput is what a DMARC record needs besides its check: where the
// reports go in words (Source: the address, or "no reports") and the page that changes it,
// the page of the reports themselves ("" when the panel has none), and whether