1package admin
2
3import (
4 "crypto"
5 "crypto/ed25519"
6 "crypto/rsa"
7 "crypto/sha256"
8 "crypto/x509"
9 "fmt"
10 "net/url"
11 "sort"
12 "strings"
13
14 "github.com/mjl-/adns"
15
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"
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 sort.Slice(selectors, func(i, j int) bool {
139 return selectors[i] < selectors[j]
140 })
141 for _, name := range selectors {
142 sel := domConf.DKIM.Selectors[name]
143 dkimr := dkim.Record{
144 Version: "DKIM1",
145 Hashes: []string{"sha256"},
146 PublicKey: sel.Key.Public(),
147 }
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)
152 }
153 txt, err := dkimr.Record()
154 if err != nil {
155 return nil, fmt.Errorf("making DKIM DNS TXT record: %v", err)
156 }
157
158 if len(txt) > 100 {
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 ().",
163 )
164 }
165 s := fmt.Sprintf("%s._domainkey.%s. TXT %s", name, d, mox.TXTStrings(txt))
166 records = append(records, s)
167
168 }
169 dmarcr := dmarc.DefaultRecord
170 dmarcr.Policy = "reject"
171 if domConf.DMARC != nil {
172 uri := url.URL{
173 Scheme: "mailto",
174 Opaque: smtp.NewAddress(domConf.DMARC.ParsedLocalpart, domConf.DMARC.DNSDomain).Pack(false),
175 }
176 dmarcr.AggregateReportAddresses = []dmarc.URI{
177 {Address: uri.String(), MaxSize: 10, Unit: "m"},
178 }
179 }
180 dspfr := spf.Record{Version: "spf1"}
181 for _, ip := range mox.DomainSPFIPs() {
182 mech := "ip4"
183 if ip.To4() == nil {
184 mech = "ip6"
185 }
186 dspfr.Directives = append(dspfr.Directives, spf.Directive{Mechanism: mech, IP: ip})
187 }
188 dspfr.Directives = append(dspfr.Directives,
189 spf.Directive{Mechanism: "mx"},
190 spf.Directive{Qualifier: "~", Mechanism: "all"},
191 )
192 dspftxt, err := dspfr.Record()
193 if err != nil {
194 return nil, fmt.Errorf("making domain spf record: %v", err)
195 }
196 records = append(records,
197 "",
198
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),
203 "",
204
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()),
210 "",
211 )
212
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",
217 "; STARTTLSTLS.",
218 fmt.Sprintf(`mta-sts.%s. CNAME %s.`, d, h),
219 fmt.Sprintf(`_mta-sts.%s. TXT "v=STSv1; id=%s"`, d, sts.PolicyID),
220 "",
221 )
222 } else {
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.",
226 "",
227 )
228 }
229
230 if domConf.TLSRPT != nil {
231 uri := url.URL{
232 Scheme: "mailto",
233 Opaque: smtp.NewAddress(domConf.TLSRPT.ParsedLocalpart, domConf.TLSRPT.DNSDomain).Pack(false),
234 }
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()),
239 "",
240 )
241 }
242
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),
249 "",
250 )
251 }
252
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),
257 "",
258
259 // ../rfc/6186:133 ../rfc/8314:692
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),
263 "",
264 // ../rfc/6186:242
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),
272 )
273
274 if certIssuerDomainName != "" {
275 // ../rfc/8659:18 for CAA records.
276 records = append(records,
277 "",
278 "; Optional:",
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),
282 )
283 if acmeAccountURI != "" {
284 // ../rfc/8657:99 for accounturi.
285 // ../rfc/8657:147 for validationmethods.
286 records = append(records,
287 ";",
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),
290 ";",
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),
295 )
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),
299 )
300 }
301 if strings.HasSuffix(h, "."+d) {
302 records = append(records,
303 ";",
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),
306 )
307 }
308 } else {
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,
313 ";",
314 "; Note: After starting up, once an ACME account has been created, CAA records",
315 "; that restrict issuance to the account will be suggested.",
316 )
317 }
318 }
319 return records, nil
320}
321