From afc9f9686ae3f84664c30cc799ea9f6de26e9de6 Mon Sep 17 00:00:00 2001 From: "A. Schippers" Date: Fri, 29 May 2020 21:30:07 +0200 Subject: [PATCH] Publish MTA-STS policy for incoming mail (#1731) Co-authored-by: Daniel Mabbett --- CHANGELOG.md | 6 ++++++ conf/mta-sts.txt | 4 ++++ conf/nginx-alldomains.conf | 3 +++ management/dns_update.py | 39 +++++++++++++++++++++++++++++++++++++- management/web_update.py | 3 +++ security.md | 6 ++++++ setup/start.sh | 6 ++++++ setup/web.sh | 17 +++++++++++++++-- 8 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 conf/mta-sts.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cd9e72..b792510 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,12 @@ Web: * Add a new hidden feature to set nginx alias in www/custom.yaml. +MTA-STS: + +* Added support for client side MTA-STS when there is a valid SSL Certificate on the primary domain +* Automatically adds reporting when alias "tlsrpt@" is added. +* Starts default on 'testing', but changes will be kept between MiaB Upgrades. + Setup: * Improved error handling. diff --git a/conf/mta-sts.txt b/conf/mta-sts.txt new file mode 100644 index 0000000..376102b --- /dev/null +++ b/conf/mta-sts.txt @@ -0,0 +1,4 @@ +version: STSv1 +mode: MODE +mx: PRIMARY_HOSTNAME +max_age: 86400 \ No newline at end of file diff --git a/conf/nginx-alldomains.conf b/conf/nginx-alldomains.conf index 1b3ad5a..4c81e3f 100644 --- a/conf/nginx-alldomains.conf +++ b/conf/nginx-alldomains.conf @@ -21,6 +21,9 @@ location = /mail/config-v1.1.xml { alias /var/lib/mailinabox/mozilla-autoconfig.xml; } + location = /.well-known/mta-sts.txt { + alias /var/lib/mailinabox/mta-sts.txt; + } # Roundcube Webmail configuration. rewrite ^/mail$ /mail/ redirect; diff --git a/management/dns_update.py b/management/dns_update.py index 7d053d5..822af4d 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -9,8 +9,9 @@ import ipaddress import rtyaml import dns.resolver -from mailconfig import get_mail_domains +from mailconfig import get_mail_domains, get_mail_aliases from utils import shell, load_env_vars_from_file, safe_domain_name, sort_domains +from ssl_certificates import get_ssl_certificates, check_certificate # From https://stackoverflow.com/questions/3026957/how-to-validate-a-domain-name-using-regex-php/16491074#16491074 # This regular expression matches domain names according to RFCs, it also accepts fqdn with an leading dot, @@ -303,6 +304,42 @@ def build_zone(domain, all_domains, additional_records, www_redirect_domains, en if not has_rec(qname, rtype): records.append((qname, rtype, value, explanation)) + # If this is a domain name that there are email addresses configured for, i.e. "something@" + # this domain name, then the domain name is a MTA-STS (https://tools.ietf.org/html/rfc8461) + # Policy Domain. + # + # A "_mta-sts" TXT record signals the presence of a MTA-STS policy, and an effectively random policy + # ID is used to signal that a new policy may (or may not) be deployed any time the DNS is + # updated. + # + # The policy itself is served at the "mta-sts" (no underscore) subdomain over HTTPS. The + # TLS certificate used by Postfix for STARTTLS must be a valid certificate for the MX + # name (PRIMARY_HOSTNAME), so we do not set an MTA-STS policy if the certificate is not + # valid (e.g. because it is self-signed and a valid certificate has not yet been provisioned). + get_prim_cert = get_ssl_certificates(env)[env['PRIMARY_HOSTNAME']] + response = check_certificate(env['PRIMARY_HOSTNAME'], get_prim_cert['certificate'],get_prim_cert['private-key']) + + if response[0] == 'OK' and domain in get_mail_domains(env): + mta_sts_records = [ + ("mta-sts", "A", env["PUBLIC_IP"], "Provides MTA-STS support"), + ("mta-sts", "AAAA", env.get('PUBLIC_IPV6'), "Provides MTA-STS support"), + ("_mta-sts", "TXT", "v=STSv1; id=%sZ" % datetime.datetime.now().strftime("%Y%m%d%H%M%S"), "Enables MTA-STS support") + ] + # Rules can be custom configured accoring to https://tools.ietf.org/html/rfc8460. + # Skip if the rules below if the user has set a custom _smtp._tls record. + if not has_rec("_smtp._tls", "TXT", prefix="v=TLSRPTv1;"): + # if the alias 'tlsrpt@PRIMARY_HOSTNAME' is configured, automaticly, reporting will be enabled to this email address + tls_rpt_email = "tlsrpt@%s" % env['PRIMARY_HOSTNAME'] + tls_rpt_string = "" + for alias in get_mail_aliases(env): + if alias[0] == tls_rpt_email: + tls_rpt_string = " rua=mailto:%s" % tls_rpt_email + mta_sts_records.append(("_smtp._tls", "TXT", "v=TLSRPTv1;%s" % tls_rpt_string, "For reporting, add an email alias: 'tlsrpt@%s' or add a custom TXT record like 'v=TLSRPTv1; rua=mailto:[youremail]@%s' for reporting" % (env["PRIMARY_HOSTNAME"], env["PRIMARY_HOSTNAME"]))) + for qname, rtype, value, explanation in mta_sts_records: + if value is None or value.strip() == "": continue # skip IPV6 if not set + if not has_rec(qname, rtype): + records.append((qname, rtype, value, explanation)) + # Sort the records. The None records *must* go first in the nsd zone file. Otherwise it doesn't matter. records.sort(key = lambda rec : list(reversed(rec[0].split(".")) if rec[0] is not None else "")) diff --git a/management/web_update.py b/management/web_update.py index e2498e7..4a07dc9 100644 --- a/management/web_update.py +++ b/management/web_update.py @@ -30,6 +30,9 @@ def get_web_domains(env, include_www_redirects=True, exclude_dns_elsewhere=True) domains |= set('autoconfig.' + maildomain for maildomain in get_mail_domains(env)) domains |= set('autodiscover.' + maildomain for maildomain in get_mail_domains(env)) + # 'mta-sts.' for MTA-STS support. + domains |= set('mta-sts.' + maildomain for maildomain in get_mail_domains(env)) + if exclude_dns_elsewhere: # ...Unless the domain has an A/AAAA record that maps it to a different # IP address than this box. Remove those domains from our list. diff --git a/security.md b/security.md index 8c9d43e..ae77f33 100644 --- a/security.md +++ b/security.md @@ -109,6 +109,12 @@ As discussed above, there is no way to require on-the-wire encryption of mail. W When DNSSEC is enabled at the box's domain name's registrar, [DANE TLSA](https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities) records are automatically published in DNS. Senders supporting DANE will enforce encryption on-the-wire between them and the box --- see the section on DANE for outgoing mail above. ([source](management/dns_update.py)) +### MTA-STS + +SMTP MTA Strict Transport Security ([SMTP MTA-STS for short](https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol#SMTP_MTA_Strict_Transport_Security)). + +MTA-STS is a mechanism that instructs an SMTP server that the communication with the other SMTP server MUST be encrypted and that the domain name on the certificate should match the domain in the policy. It uses a combination of DNS and HTTPS to publish a policy that tells the sending party what to do when an encrypted channel can not be negotiated. + ### Filters Incoming mail is run through several filters. Email is bounced if the sender's IP address is listed in the [Spamhaus Zen blacklist](http://www.spamhaus.org/zen/) or if the sender's domain is listed in the [Spamhaus Domain Block List](http://www.spamhaus.org/dbl/). Greylisting (with [postgrey](http://postgrey.schweikert.ch/)) is also used to cut down on spam. ([source](setup/mail-postfix.sh)) diff --git a/setup/start.sh b/setup/start.sh index 0b14502..9e2db21 100755 --- a/setup/start.sh +++ b/setup/start.sh @@ -82,6 +82,11 @@ if [ ! -f $STORAGE_ROOT/mailinabox.version ]; then chown $STORAGE_USER.$STORAGE_USER $STORAGE_ROOT/mailinabox.version fi +# Default policy (initial) for MTA_STS = testing in the current state of inclusion. +# it can be changed to "none", "testing" or "enforce". With this extention, this is preserved by +# future upgrades + +MTA_STS="${DEFAULT_MTA_STS:-testing}" # Save the global options in /etc/mailinabox.conf so that standalone # tools know where to look for data. @@ -93,6 +98,7 @@ PUBLIC_IP=$PUBLIC_IP PUBLIC_IPV6=$PUBLIC_IPV6 PRIVATE_IP=$PRIVATE_IP PRIVATE_IPV6=$PRIVATE_IPV6 +MTA_STS=$MTA_STS EOF # Start service configuration. diff --git a/setup/web.sh b/setup/web.sh index e6aac6e..c384c00 100755 --- a/setup/web.sh +++ b/setup/web.sh @@ -19,7 +19,7 @@ fi echo "Installing Nginx (web server)..." -apt_install nginx php-cli php-fpm +apt_install nginx php-cli php-fpm idn2 rm -f /etc/nginx/sites-enabled/default @@ -122,6 +122,20 @@ cat conf/mozilla-autoconfig.xml \ > /var/lib/mailinabox/mozilla-autoconfig.xml chmod a+r /var/lib/mailinabox/mozilla-autoconfig.xml +# Create a generic mta-sts.txt file which is exposed via the +# nginx configuration at /.well-known/mta-sts.txt +# more documentation is available on: +# https://www.uriports.com/blog/mta-sts-explained/ +# default mode is "testing", which means: "Messages will be delivered as +# though there was no failure but a report will be sent if TLS-RPT is configured" +# other valid modes are: "enforce" and "none". +PUNY_PRIMARY_HOSTNAME=$(echo "$PRIMARY_HOSTNAME" | idn2) +cat conf/mta-sts.txt \ + | sed "s/MODE/$MTA_STS/" \ + | sed "s/PRIMARY_HOSTNAME/$PUNY_PRIMARY_HOSTNAME/" \ + > /var/lib/mailinabox/mta-sts.txt +chmod a+r /var/lib/mailinabox/mta-sts.txt + # make a default homepage if [ -d $STORAGE_ROOT/www/static ]; then mv $STORAGE_ROOT/www/static $STORAGE_ROOT/www/default; fi # migration #NODOC mkdir -p $STORAGE_ROOT/www/default @@ -137,4 +151,3 @@ restart_service php7.2-fpm # Open ports. ufw_allow http ufw_allow https -