14 "github.com/mjl-/adns"
16 "github.com/mjl-/mox/config"
17 "github.com/mjl-/mox/dkim"
18 "github.com/mjl-/mox/dmarc"
19 "github.com/mjl-/mox/dns"
20 "github.com/mjl-/mox/mox-"
21 "github.com/mjl-/mox/smtp"
22 "github.com/mjl-/mox/spf"
23 "github.com/mjl-/mox/tlsrpt"
26// todo: find a way to automatically create the dns records as it would greatly simplify setting up email for a domain. we could also dynamically make changes, e.g. providing grace periods after disabling a dkim key, only automatically removing the dkim dns key after a few days. but this requires some kind of api and authentication to the dns server. there doesn't appear to be a single commonly used api for dns management. each of the numerous cloud providers have their own APIs and rather large SKDs to use them. we don't want to link all of them in.
28// DomainRecords returns text lines describing DNS records required for configuring
31// If certIssuerDomainName is set, CAA records to limit TLS certificate issuance to
32// that caID will be suggested. If acmeAccountURI is also set, CAA records also
33// restricting issuance to that account ID will be suggested.
34func DomainRecords(domConf config.Domain, domain dns.Domain, hasDNSSEC bool, certIssuerDomainName, acmeAccountURI string) ([]string, error) {
36 h := mox.Conf.Static.HostnameDomain.ASCII
38 // The first line with ";" is used by ../testdata/integration/moxacmepebble.sh and
39 // ../testdata/integration/moxmail2.sh for selecting DNS records
41 "; Time To Live of 5 minutes, may be recognized if importing as a zone file.",
42 "; Once your setup is working, you may want to increase the TTL.",
47 if public, ok := mox.Conf.Static.Listeners["public"]; ok && public.TLS != nil && (len(public.TLS.HostPrivateRSA2048Keys) > 0 || len(public.TLS.HostPrivateECDSAP256Keys) > 0) {
48 records = append(records,
49 `; DANE: These records indicate that a remote mail server trying to deliver email`,
50 `; with SMTP (TCP port 25) must verify the TLS certificate with DANE-EE (3), based`,
51 `; on the certificate public key ("SPKI", 1) that is SHA2-256-hashed (1) to the`,
52 `; hexadecimal hash. DANE-EE verification means only the certificate or public`,
53 `; key is verified, not whether the certificate is signed by a (centralized)`,
54 `; certificate authority (CA), is expired, or matches the host name.`,
56 `; NOTE: Create the records below only once: They are for the machine, and apply`,
57 `; to all hosted domains.`,
60 records = append(records,
62 "; WARNING: Domain does not appear to be DNSSEC-signed. To enable DANE, first",
63 "; enable DNSSEC on your domain, then add the TLSA records. Records below have been",
67 addTLSA := func(privKey crypto.Signer) error {
68 spkiBuf, err := x509.MarshalPKIXPublicKey(privKey.Public())
70 return fmt.Errorf("marshal SubjectPublicKeyInfo for DANE record: %v", err)
72 sum := sha256.Sum256(spkiBuf)
73 tlsaRecord := adns.TLSA{
74 Usage: adns.TLSAUsageDANEEE,
75 Selector: adns.TLSASelectorSPKI,
76 MatchType: adns.TLSAMatchTypeSHA256,
81 s = fmt.Sprintf("_25._tcp.%-*s TLSA %s", 20+len(d)-len("_25._tcp."), h+".", tlsaRecord.Record())
83 s = fmt.Sprintf(";; _25._tcp.%-*s TLSA %s", 20+len(d)-len(";; _25._tcp."), h+".", tlsaRecord.Record())
85 records = append(records, s)
88 for _, privKey := range public.TLS.HostPrivateECDSAP256Keys {
89 if err := addTLSA(privKey); err != nil {
93 for _, privKey := range public.TLS.HostPrivateRSA2048Keys {
94 if err := addTLSA(privKey); err != nil {
98 records = append(records, "")
102 records = append(records,
103 "; For the machine, only needs to be created once, for the first domain added:",
105 "; SPF-allow host for itself, resulting in relaxed DMARC pass for (postmaster)",
106 "; messages (DSNs) sent from host:",
111 if d != h && mox.Conf.Static.HostTLSRPT.ParsedLocalpart != "" {
114 Opaque: smtp.NewAddress(mox.Conf.Static.HostTLSRPT.ParsedLocalpart, mox.Conf.Static.HostnameDomain).Pack(false),
116 tlsrptr := tlsrpt.Record{Version: "TLSRPTv1", RUAs: [][]tlsrpt.RUA{{tlsrpt.RUA(uri.String())}}}
117 records = append(records,
118 "; For the machine, only needs to be created once, for the first domain added:",
120 "; Request reporting about success/failures of TLS connections to (MX) host, for DANE.",
121 fmt.Sprintf(`_smtp._tls.%-*s TXT "%s"`, 20+len(d)-len("_smtp._tls."), h+".", tlsrptr.String()),
126 records = append(records,
127 "; Deliver email for the domain to this host.",
128 fmt.Sprintf("%s. MX 10 %s.", d, h),
131 "; Outgoing messages will be signed with the first two DKIM keys. The other two",
132 "; configured for backup, switching to them is just a config change.",
134 var selectors []string
135 for name := range domConf.DKIM.Selectors {
136 selectors = append(selectors, name)
138 sort.Slice(selectors, func(i, j int) bool {
139 return selectors[i] < selectors[j]
141 for _, name := range selectors {
142 sel := domConf.DKIM.Selectors[name]
143 dkimr := dkim.Record{
145 Hashes: []string{"sha256"},
146 PublicKey: sel.Key.Public(),
148 if _, ok := sel.Key.(ed25519.PrivateKey); ok {
149 dkimr.Key = "ed25519"
150 } else if _, ok := sel.Key.(*rsa.PrivateKey); !ok {
151 return nil, fmt.Errorf("unrecognized private key for DKIM selector %q: %T", name, sel.Key)
153 txt, err := dkimr.Record()
155 return nil, fmt.Errorf("making DKIM DNS TXT record: %v", err)
159 records = append(records,
160 "; NOTE: The following is a single long record split over several lines for use",
161 "; in zone files. When adding through a DNS operator web interface, combine the",
162 "; strings into a single string, without ().",
165 s := fmt.Sprintf("%s._domainkey.%s. TXT %s", name, d, mox.TXTStrings(txt))
166 records = append(records, s)
169 dmarcr := dmarc.DefaultRecord
170 dmarcr.Policy = "reject"
171 if domConf.DMARC != nil {
174 Opaque: smtp.NewAddress(domConf.DMARC.ParsedLocalpart, domConf.DMARC.DNSDomain).Pack(false),
176 dmarcr.AggregateReportAddresses = []dmarc.URI{
177 {Address: uri.String(), MaxSize: 10, Unit: "m"},
180 dspfr := spf.Record{Version: "spf1"}
181 for _, ip := range mox.DomainSPFIPs() {
186 dspfr.Directives = append(dspfr.Directives, spf.Directive{Mechanism: mech, IP: ip})
188 dspfr.Directives = append(dspfr.Directives,
189 spf.Directive{Mechanism: "mx"},
190 spf.Directive{Qualifier: "~", Mechanism: "all"},
192 dspftxt, err := dspfr.Record()
194 return nil, fmt.Errorf("making domain spf record: %v", err)
196 records = append(records,
199 "; Specify the MX host is allowed to send for our domain and for itself (for DSNs).",
200 "; ~all means softfail for anything else, which is done instead of -all to prevent older",
201 "; mail servers from rejecting the message because they never get to looking for a dkim/dmarc pass.",
202 fmt.Sprintf(`%s. TXT "%s"`, d, dspftxt),
205 "; Emails that fail the DMARC check (without aligned DKIM and without aligned SPF)",
206 "; should be rejected, and request reports. If you email through mailing lists that",
207 "; strip DKIM-Signature headers and don't rewrite the From header, you may want to",
208 "; set the policy to p=none.",
209 fmt.Sprintf(`_dmarc.%s. TXT "%s"`, d, dmarcr.String()),
213 if sts := domConf.MTASTS; sts != nil {
214 records = append(records,
215 "; Remote servers can use MTA-STS to verify our TLS certificate with the",
216 "; WebPKI pool of CA's (certificate authorities) when delivering over SMTP with",
218 fmt.Sprintf(`mta-sts.%s. CNAME %s.`, d, h),
219 fmt.Sprintf(`_mta-sts.%s. TXT "v=STSv1; id=%s"`, d, sts.PolicyID),
223 records = append(records,
224 "; Note: No MTA-STS to indicate TLS should be used. Either because disabled for the",
225 "; domain or because mox.conf does not have a listener with MTA-STS configured.",
230 if domConf.TLSRPT != nil {
233 Opaque: smtp.NewAddress(domConf.TLSRPT.ParsedLocalpart, domConf.TLSRPT.DNSDomain).Pack(false),
235 tlsrptr := tlsrpt.Record{Version: "TLSRPTv1", RUAs: [][]tlsrpt.RUA{{tlsrpt.RUA(uri.String())}}}
236 records = append(records,
237 "; Request reporting about TLS failures.",
238 fmt.Sprintf(`_smtp._tls.%s. TXT "%s"`, d, tlsrptr.String()),
243 if domConf.ClientSettingsDomain != "" && domConf.ClientSettingsDNSDomain != mox.Conf.Static.HostnameDomain {
244 records = append(records,
245 "; Client settings will reference a subdomain of the hosted domain, making it",
246 "; easier to migrate to a different server in the future by not requiring settings",
247 "; in all clients to be updated.",
248 fmt.Sprintf(`%-*s CNAME %s.`, 20+len(d), domConf.ClientSettingsDNSDomain.ASCII+".", h),
253 records = append(records,
254 "; Autoconfig is used by Thunderbird. Autodiscover is (in theory) used by Microsoft.",
255 fmt.Sprintf(`autoconfig.%s. CNAME %s.`, d, h),
256 fmt.Sprintf(`_autodiscover._tcp.%s. SRV 0 1 443 %s.`, d, h),
260 "; For secure IMAP and submission autoconfig, point to mail host.",
261 fmt.Sprintf(`_imaps._tcp.%s. SRV 0 1 993 %s.`, d, h),
262 fmt.Sprintf(`_submissions._tcp.%s. SRV 0 1 465 %s.`, d, h),
265 "; Next records specify POP3 and non-TLS ports are not to be used.",
266 "; These are optional and safe to leave out (e.g. if you have to click a lot in a",
267 "; DNS admin web interface).",
268 fmt.Sprintf(`_imap._tcp.%s. SRV 0 0 0 .`, d),
269 fmt.Sprintf(`_submission._tcp.%s. SRV 0 0 0 .`, d),
270 fmt.Sprintf(`_pop3._tcp.%s. SRV 0 0 0 .`, d),
271 fmt.Sprintf(`_pop3s._tcp.%s. SRV 0 0 0 .`, d),
274 if certIssuerDomainName != "" {
276 records = append(records,
279 "; You could mark Let's Encrypt as the only Certificate Authority allowed to",
280 "; sign TLS certificates for your domain.",
281 fmt.Sprintf(`%s. CAA 0 issue "%s"`, d, certIssuerDomainName),
283 if acmeAccountURI != "" {
286 records = append(records,
288 "; Optionally limit certificates for this domain to the account ID and methods used by mox.",
289 fmt.Sprintf(`;; %s. CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, d, certIssuerDomainName, acmeAccountURI),
291 "; Or alternatively only limit for email-specific subdomains, so you can use",
292 "; other accounts/methods for other subdomains.",
293 fmt.Sprintf(`;; autoconfig.%s. CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, d, certIssuerDomainName, acmeAccountURI),
294 fmt.Sprintf(`;; mta-sts.%s. CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, d, certIssuerDomainName, acmeAccountURI),
296 if domConf.ClientSettingsDomain != "" && domConf.ClientSettingsDNSDomain != mox.Conf.Static.HostnameDomain {
297 records = append(records,
298 fmt.Sprintf(`;; %-*s CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, 20-3+len(d), domConf.ClientSettingsDNSDomain.ASCII, certIssuerDomainName, acmeAccountURI),
301 if strings.HasSuffix(h, "."+d) {
302 records = append(records,
304 "; And the mail hostname.",
305 fmt.Sprintf(`;; %-*s CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, 20-3+len(d), h+".", certIssuerDomainName, acmeAccountURI),
309 // The string "will be suggested" is used by
310 // ../testdata/integration/moxacmepebble.sh and ../testdata/integration/moxmail2.sh
311 // as end of DNS records.
312 records = append(records,
314 "; Note: After starting up, once an ACME account has been created, CAA records",
315 "; that restrict issuance to the account will be suggested.",