1// Package autotls automatically configures TLS (for SMTP, IMAP, HTTP) by
2// requesting certificates with ACME, typically from Let's Encrypt.
3package autotls
4
5// We do tls-alpn-01, and also http-01. For DNS we would need a third party tool
6// with an API that can make the DNS changes, as we don't want to link in dozens of
7// bespoke API's for DNS record manipulation into mox.
8
9import (
10 "bytes"
11 "context"
12 "crypto"
13 "crypto/ecdsa"
14 "crypto/elliptic"
15 cryptorand "crypto/rand"
16 "crypto/rsa"
17 "crypto/tls"
18 "crypto/x509"
19 "encoding/pem"
20 "errors"
21 "fmt"
22 "io"
23 "net"
24 "os"
25 "path/filepath"
26 "sort"
27 "strings"
28 "sync"
29 "time"
30
31 "github.com/prometheus/client_golang/prometheus"
32 "github.com/prometheus/client_golang/prometheus/promauto"
33 "golang.org/x/crypto/acme"
34 "golang.org/x/crypto/acme/autocert"
35
36 "github.com/mjl-/mox/dns"
37 "github.com/mjl-/mox/mlog"
38 "github.com/mjl-/mox/moxvar"
39)
40
41var xlog = mlog.New("autotls")
42
43var (
44 metricCertput = promauto.NewCounter(
45 prometheus.CounterOpts{
46 Name: "mox_autotls_certput_total",
47 Help: "Number of certificate store puts.",
48 },
49 )
50)
51
52// Manager is in charge of a single ACME identity, and automatically requests
53// certificates for allowlisted hosts.
54type Manager struct {
55 ACMETLSConfig *tls.Config // For serving HTTPS on port 443, which is required for certificate requests to succeed.
56 TLSConfig *tls.Config // For all TLS servers not used for validating ACME requests. Like SMTP and IMAP (including with STARTTLS) and HTTPS on ports other than 443.
57 Manager *autocert.Manager
58
59 shutdown <-chan struct{}
60
61 sync.Mutex
62 hosts map[dns.Domain]struct{}
63}
64
65// Load returns an initialized autotls manager for "name" (used for the ACME key
66// file and requested certs and their keys). All files are stored within acmeDir.
67// contactEmail must be a valid email address to which notifications about ACME can
68// be sent. directoryURL is the ACME starting point. When shutdown is closed, no
69// new TLS connections can be created.
70func Load(name, acmeDir, contactEmail, directoryURL string, shutdown <-chan struct{}) (*Manager, error) {
71 if directoryURL == "" {
72 return nil, fmt.Errorf("empty ACME directory URL")
73 }
74 if contactEmail == "" {
75 return nil, fmt.Errorf("empty contact email")
76 }
77
78 // Load identity key if it exists. Otherwise, create a new key.
79 p := filepath.Join(acmeDir + "/" + name + ".key")
80 var key crypto.Signer
81 f, err := os.Open(p)
82 if f != nil {
83 defer f.Close()
84 }
85 if err != nil && os.IsNotExist(err) {
86 key, err = ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
87 if err != nil {
88 return nil, fmt.Errorf("generating ecdsa identity key: %s", err)
89 }
90 der, err := x509.MarshalPKCS8PrivateKey(key)
91 if err != nil {
92 return nil, fmt.Errorf("marshal identity key: %s", err)
93 }
94 block := &pem.Block{
95 Type: "PRIVATE KEY",
96 Headers: map[string]string{
97 "Note": fmt.Sprintf("PEM PKCS8 ECDSA private key generated for ACME provider %s by mox", name),
98 },
99 Bytes: der,
100 }
101 b := &bytes.Buffer{}
102 if err := pem.Encode(b, block); err != nil {
103 return nil, fmt.Errorf("pem encode: %s", err)
104 } else if err := os.WriteFile(p, b.Bytes(), 0660); err != nil {
105 return nil, fmt.Errorf("writing identity key: %s", err)
106 }
107 } else if err != nil {
108 return nil, fmt.Errorf("open identity key file: %s", err)
109 } else {
110 var privKey any
111 if buf, err := io.ReadAll(f); err != nil {
112 return nil, fmt.Errorf("reading identity key: %s", err)
113 } else if p, _ := pem.Decode(buf); p == nil {
114 return nil, fmt.Errorf("no pem data")
115 } else if p.Type != "PRIVATE KEY" {
116 return nil, fmt.Errorf("got PEM block %q, expected \"PRIVATE KEY\"", p.Type)
117 } else if privKey, err = x509.ParsePKCS8PrivateKey(p.Bytes); err != nil {
118 return nil, fmt.Errorf("parsing PKCS8 private key: %s", err)
119 }
120 switch k := privKey.(type) {
121 case *ecdsa.PrivateKey:
122 key = k
123 case *rsa.PrivateKey:
124 key = k
125 default:
126 return nil, fmt.Errorf("unsupported private key type %T", key)
127 }
128 }
129
130 m := &autocert.Manager{
131 Cache: dirCache(acmeDir + "/keycerts/" + name),
132 Prompt: autocert.AcceptTOS,
133 Email: contactEmail,
134 Client: &acme.Client{
135 DirectoryURL: directoryURL,
136 Key: key,
137 UserAgent: "mox/" + moxvar.Version,
138 },
139 // HostPolicy set below.
140 }
141
142 loggingGetCertificate := func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
143 log := xlog.WithContext(hello.Context())
144
145 // Handle missing SNI to prevent logging an error below.
146 // At startup, during config initialization, we already adjust the tls config to
147 // inject the listener hostname if there isn't one in the TLS client hello. This is
148 // common for SMTP STARTTLS connections, which often do not care about the
149 // validation of the certificate.
150 if hello.ServerName == "" {
151 log.Debug("tls request without sni servername, rejecting", mlog.Field("localaddr", hello.Conn.LocalAddr()), mlog.Field("supportedprotos", hello.SupportedProtos))
152 return nil, fmt.Errorf("sni server name required")
153 }
154
155 cert, err := m.GetCertificate(hello)
156 if err != nil {
157 if errors.Is(err, errHostNotAllowed) {
158 log.Debugx("requesting certificate", err, mlog.Field("host", hello.ServerName))
159 } else {
160 log.Errorx("requesting certificate", err, mlog.Field("host", hello.ServerName))
161 }
162 }
163 return cert, err
164 }
165
166 acmeTLSConfig := *m.TLSConfig()
167 acmeTLSConfig.GetCertificate = loggingGetCertificate
168
169 tlsConfig := tls.Config{
170 GetCertificate: loggingGetCertificate,
171 }
172
173 a := &Manager{
174 ACMETLSConfig: &acmeTLSConfig,
175 TLSConfig: &tlsConfig,
176 Manager: m,
177 shutdown: shutdown,
178 hosts: map[dns.Domain]struct{}{},
179 }
180 m.HostPolicy = a.HostPolicy
181 return a, nil
182}
183
184// SetAllowedHostnames sets a new list of allowed hostnames for automatic TLS.
185// After setting the host names, a goroutine is start to check that new host names
186// are fully served by publicIPs (only if non-empty and there is no unspecified
187// address in the list). If no, log an error with a warning that ACME validation
188// may fail.
189func (m *Manager) SetAllowedHostnames(resolver dns.Resolver, hostnames map[dns.Domain]struct{}, publicIPs []string, checkHosts bool) {
190 m.Lock()
191 defer m.Unlock()
192
193 // Log as slice, sorted.
194 l := make([]dns.Domain, 0, len(hostnames))
195 for d := range hostnames {
196 l = append(l, d)
197 }
198 sort.Slice(l, func(i, j int) bool {
199 return l[i].Name() < l[j].Name()
200 })
201
202 xlog.Debug("autotls setting allowed hostnames", mlog.Field("hostnames", l), mlog.Field("publicips", publicIPs))
203 var added []dns.Domain
204 for h := range hostnames {
205 if _, ok := m.hosts[h]; !ok {
206 added = append(added, h)
207 }
208 }
209 m.hosts = hostnames
210
211 if checkHosts && len(added) > 0 && len(publicIPs) > 0 {
212 for _, ip := range publicIPs {
213 if net.ParseIP(ip).IsUnspecified() {
214 return
215 }
216 }
217 go func() {
218 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
219 defer cancel()
220
221 publicIPstrs := map[string]struct{}{}
222 for _, ip := range publicIPs {
223 publicIPstrs[ip] = struct{}{}
224 }
225
226 xlog.Debug("checking ips of hosts configured for acme tls cert validation")
227 for _, h := range added {
228 ips, err := resolver.LookupIP(ctx, "ip", h.ASCII+".")
229 if err != nil {
230 xlog.Errorx("warning: acme tls cert validation for host may fail due to dns lookup error", err, mlog.Field("host", h))
231 continue
232 }
233 for _, ip := range ips {
234 if _, ok := publicIPstrs[ip.String()]; !ok {
235 xlog.Error("warning: acme tls cert validation for host is likely to fail because not all its ips are being listened on", mlog.Field("hostname", h), mlog.Field("listenedips", publicIPs), mlog.Field("hostips", ips), mlog.Field("missingip", ip))
236 }
237 }
238 }
239 }()
240 }
241}
242
243// Hostnames returns the allowed host names for use with ACME.
244func (m *Manager) Hostnames() []dns.Domain {
245 m.Lock()
246 defer m.Unlock()
247 var l []dns.Domain
248 for h := range m.hosts {
249 l = append(l, h)
250 }
251 return l
252}
253
254var errHostNotAllowed = errors.New("autotls: host not in allowlist")
255
256// HostPolicy decides if a host is allowed for use with ACME, i.e. whether a
257// certificate will be returned if present and/or will be requested if not yet
258// present. Only hosts added with SetAllowedHostnames are allowed. During shutdown,
259// no new connections are allowed.
260func (m *Manager) HostPolicy(ctx context.Context, host string) (rerr error) {
261 log := xlog.WithContext(ctx)
262 defer func() {
263 log.WithContext(ctx).Debugx("autotls hostpolicy result", rerr, mlog.Field("host", host))
264 }()
265
266 // Don't request new TLS certs when we are shutting down.
267 select {
268 case <-m.shutdown:
269 return fmt.Errorf("shutting down")
270 default:
271 }
272
273 xhost, _, err := net.SplitHostPort(host)
274 if err == nil {
275 // For http-01, host may include a port number.
276 host = xhost
277 }
278
279 d, err := dns.ParseDomain(host)
280 if err != nil {
281 return fmt.Errorf("invalid host: %v", err)
282 }
283
284 m.Lock()
285 defer m.Unlock()
286 if _, ok := m.hosts[d]; !ok {
287 return fmt.Errorf("%w: %q", errHostNotAllowed, d)
288 }
289 return nil
290}
291
292type dirCache autocert.DirCache
293
294func (d dirCache) Delete(ctx context.Context, name string) (rerr error) {
295 log := xlog.WithContext(ctx)
296 defer func() {
297 log.Debugx("dircache delete result", rerr, mlog.Field("name", name))
298 }()
299 err := autocert.DirCache(d).Delete(ctx, name)
300 if err != nil {
301 log.Errorx("deleting cert from dir cache", err, mlog.Field("name", name))
302 } else if !strings.HasSuffix(name, "+token") {
303 log.Info("autotls cert delete", mlog.Field("name", name))
304 }
305 return err
306}
307
308func (d dirCache) Get(ctx context.Context, name string) (rbuf []byte, rerr error) {
309 log := xlog.WithContext(ctx)
310 defer func() {
311 log.Debugx("dircache get result", rerr, mlog.Field("name", name))
312 }()
313 buf, err := autocert.DirCache(d).Get(ctx, name)
314 if err != nil && errors.Is(err, autocert.ErrCacheMiss) {
315 log.Infox("getting cert from dir cache", err, mlog.Field("name", name))
316 } else if err != nil {
317 log.Errorx("getting cert from dir cache", err, mlog.Field("name", name))
318 } else if !strings.HasSuffix(name, "+token") {
319 log.Debug("autotls cert get", mlog.Field("name", name))
320 }
321 return buf, err
322}
323
324func (d dirCache) Put(ctx context.Context, name string, data []byte) (rerr error) {
325 log := xlog.WithContext(ctx)
326 defer func() {
327 log.Debugx("dircache put result", rerr, mlog.Field("name", name))
328 }()
329 metricCertput.Inc()
330 err := autocert.DirCache(d).Put(ctx, name, data)
331 if err != nil {
332 log.Errorx("storing cert in dir cache", err, mlog.Field("name", name))
333 } else if !strings.HasSuffix(name, "+token") {
334 log.Info("autotls cert store", mlog.Field("name", name))
335 }
336 return err
337}
338