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).
This commit is contained in:
Mechiel Lukkien 2023-03-10 16:25:18 +01:00
parent 47b88550be
commit f60ad1452f
No known key found for this signature in database
5 changed files with 175 additions and 4 deletions

View file

@ -44,7 +44,7 @@ type Static struct {
CertFiles []string `sconf:"optional"` CertFiles []string `sconf:"optional"`
} `sconf:"optional"` } `sconf:"optional"`
CertPool *x509.CertPool `sconf:"-" json:"-"` 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."` 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. 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 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."`

View file

@ -49,8 +49,8 @@ describe-static" and "mox config describe-domains":
# (optional) # (optional)
CheckUpdates: false CheckUpdates: false
# Global TLS configuration, e.g. for additional Certificate Authorities. # Global TLS configuration, e.g. for additional Certificate Authorities. Used for
# (optional) # outgoing SMTP connections, HTTPS requests. (optional)
TLS: TLS:
# (optional) # (optional)

158
develop.txt Normal file
View file

@ -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 <<EOF >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 <<EOF >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/<env>/config/mox.conf and local/cfssl/.
- ../../cfssl/ca.pem
[...]
Listeners:
public:
TLS:
KeyCerts:
# Assuming local/<env>/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 <<EOF >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/<env>/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.

View file

@ -404,6 +404,7 @@ func checkDomain(ctx context.Context, resolver dns.Resolver, dialer *net.Dialer,
Config: &tls.Config{ Config: &tls.Config{
ServerName: host, ServerName: host,
MinVersion: tls.VersionTLS12, // ../rfc/8996:31 ../rfc/8997:66 MinVersion: tls.VersionTLS12, // ../rfc/8996:31 ../rfc/8997:66
RootCAs: mox.Conf.Static.TLS.CertPool,
}, },
} }
for _, ip := range ips { for _, ip := range ips {
@ -600,7 +601,11 @@ func checkDomain(ctx context.Context, resolver dns.Resolver, dialer *net.Dialer,
if !strings.HasPrefix(line, "220 ") { if !strings.HasPrefix(line, "220 ") {
return fmt.Errorf("SMTP STARTTLS response from remote not 220 OK: %q", strings.TrimSuffix(line, "\r\n")) 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 { if err := tlsconn.HandshakeContext(cctx); err != nil {
return fmt.Errorf("TLS handshake after SMTP STARTTLS: %s", err) return fmt.Errorf("TLS handshake after SMTP STARTTLS: %s", err)
} }

View file

@ -11,6 +11,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net" "net"
"net/http"
"net/url" "net/url"
"os" "os"
"os/user" "os/user"
@ -344,6 +345,13 @@ func LoadConfig(ctx context.Context, checkACMEHosts bool) []error {
func SetConfig(c *Config) { func SetConfig(c *Config) {
// Cannot just assign *c to Conf, it would copy the mutex. // 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} 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 // ParseConfig parses the static config at path p. If checkOnly is true, no changes