1package admin
2
3import (
4 "crypto"
5 "crypto/ed25519"
6 "crypto/rsa"
7 "crypto/sha256"
8 "crypto/x509"
9 "fmt"
10 "net/url"
11 "strings"
12
13 "github.com/mjl-/adns"
14
15 "github.com/mjl-/mox/config"
16 "github.com/mjl-/mox/dkim"
17 "github.com/mjl-/mox/dmarc"
18 "github.com/mjl-/mox/dns"
19 "github.com/mjl-/mox/mox-"
20 "github.com/mjl-/mox/smtp"
21 "github.com/mjl-/mox/spf"
22 "github.com/mjl-/mox/tlsrpt"
23 "slices"
24)
25
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.
27
28// DomainRecords returns text lines describing DNS records required for configuring
29// a domain.
30//
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) {
35 d := domain.ASCII
36 h := mox.Conf.Static.HostnameDomain.ASCII
37
38 // The first line with ";" is used by ../testdata/integration/moxacmepebble.sh and
39 // ../testdata/integration/moxmail2.sh for selecting DNS records
40 records := []string{
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.",
43 "$TTL 300",
44 "",
45 }
46
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.`,
55 `;`,
56 `; NOTE: Create the records below only once: They are for the machine, and apply`,
57 `; to all hosted domains.`,
58 )
59 if !hasDNSSEC {
60 records = append(records,
61 ";",
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",
64 "; commented out.",
65 )
66 }
67 addTLSA := func(privKey crypto.Signer) error {
68 spkiBuf, err := x509.MarshalPKIXPublicKey(privKey.Public())
69 if err != nil {
70 return fmt.Errorf("marshal SubjectPublicKeyInfo for DANE record: %v", err)
71 }
72 sum := sha256.Sum256(spkiBuf)
73 tlsaRecord := adns.TLSA{
74 Usage: adns.TLSAUsageDANEEE,
75 Selector: adns.TLSASelectorSPKI,
76 MatchType: adns.TLSAMatchTypeSHA256,
77 CertAssoc: sum[:],
78 }
79 var s string
80 if hasDNSSEC {
81 s = fmt.Sprintf("_25._tcp.%-*s TLSA %s", 20+len(d)-len("_25._tcp."), h+".", tlsaRecord.Record())
82 } else {
83 s = fmt.Sprintf(";; _25._tcp.%-*s TLSA %s", 20+len(d)-len(";; _25._tcp."), h+".", tlsaRecord.Record())
84 }
85 records = append(records, s)
86 return nil
87 }
88 for _, privKey := range public.TLS.HostPrivateECDSAP256Keys {
89 if err := addTLSA(privKey); err != nil {
90 return nil, err
91 }
92 }
93 for _, privKey := range public.TLS.HostPrivateRSA2048Keys {
94 if err := addTLSA(privKey); err != nil {
95 return nil, err
96 }
97 }
98 records = append(records, "")
99 }
100
101 if d != h {
102 records = append(records,
103 "; For the machine, only needs to be created once, for the first domain added:",
104 "; ",
105 "; SPF-allow host for itself, resulting in relaxed DMARC pass for (postmaster)",
106 "; messages (DSNs) sent from host:",
107 fmt.Sprintf(`%-*s TXT "v=spf1 a -all"`, 20+len(d), h+"."), // ../rfc/7208:2263 ../rfc/7208:2287
108 "",
109 )
110 }
111 if d != h && mox.Conf.Static.HostTLSRPT.ParsedLocalpart != "" {
112 uri := url.URL{
113 Scheme: "mailto",
114 Opaque: smtp.NewAddress(mox.Conf.Static.HostTLSRPT.ParsedLocalpart, mox.Conf.Static.HostnameDomain).Pack(false),
115 }
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:",
119 "; ",
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()),
122 "",
123 )
124 }
125
126 records = append(records,
127 "; Deliver email for the domain to this host.",
128 fmt.Sprintf("%s. MX 10 %s.", d, h),
129 "",
130
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.",
133 )
134 var selectors []string
135 for name := range domConf.DKIM.Selectors {
136 selectors = append(selectors, name)
137 }
138 slices.Sort(selectors)
139 for _, name := range selectors {
140 sel := domConf.DKIM.Selectors[name]
141 dkimr := dkim.Record{
142 Version: "DKIM1",
143 Hashes: []string{"sha256"},
144 PublicKey: sel.Key.Public(),
145 }
146 if _, ok := sel.Key.(ed25519.PrivateKey); ok {
147 dkimr.Key = "ed25519"
148 } else if _, ok := sel.Key.(*rsa.PrivateKey); !ok {
149 return nil, fmt.Errorf("unrecognized private key for DKIM selector %q: %T", name, sel.Key)
150 }
151 txt, err := dkimr.Record()
152 if err != nil {
153 return nil, fmt.Errorf("making DKIM DNS TXT record: %v", err)
154 }
155
156 if len(txt) > 100 {
157 records = append(records,
158 "; NOTE: The following is a single long record split over several lines for use",
159 "; in zone files. When adding through a DNS operator web interface, combine the",
160 "; strings into a single string, without ().",
161 )
162 }
163 s := fmt.Sprintf("%s._domainkey.%s. TXT %s", name, d, mox.TXTStrings(txt))
164 records = append(records, s)
165
166 }
167 dmarcr := dmarc.DefaultRecord
168 dmarcr.Policy = "reject"
169 if domConf.DMARC != nil {
170 uri := url.URL{
171 Scheme: "mailto",
172 Opaque: smtp.NewAddress(domConf.DMARC.ParsedLocalpart, domConf.DMARC.DNSDomain).Pack(false),
173 }
174 dmarcr.AggregateReportAddresses = []dmarc.URI{
175 {Address: uri.String(), MaxSize: 10, Unit: "m"},
176 }
177 }
178 dspfr := spf.Record{Version: "spf1"}
179 for _, ip := range mox.DomainSPFIPs() {
180 mech := "ip4"
181 if ip.To4() == nil {
182 mech = "ip6"
183 }
184 dspfr.Directives = append(dspfr.Directives, spf.Directive{Mechanism: mech, IP: ip})
185 }
186 dspfr.Directives = append(dspfr.Directives,
187 spf.Directive{Mechanism: "mx"},
188 spf.Directive{Qualifier: "~", Mechanism: "all"},
189 )
190 dspftxt, err := dspfr.Record()
191 if err != nil {
192 return nil, fmt.Errorf("making domain spf record: %v", err)
193 }
194 records = append(records,
195 "",
196
197 "; Specify the MX host is allowed to send for our domain and for itself (for DSNs).",
198 "; ~all means softfail for anything else, which is done instead of -all to prevent older",
199 "; mail servers from rejecting the message because they never get to looking for a dkim/dmarc pass.",
200 fmt.Sprintf(`%s. TXT "%s"`, d, dspftxt),
201 "",
202
203 "; Emails that fail the DMARC check (without aligned DKIM and without aligned SPF)",
204 "; should be rejected, and request reports. If you email through mailing lists that",
205 "; strip DKIM-Signature headers and don't rewrite the From header, you may want to",
206 "; set the policy to p=none.",
207 fmt.Sprintf(`_dmarc.%s. TXT "%s"`, d, dmarcr.String()),
208 "",
209 )
210
211 if sts := domConf.MTASTS; sts != nil {
212 records = append(records,
213 "; Remote servers can use MTA-STS to verify our TLS certificate with the",
214 "; WebPKI pool of CA's (certificate authorities) when delivering over SMTP with",
215 "; STARTTLSTLS.",
216 fmt.Sprintf(`mta-sts.%s. CNAME %s.`, d, h),
217 fmt.Sprintf(`_mta-sts.%s. TXT "v=STSv1; id=%s"`, d, sts.PolicyID),
218 "",
219 )
220 } else {
221 records = append(records,
222 "; Note: No MTA-STS to indicate TLS should be used. Either because disabled for the",
223 "; domain or because mox.conf does not have a listener with MTA-STS configured.",
224 "",
225 )
226 }
227
228 if domConf.TLSRPT != nil {
229 uri := url.URL{
230 Scheme: "mailto",
231 Opaque: smtp.NewAddress(domConf.TLSRPT.ParsedLocalpart, domConf.TLSRPT.DNSDomain).Pack(false),
232 }
233 tlsrptr := tlsrpt.Record{Version: "TLSRPTv1", RUAs: [][]tlsrpt.RUA{{tlsrpt.RUA(uri.String())}}}
234 records = append(records,
235 "; Request reporting about TLS failures.",
236 fmt.Sprintf(`_smtp._tls.%s. TXT "%s"`, d, tlsrptr.String()),
237 "",
238 )
239 }
240
241 if domConf.ClientSettingsDomain != "" && domConf.ClientSettingsDNSDomain != mox.Conf.Static.HostnameDomain {
242 records = append(records,
243 "; Client settings will reference a subdomain of the hosted domain, making it",
244 "; easier to migrate to a different server in the future by not requiring settings",
245 "; in all clients to be updated.",
246 fmt.Sprintf(`%-*s CNAME %s.`, 20+len(d), domConf.ClientSettingsDNSDomain.ASCII+".", h),
247 "",
248 )
249 }
250
251 records = append(records,
252 "; Autoconfig is used by Thunderbird. Autodiscover is (in theory) used by Microsoft.",
253 fmt.Sprintf(`autoconfig.%s. CNAME %s.`, d, h),
254 fmt.Sprintf(`_autodiscover._tcp.%s. SRV 0 1 443 %s.`, d, h),
255 "",
256
257 // ../rfc/6186:133 ../rfc/8314:692
258 "; For secure IMAP and submission autoconfig, point to mail host.",
259 fmt.Sprintf(`_imaps._tcp.%s. SRV 0 1 993 %s.`, d, h),
260 fmt.Sprintf(`_submissions._tcp.%s. SRV 0 1 465 %s.`, d, h),
261 "",
262 // ../rfc/6186:242
263 "; Next records specify POP3 and non-TLS ports are not to be used.",
264 "; These are optional and safe to leave out (e.g. if you have to click a lot in a",
265 "; DNS admin web interface).",
266 fmt.Sprintf(`_imap._tcp.%s. SRV 0 0 0 .`, d),
267 fmt.Sprintf(`_submission._tcp.%s. SRV 0 0 0 .`, d),
268 fmt.Sprintf(`_pop3._tcp.%s. SRV 0 0 0 .`, d),
269 fmt.Sprintf(`_pop3s._tcp.%s. SRV 0 0 0 .`, d),
270 )
271
272 if certIssuerDomainName != "" {
273 // ../rfc/8659:18 for CAA records.
274 records = append(records,
275 "",
276 "; Optional:",
277 "; You could mark Let's Encrypt as the only Certificate Authority allowed to",
278 "; sign TLS certificates for your domain.",
279 fmt.Sprintf(`%s. CAA 0 issue "%s"`, d, certIssuerDomainName),
280 )
281 if acmeAccountURI != "" {
282 // ../rfc/8657:99 for accounturi.
283 // ../rfc/8657:147 for validationmethods.
284 records = append(records,
285 ";",
286 "; Optionally limit certificates for this domain to the account ID and methods used by mox.",
287 fmt.Sprintf(`;; %s. CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, d, certIssuerDomainName, acmeAccountURI),
288 ";",
289 "; Or alternatively only limit for email-specific subdomains, so you can use",
290 "; other accounts/methods for other subdomains.",
291 fmt.Sprintf(`;; autoconfig.%s. CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, d, certIssuerDomainName, acmeAccountURI),
292 fmt.Sprintf(`;; mta-sts.%s. CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, d, certIssuerDomainName, acmeAccountURI),
293 )
294 if domConf.ClientSettingsDomain != "" && domConf.ClientSettingsDNSDomain != mox.Conf.Static.HostnameDomain {
295 records = append(records,
296 fmt.Sprintf(`;; %-*s CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, 20-3+len(d), domConf.ClientSettingsDNSDomain.ASCII, certIssuerDomainName, acmeAccountURI),
297 )
298 }
299 if strings.HasSuffix(h, "."+d) {
300 records = append(records,
301 ";",
302 "; And the mail hostname.",
303 fmt.Sprintf(`;; %-*s CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, 20-3+len(d), h+".", certIssuerDomainName, acmeAccountURI),
304 )
305 }
306 } else {
307 // The string "will be suggested" is used by
308 // ../testdata/integration/moxacmepebble.sh and ../testdata/integration/moxmail2.sh
309 // as end of DNS records.
310 records = append(records,
311 ";",
312 "; Note: After starting up, once an ACME account has been created, CAA records",
313 "; that restrict issuance to the account will be suggested.",
314 )
315 }
316 }
317 return records, nil
318}
319