From f60ad1452f300c63a14d64b10be0c73832ec1dfb Mon Sep 17 00:00:00 2001 From: Mechiel Lukkien Date: Fri, 10 Mar 2023 16:25:18 +0100 Subject: [PATCH] use configured tls ca config for all tls connections, so https as well and add documentation for developers for setting up certificates with manual local CA (with cfssl) or local ACME CA (with pebble). --- config/config.go | 2 +- config/doc.go | 4 +- develop.txt | 158 +++++++++++++++++++++++++++++++++++++++++++++++ http/admin.go | 7 ++- mox-/config.go | 8 +++ 5 files changed, 175 insertions(+), 4 deletions(-) create mode 100644 develop.txt 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