at startup, with acme, if the config has explicitly configured public ips (the default with the quickstart), lookup the host names allowed for acme validation and warn about ips that mox is not configured to listen on

i've seen this cause acme validation failures 3 times now, so give a hint in
the logs to new users. also for issue #13.
This commit is contained in:
Mechiel Lukkien 2023-03-05 16:22:23 +01:00
parent 845a72d07a
commit dedc90f455
No known key found for this signature in database
5 changed files with 51 additions and 13 deletions

View file

@ -22,11 +22,13 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"net"
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
"sync" "sync"
"time"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promauto"
@ -182,7 +184,11 @@ func Load(name, acmeDir, contactEmail, directoryURL string, shutdown <-chan stru
} }
// SetAllowedHostnames sets a new list of allowed hostnames for automatic TLS. // SetAllowedHostnames sets a new list of allowed hostnames for automatic TLS.
func (m *Manager) SetAllowedHostnames(hostnames map[dns.Domain]struct{}) { // After setting the host names, a goroutine is start to check that new host names
// are fully served by publicIPs (only if non-empty and there is no unspecified
// address in the list). If no, log an error with a warning that ACME validation
// may fail.
func (m *Manager) SetAllowedHostnames(resolver dns.Resolver, hostnames map[dns.Domain]struct{}, publicIPs []string) {
m.Lock() m.Lock()
defer m.Unlock() defer m.Unlock()
@ -195,8 +201,44 @@ func (m *Manager) SetAllowedHostnames(hostnames map[dns.Domain]struct{}) {
return l[i].Name() < l[j].Name() return l[i].Name() < l[j].Name()
}) })
xlog.Debug("autotls setting allowed hostnames", mlog.Field("hostnames", l)) xlog.Debug("autotls setting allowed hostnames", mlog.Field("hostnames", l), mlog.Field("publicips", publicIPs))
var added []dns.Domain
for h := range hostnames {
if _, ok := m.hosts[h]; !ok {
added = append(added, h)
}
}
m.hosts = hostnames m.hosts = hostnames
if len(added) > 0 && len(publicIPs) > 0 {
for _, ip := range publicIPs {
if net.ParseIP(ip).IsUnspecified() {
return
}
}
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
publicIPstrs := map[string]struct{}{}
for _, ip := range publicIPs {
publicIPstrs[ip] = struct{}{}
}
for _, h := range added {
ips, err := resolver.LookupIP(ctx, "ip", h.ASCII+".")
if err != nil {
xlog.Errorx("warning: acme tls cert validation for host may fail due to dns lookup error", err, mlog.Field("host", h))
continue
}
for _, ip := range ips {
if _, ok := publicIPstrs[ip.String()]; !ok {
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))
}
}
}
}()
}
} }
// Hostnames returns the allowed host names for use with ACME. // Hostnames returns the allowed host names for use with ACME.

View file

