diff --git a/config/config.go b/config/config.go index b5446a9..591649e 100644 --- a/config/config.go +++ b/config/config.go @@ -44,7 +44,7 @@ type Static struct { CertFiles []string `sconf:"optional"` } `sconf:"optional"` CertPool *x509.CertPool `sconf:"-" json:"-"` - } `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. Used for outgoing SMTP connections, HTTPS requests."` 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)."` 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."` diff --git a/config/doc.go b/config/doc.go index cf5511c..9f8b7d5 100644 --- a/config/doc.go +++ b/config/doc.go @@ -49,8 +49,8 @@ describe-static" and "mox config describe-domains": # (optional) CheckUpdates: false - # Global TLS configuration, e.g. for additional Certificate Authorities. - # (optional) + # Global TLS configuration, e.g. for additional Certificate Authorities. Used for + # outgoing SMTP connections, HTTPS requests. (optional) TLS: # (optional) diff --git a/develop.txt b/develop.txt new file mode 100644 index 0000000..148eb60 --- /dev/null +++ b/develop.txt @@ -0,0 +1,158 @@ +This file has notes useful for mox developers. + +# TLS certificates + +https://github.com/cloudflare/cfssl is useful for testing with TLS +certificates. Create a CA and configure it in mox.conf TLS.CA.CertFiles, and +sign host certificates and configure them in the listeners TLS.KeyCerts. + +Setup a local CA with cfssl, run once: + +```sh +go install github.com/cloudflare/cfssl/cmd/cfssl@latest +go install github.com/cloudflare/cfssl/cmd/cfssljson@latest + +mkdir -p local/cfssl +cd local/cfssl + +cfssl print-defaults config > ca-config.json # defaults are fine + +# Based on: cfssl print-defaults csr > ca-csr.json +cat <ca-csr.json +{ + "CN": "mox ca", + "key": { + "algo": "ecdsa", + "size": 256 + }, + "names": [ + { + "C": "NL" + } + ] +} +EOF + +cfssl gencert -initca ca-csr.json | cfssljson -bare ca - # Generate ca key and cert. + +# Generate wildcard certificates for one or more domains, add localhost for use with pebble, see below. +domains="moxtest.example localhost" +for domain in $domains; do + cat <wildcard.$domain.csr.json +{ + "key": { + "algo": "ecdsa", + "size": 256 + }, + "names": [ + { + "O": "mox" + } + ], + "hosts": [ + "$domain", + "*.$domain" + ] +} +EOF + cfssl gencert -ca ca.pem -ca-key ca-key.pem -profile=www wildcard.$domain.csr.json | cfssljson -bare wildcard.$domain +done +``` + +Now configure mox.conf to add the cfssl CA root certificate: + +``` +TLS: + CA: + AdditionalToSystem: true + CertFiles: + # Assuming local//config/mox.conf and local/cfssl/. + - ../../cfssl/ca.pem + +[...] + +Listeners: + public: + TLS: + KeyCerts: + # Assuming local//config/mox.conf and local/cfssl/. + CertFile: ../../cfssl/wildcard.$domain.pem + KeyFile: ../../cfssl/wildcard.$domain-key.pem +``` + +# ACME + +https://github.com/letsencrypt/pebble is useful for testing with ACME. Start a +pebble instance that uses the localhost TLS cert/key created by cfssl for its +TLS serving. Pebble generates a new CA certificate for its own use each time it +is started. Fetch it from https://localhost:14000/root, write it to a file, and +add it to mox.conf TLS.CA.CertFiles. See below. + +Setup pebble, run once: + +```sh +go install github.com/letsencrypt/pebble/cmd/pebble@latest + +mkdir -p local/pebble +cat <local/pebble/config.json +{ + "pebble": { + "listenAddress": "localhost:14000", + "managementListenAddress": "localhost:15000", + "certificate": "local/cfssl/localhost.pem", + "privateKey": "local/cfssl/localhost-key.pem", + "httpPort": 80, + "tlsPort": 443, + "ocspResponderURL": "", + "externalAccountBindingRequired": false + } +} +EOF +``` + +Start pebble, this generates a new temporary pebble CA certificate: + +```sh +pebble -config local/pebble/config.json +``` + +Write new CA bundle that includes pebble's temporary CA cert: + +```sh +export CURL_CA_BUNDLE=local/ca-bundle.pem # for curl +export SSL_CERT_FILE=local/ca-bundle.pem # for go apps +cat /etc/ssl/certs/ca-certificates.crt local/cfssl/ca.pem >local/ca-bundle.pem +curl https://localhost:14000/root >local/pebble/ca.pem # fetch temp pebble ca, DO THIS EVERY TIME PEBBLE IS RESTARTED! +cat /etc/ssl/certs/ca-certificates.crt local/cfssl/ca.pem local/pebble/ca.pem >local/ca-bundle.pem # create new list that includes cfssl ca and temp pebble ca. +rm -r local/*/data/acme/keycerts/pebble # remove existing pebble-signed certs in acme cert/key cache, they are invalid due to newly generated temp pebble ca. +``` + +Edit mox.conf, adding pebble ACME and its ca.pem: + +``` +ACME: + pebble: + DirectoryURL: https://localhost:14000/dir + ContactEmail: root@mox.example +TLS: + CA: + AdditionalToSystem: true + CertFiles: + # Assuming local//config/mox.conf and local/pebble/ca.pem and local/cfssl/ca.pem. + - ../../pebble/ca.pem + - ../../cfssl/ca.pem + +[...] + +Listeners: + public: + TLS: + ACME: pebble +``` + +For mail clients and browsers to accept pebble-signed certificates, you must add +the temporary pebble CA cert to their trusted root CA store each time pebble is +started (e.g. to your thunderbird/firefox testing profile). Pebble has no option +to not regenerate its CA certificate, presumably for fear of people using it for +non-testing purposes. Unfortunately, this also makes it inconvenient to use for +testing purposes. diff --git a/http/admin.go b/http/admin.go index d1bde8a..320773f 100644 --- a/http/admin.go +++ b/http/admin.go @@ -404,6 +404,7 @@ func checkDomain(ctx context.Context, resolver dns.Resolver, dialer *net.Dialer, Config: &tls.Config{ ServerName: host, MinVersion: tls.VersionTLS12, // ../rfc/8996:31 ../rfc/8997:66 + RootCAs: mox.Conf.Static.TLS.CertPool, }, } for _, ip := range ips { @@ -600,7 +601,11 @@ func checkDomain(ctx context.Context, resolver dns.Resolver, dialer *net.Dialer, if !strings.HasPrefix(line, "220 ") { return fmt.Errorf("SMTP STARTTLS response from remote not 220 OK: %q", strings.TrimSuffix(line, "\r\n")) } - tlsconn := tls.Client(conn, &tls.Config{ServerName: host}) + config := &tls.Config{ + ServerName: host, + RootCAs: mox.Conf.Static.TLS.CertPool, + } + tlsconn := tls.Client(conn, config) if err := tlsconn.HandshakeContext(cctx); err != nil { return fmt.Errorf("TLS handshake after SMTP STARTTLS: %s", err) } diff --git a/mox-/config.go b/mox-/config.go index 6d91214..ddd914d 100644 --- a/mox-/config.go +++ b/mox-/config.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "net" + "net/http" "net/url" "os" "os/user" @@ -344,6 +345,13 @@ func LoadConfig(ctx context.Context, checkACMEHosts bool) []error { func SetConfig(c *Config) { // Cannot just assign *c to Conf, it would copy the mutex. Conf = Config{c.Static, sync.Mutex{}, c.Log, sync.Mutex{}, c.Dynamic, c.dynamicMtime, c.DynamicLastCheck, c.accountDestinations} + + // If we have non-standard CA roots, use them for all HTTPS requests. + if Conf.Static.TLS.CertPool != nil { + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{ + RootCAs: Conf.Static.TLS.CertPool, + } + } } // ParseConfig parses the static config at path p. If checkOnly is true, no changes