@ -28,7 +28,7 @@ func TestAutotls(t *testing.T) {
if err := m.HostPolicy(context.Background(), "mox.example"); err == nil || !errors.Is(err, errHostNotAllowed) { if err := m.HostPolicy(context.Background(), "mox.example"); err == nil || !errors.Is(err, errHostNotAllowed) {
t.Fatalf("hostpolicy, got err %v, expected errHostNotAllowed", err) t.Fatalf("hostpolicy, got err %v, expected errHostNotAllowed", err)
} }
m.SetAllowedHostnames(map[dns.Domain]struct{}{{ASCII: "mox.example"}: {}}) m.SetAllowedHostnames(dns.StrictResolver{}, map[dns.Domain]struct{}{{ASCII: "mox.example"}: {}}, nil)
l = m.Hostnames() l = m.Hostnames()
if !reflect.DeepEqual(l, []dns.Domain{{ASCII: "mox.example"}}) { if !reflect.DeepEqual(l, []dns.Domain{{ASCII: "mox.example"}}) {
t.Fatalf("hostnames, got %v, expected single mox.example", l) t.Fatalf("hostnames, got %v, expected single mox.example", l)
@ -79,7 +79,7 @@ func TestAutotls(t *testing.T) {
t.Fatalf("private key changed after reload") t.Fatalf("private key changed after reload")
} }
m.shutdown = make(chan struct{}) m.shutdown = make(chan struct{})
m.SetAllowedHostnames(map[dns.Domain]struct{}{{ASCII: "mox.example"}: {}}) m.SetAllowedHostnames(dns.StrictResolver{}, map[dns.Domain]struct{}{{ASCII: "mox.example"}: {}}, nil)
if err := m.HostPolicy(context.Background(), "mox.example"); err != nil { if err := m.HostPolicy(context.Background(), "mox.example"); err != nil {
t.Fatalf("hostpolicy, got err %v, expected no error", err) t.Fatalf("hostpolicy, got err %v, expected no error", err)
} }

View file

@ -47,7 +47,7 @@ type Static struct {
} `sconf:"optional" sconf-doc:"Global TLS configuration, e.g. for additional Certificate Authorities."` } `sconf:"optional" sconf-doc:"Global TLS configuration, e.g. for additional Certificate Authorities."`
ACME map[string]ACME `sconf:"optional" sconf-doc:"Automatic TLS configuration with ACME, e.g. through Let's Encrypt. The key is a name referenced in TLS configs, e.g. letsencrypt."` ACME map[string]ACME `sconf:"optional" sconf-doc:"Automatic TLS configuration with ACME, e.g. through Let's Encrypt. The key is a name referenced in TLS configs, e.g. letsencrypt."`
AdminPasswordFile string `sconf:"optional" sconf-doc:"File containing hash of admin password, for authentication in the web admin pages (if enabled)."` AdminPasswordFile string `sconf:"optional" sconf-doc:"File containing hash of admin password, for authentication in the web admin pages (if enabled)."`
Listeners map[string]Listener `sconf-doc:"Listeners are groups of IP addresses and services enabled on those IP addresses, such as SMTP/IMAP or internal endpoints for administration or Prometheus metrics. All listeners with SMTP/IMAP services enabled will serve all configured domains."` Listeners map[string]Listener `sconf-doc:"Listeners are groups of IP addresses and services enabled on those IP addresses, such as SMTP/IMAP or internal endpoints for administration or Prometheus metrics. All listeners with SMTP/IMAP services enabled will serve all configured domains. If the listener is named 'public', it will get a few helpful additional configuration checks, for acme automatic tls certificates and monitoring of ips in dnsbls if those are configured."`
Postmaster struct { Postmaster struct {
Account string Account string
Mailbox string `sconf-doc:"E.g. Postmaster or Inbox."` Mailbox string `sconf-doc:"E.g. Postmaster or Inbox."`

View file

@ -89,7 +89,9 @@ describe-static" and "mox config describe-domains":
# Listeners are groups of IP addresses and services enabled on those IP addresses, # Listeners are groups of IP addresses and services enabled on those IP addresses,
# such as SMTP/IMAP or internal endpoints for administration or Prometheus # such as SMTP/IMAP or internal endpoints for administration or Prometheus
# metrics. All listeners with SMTP/IMAP services enabled will serve all configured # metrics. All listeners with SMTP/IMAP services enabled will serve all configured
# domains. # domains. If the listener is named 'public', it will get a few helpful additional
# configuration checks, for acme automatic tls certificates and monitoring of ips
# in dnsbls if those are configured.
Listeners: Listeners:
x: x:

View file

@ -227,12 +227,6 @@ func (c *Config) allowACMEHosts() {
} else { } else {
hostnames[d] = struct{}{} hostnames[d] = struct{}{}
} }
if d, err := dns.ParseDomain("autodiscover." + dom.Domain.ASCII); err != nil {
xlog.Errorx("parsing autodiscover domain", err, mlog.Field("domain", dom.Domain))
} else {
hostnames[d] = struct{}{}
}
} }
if l.MTASTSHTTPS.Enabled && dom.MTASTS != nil && !l.MTASTSHTTPS.NonTLS { if l.MTASTSHTTPS.Enabled && dom.MTASTS != nil && !l.MTASTSHTTPS.NonTLS {
@ -254,7 +248,7 @@ func (c *Config) allowACMEHosts() {
} }
} }
m.SetAllowedHostnames(hostnames) m.SetAllowedHostnames(dns.StrictResolver{Pkg: "autotls"}, hostnames, c.Static.Listeners["public"].IPs)
} }
} }