From 4f96e991449b52400b5a1fe8d968f07ff9346842 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sun, 29 Aug 2021 17:40:37 +0200 Subject: [PATCH 01/23] MTA-STS (use rather than publish policies) --- core/postfix/Dockerfile | 5 +++++ core/postfix/conf/main.cf | 2 +- core/postfix/mta-sts-daemon.yml | 10 ++++++++++ core/postfix/start.py | 10 ++++++++++ 4 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 core/postfix/mta-sts-daemon.yml diff --git a/core/postfix/Dockerfile b/core/postfix/Dockerfile index 062155c1..8efe5da4 100644 --- a/core/postfix/Dockerfile +++ b/core/postfix/Dockerfile @@ -12,10 +12,15 @@ RUN pip3 install socrate==0.2.0 RUN pip3 install "podop>0.2.5" # Image specific layers under this line +RUN apk add --no-cache --virtual .build-deps gcc musl-dev python3-dev +RUN pip3 install --no-binary :all: postfix-mta-sts-resolver==1.0.1 +RUN apk del .build-deps gcc musl-dev python3-dev + RUN apk add --no-cache postfix postfix-pcre cyrus-sasl-login COPY conf /conf COPY start.py /start.py +COPY mta-sts-daemon.yml /etc/ EXPOSE 25/tcp 10025/tcp VOLUME ["/queue"] diff --git a/core/postfix/conf/main.cf b/core/postfix/conf/main.cf index 7f84ade7..0194324f 100644 --- a/core/postfix/conf/main.cf +++ b/core/postfix/conf/main.cf @@ -59,7 +59,7 @@ tls_ssl_options = NO_COMPRESSION, NO_TICKET smtp_tls_mandatory_protocols = !SSLv2, !SSLv3 smtp_tls_protocols =!SSLv2,!SSLv3 smtp_tls_security_level = {{ OUTBOUND_TLS_LEVEL|default('may') }} -smtp_tls_policy_maps=hash:/etc/postfix/tls_policy.map +smtp_tls_policy_maps=hash:/etc/postfix/tls_policy.map, socketmap:unix:/tmp/mta-sts.socket:postfix smtp_tls_CApath = /etc/ssl/certs smtp_tls_session_cache_database = lmdb:/dev/shm/postfix/smtp_scache smtpd_tls_session_cache_database = lmdb:/dev/shm/postfix/smtpd_scache diff --git a/core/postfix/mta-sts-daemon.yml b/core/postfix/mta-sts-daemon.yml new file mode 100644 index 00000000..39f60e48 --- /dev/null +++ b/core/postfix/mta-sts-daemon.yml @@ -0,0 +1,10 @@ +path: "/tmp/mta-sts.socket" +mode: 0600 +shutdown_timeout: 20 +cache: + type: internal + options: + cache_size: 10000 +default_zone: + strict_testing: false + timeout: 4 diff --git a/core/postfix/start.py b/core/postfix/start.py index 799d42f5..50565e3d 100755 --- a/core/postfix/start.py +++ b/core/postfix/start.py @@ -30,6 +30,12 @@ def start_podop(): ("senderrate", "url", url + "sender/rate/§") ]) +def start_mta_sts_daemon(): + os.chmod("/root/", 0o755) # read access to /root/.netrc required + os.setuid(getpwnam('postfix').pw_uid) + from postfix_mta_sts_resolver import daemon + daemon.main() + def is_valid_postconf_line(line): return not line.startswith("#") \ and not line == '' @@ -68,6 +74,9 @@ for map_file in glob.glob("/overrides/*.map"): os.system("postmap {}".format(destination)) os.remove(destination) +if os.path.exists("/overrides/mta-sts-daemon.yml"): + shutil.copyfile("/overrides/mta-sts-daemon.yml", "/etc/mta-sts-daemon.yml") + if not os.path.exists("/etc/postfix/tls_policy.map.db"): with open("/etc/postfix/tls_policy.map", "w") as f: for domain in ['gmail.com', 'yahoo.com', 'hotmail.com', 'aol.com', 'outlook.com', 'comcast.net', 'icloud.com', 'msn.com', 'hotmail.co.uk', 'live.com', 'yahoo.co.in', 'me.com', 'mail.ru', 'cox.net', 'yahoo.co.uk', 'verizon.net', 'ymail.com', 'hotmail.it', 'kw.com', 'yahoo.com.tw', 'mac.com', 'live.se', 'live.nl', 'yahoo.com.br', 'googlemail.com', 'libero.it', 'web.de', 'allstate.com', 'btinternet.com', 'online.no', 'yahoo.com.au', 'live.dk', 'earthlink.net', 'yahoo.fr', 'yahoo.it', 'gmx.de', 'hotmail.fr', 'shawinc.com', 'yahoo.de', 'moe.edu.sg', 'naver.com', 'bigpond.com', 'statefarm.com', 'remax.net', 'rocketmail.com', 'live.no', 'yahoo.ca', 'bigpond.net.au', 'hotmail.se', 'gmx.at', 'live.co.uk', 'mail.com', 'yahoo.in', 'yandex.ru', 'qq.com', 'charter.net', 'indeedemail.com', 'alice.it', 'hotmail.de', 'bluewin.ch', 'optonline.net', 'wp.pl', 'yahoo.es', 'hotmail.no', 'pindotmedia.com', 'orange.fr', 'live.it', 'yahoo.co.id', 'yahoo.no', 'hotmail.es', 'morganstanley.com', 'wellsfargo.com', 'wanadoo.fr', 'facebook.com', 'yahoo.se', 'fema.dhs.gov', 'rogers.com', 'yahoo.com.hk', 'live.com.au', 'nic.in', 'nab.com.au', 'ubs.com', 'shaw.ca', 'umich.edu', 'westpac.com.au', 'yahoo.com.mx', 'yahoo.com.sg', 'farmersagent.com', 'yahoo.dk', 'dhs.gov']: @@ -81,6 +90,7 @@ if "RELAYUSER" in os.environ: # Run Podop and Postfix multiprocessing.Process(target=start_podop).start() +multiprocessing.Process(target=start_mta_sts_daemon).start() os.system("/usr/libexec/postfix/post-install meta_directory=/etc/postfix create-missing") # Before starting postfix, we need to check permissions on /queue # in the event that postfix,postdrop id have changed From 52d3a338751c6f7dd14c63de6c2164d3a9d9f8da Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sun, 29 Aug 2021 17:41:55 +0200 Subject: [PATCH 02/23] Remove the domains that have a valid MTA-STS policy gmail.com comcast.net mail.ru googlemail.com wp.pl --- core/postfix/start.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/postfix/start.py b/core/postfix/start.py index 50565e3d..de559b27 100755 --- a/core/postfix/start.py +++ b/core/postfix/start.py @@ -79,7 +79,7 @@ if os.path.exists("/overrides/mta-sts-daemon.yml"): if not os.path.exists("/etc/postfix/tls_policy.map.db"): with open("/etc/postfix/tls_policy.map", "w") as f: - for domain in ['gmail.com', 'yahoo.com', 'hotmail.com', 'aol.com', 'outlook.com', 'comcast.net', 'icloud.com', 'msn.com', 'hotmail.co.uk', 'live.com', 'yahoo.co.in', 'me.com', 'mail.ru', 'cox.net', 'yahoo.co.uk', 'verizon.net', 'ymail.com', 'hotmail.it', 'kw.com', 'yahoo.com.tw', 'mac.com', 'live.se', 'live.nl', 'yahoo.com.br', 'googlemail.com', 'libero.it', 'web.de', 'allstate.com', 'btinternet.com', 'online.no', 'yahoo.com.au', 'live.dk', 'earthlink.net', 'yahoo.fr', 'yahoo.it', 'gmx.de', 'hotmail.fr', 'shawinc.com', 'yahoo.de', 'moe.edu.sg', 'naver.com', 'bigpond.com', 'statefarm.com', 'remax.net', 'rocketmail.com', 'live.no', 'yahoo.ca', 'bigpond.net.au', 'hotmail.se', 'gmx.at', 'live.co.uk', 'mail.com', 'yahoo.in', 'yandex.ru', 'qq.com', 'charter.net', 'indeedemail.com', 'alice.it', 'hotmail.de', 'bluewin.ch', 'optonline.net', 'wp.pl', 'yahoo.es', 'hotmail.no', 'pindotmedia.com', 'orange.fr', 'live.it', 'yahoo.co.id', 'yahoo.no', 'hotmail.es', 'morganstanley.com', 'wellsfargo.com', 'wanadoo.fr', 'facebook.com', 'yahoo.se', 'fema.dhs.gov', 'rogers.com', 'yahoo.com.hk', 'live.com.au', 'nic.in', 'nab.com.au', 'ubs.com', 'shaw.ca', 'umich.edu', 'westpac.com.au', 'yahoo.com.mx', 'yahoo.com.sg', 'farmersagent.com', 'yahoo.dk', 'dhs.gov']: + for domain in ['yahoo.com', 'hotmail.com', 'aol.com', 'outlook.com', 'icloud.com', 'msn.com', 'hotmail.co.uk', 'live.com', 'yahoo.co.in', 'me.com', 'cox.net', 'yahoo.co.uk', 'verizon.net', 'ymail.com', 'hotmail.it', 'kw.com', 'yahoo.com.tw', 'mac.com', 'live.se', 'live.nl', 'yahoo.com.br', 'libero.it', 'web.de', 'allstate.com', 'btinternet.com', 'online.no', 'yahoo.com.au', 'live.dk', 'earthlink.net', 'yahoo.fr', 'yahoo.it', 'gmx.de', 'hotmail.fr', 'shawinc.com', 'yahoo.de', 'moe.edu.sg', 'naver.com', 'bigpond.com', 'statefarm.com', 'remax.net', 'rocketmail.com', 'live.no', 'yahoo.ca', 'bigpond.net.au', 'hotmail.se', 'gmx.at', 'live.co.uk', 'mail.com', 'yahoo.in', 'yandex.ru', 'qq.com', 'charter.net', 'indeedemail.com', 'alice.it', 'hotmail.de', 'bluewin.ch', 'optonline.net', 'yahoo.es', 'hotmail.no', 'pindotmedia.com', 'orange.fr', 'live.it', 'yahoo.co.id', 'yahoo.no', 'hotmail.es', 'morganstanley.com', 'wellsfargo.com', 'wanadoo.fr', 'facebook.com', 'yahoo.se', 'fema.dhs.gov', 'rogers.com', 'yahoo.com.hk', 'live.com.au', 'nic.in', 'nab.com.au', 'ubs.com', 'shaw.ca', 'umich.edu', 'westpac.com.au', 'yahoo.com.mx', 'yahoo.com.sg', 'farmersagent.com', 'yahoo.dk', 'dhs.gov']: f.write(f'{domain}\tsecure\n') os.system("postmap /etc/postfix/tls_policy.map") From a01960787362ed3fc5e7774c3bc573b3c52a86d9 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sun, 29 Aug 2021 17:46:28 +0200 Subject: [PATCH 03/23] towncrier --- towncrier/newsfragments/1798.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 towncrier/newsfragments/1798.feature diff --git a/towncrier/newsfragments/1798.feature b/towncrier/newsfragments/1798.feature new file mode 100644 index 00000000..1b63a85c --- /dev/null +++ b/towncrier/newsfragments/1798.feature @@ -0,0 +1 @@ +Implement MTA-STS (use published policies) From 5634354911bf645ea6b8204af4964d51e06178db Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sun, 29 Aug 2021 18:28:56 +0200 Subject: [PATCH 04/23] document how to publish an MTA-STS policy --- docs/faq.rst | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/faq.rst b/docs/faq.rst index a2c6bd33..98026ab1 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -369,6 +369,31 @@ How do I use webdav (radicale)? .. _`575`: https://github.com/Mailu/Mailu/issues/575 .. _`1591`: https://github.com/Mailu/Mailu/issues/1591 +How do I setup a MTA-STS policy? +```````````````````````````````` + +Mailu can serve an `MTA-STS policy`_; To configure it you will need to: + +1. setup the appropriate DNS/CNAME record (``mta-sts.example.com`` -> ``mailu.example.com``) and DNS/TXT record (``_mta-sts.example.com`` -> ``v=STSv1; id=1``) paying attention to the ``TTL`` as this is used by MTA-STS. + +2. configure an override with the policy itself; for example, your ``overrides/mta-sts.conf`` could read: + +.. code-block:: bash + + location ^~ /.well-known/mta-sts.txt { + return 200 "version: STSv1 + mode: enforce + max_age: 86401 + mx: mailu.example.com\r\n"; + } + +3. add ``mta-sts.example.com`` to the ``HOSTNAMES`` configuration variable (and ensure that a valid SSL certificate is available for it) + +*issue reference:* `1798`_. + +.. _`1798`: https://github.com/Mailu/Mailu/issues/1798 +.. _`MTA-STS policy`: https://datatracker.ietf.org/doc/html/rfc8461 + Technical issues ---------------- From 5efe35329be9862cf4dcb34292b7b5a9b1976b5f Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sun, 29 Aug 2021 18:29:44 +0200 Subject: [PATCH 05/23] doh --- docs/faq.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.rst b/docs/faq.rst index 98026ab1..92b208de 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -376,7 +376,7 @@ Mailu can serve an `MTA-STS policy`_; To configure it you will need to: 1. setup the appropriate DNS/CNAME record (``mta-sts.example.com`` -> ``mailu.example.com``) and DNS/TXT record (``_mta-sts.example.com`` -> ``v=STSv1; id=1``) paying attention to the ``TTL`` as this is used by MTA-STS. -2. configure an override with the policy itself; for example, your ``overrides/mta-sts.conf`` could read: +2. configure an override with the policy itself; for example, your ``overrides/nginx/mta-sts.conf`` could read: .. code-block:: bash From 7c5dcfa025e8b7e1f7e5bb6b3ac56331bc928287 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sun, 29 Aug 2021 18:32:17 +0200 Subject: [PATCH 06/23] MTA-STS is a major feature --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c4354b28..8c7d1640 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Main features include: - **Web access**, multiple Webmails and administration interface - **User features**, aliases, auto-reply, auto-forward, fetched accounts - **Admin features**, global admins, announcements, per-domain delegation, quotas -- **Security**, enforced TLS, Letsencrypt!, outgoing DKIM, anti-virus scanner +- **Security**, enforced TLS, MTA-STS, Letsencrypt!, outgoing DKIM, anti-virus scanner - **Antispam**, auto-learn, greylisting, DMARC and SPF - **Freedom**, all FOSS components, no tracker included From a8142dabbe4df86a2aa87d3f323de20c045d7db3 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 30 Aug 2021 14:21:28 +0200 Subject: [PATCH 07/23] Introduce DEFER_ON_TLS_ERROR This will default to True and defer emails that fail even "loose" validation of DANE or MTA-STS It should work most of the time but if it doesn't and you would rather see your emails delivered, you can turn it off. --- core/postfix/conf/main.cf | 6 +++++- core/postfix/mta-sts-daemon.yml | 2 +- core/postfix/start.py | 1 + docs/configuration.rst | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/core/postfix/conf/main.cf b/core/postfix/conf/main.cf index 0194324f..78ffcee1 100644 --- a/core/postfix/conf/main.cf +++ b/core/postfix/conf/main.cf @@ -58,13 +58,17 @@ tls_ssl_options = NO_COMPRESSION, NO_TICKET # 2. not all will have and up-to-date TLS stack. smtp_tls_mandatory_protocols = !SSLv2, !SSLv3 smtp_tls_protocols =!SSLv2,!SSLv3 -smtp_tls_security_level = {{ OUTBOUND_TLS_LEVEL|default('may') }} +smtp_tls_security_level = {{ OUTBOUND_TLS_LEVEL|default('dane') }} +smtp_tls_dane_insecure_mx_policy = dane smtp_tls_policy_maps=hash:/etc/postfix/tls_policy.map, socketmap:unix:/tmp/mta-sts.socket:postfix smtp_tls_CApath = /etc/ssl/certs smtp_tls_session_cache_database = lmdb:/dev/shm/postfix/smtp_scache smtpd_tls_session_cache_database = lmdb:/dev/shm/postfix/smtpd_scache smtp_host_lookup = dns smtp_dns_support_level = dnssec +delay_warning_time = 5m +smtp_tls_loglevel = 1 +notify_classes = resource, software, delay ############### # Virtual diff --git a/core/postfix/mta-sts-daemon.yml b/core/postfix/mta-sts-daemon.yml index 39f60e48..361bcbf9 100644 --- a/core/postfix/mta-sts-daemon.yml +++ b/core/postfix/mta-sts-daemon.yml @@ -6,5 +6,5 @@ cache: options: cache_size: 10000 default_zone: - strict_testing: false + strict_testing: {{ DEFER_ON_TLS_ERROR |default('true') }} timeout: 4 diff --git a/core/postfix/start.py b/core/postfix/start.py index de559b27..5e439bdb 100755 --- a/core/postfix/start.py +++ b/core/postfix/start.py @@ -76,6 +76,7 @@ for map_file in glob.glob("/overrides/*.map"): if os.path.exists("/overrides/mta-sts-daemon.yml"): shutil.copyfile("/overrides/mta-sts-daemon.yml", "/etc/mta-sts-daemon.yml") +conf.jinja("/etc/mta-sts-daemon.yml", os.environ, "/etc/mta-sts-daemon.yml") if not os.path.exists("/etc/postfix/tls_policy.map.db"): with open("/etc/postfix/tls_policy.map", "w") as f: diff --git a/docs/configuration.rst b/docs/configuration.rst index 27f8db7d..4fd84c07 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -73,7 +73,7 @@ mail in following format: ``[HOST]:PORT``. By default postfix uses "opportunistic TLS" for outbound mail. This can be changed by setting ``OUTBOUND_TLS_LEVEL`` to ``encrypt`` or ``secure``. This setting is highly recommended -if you are using a relayhost that supports TLS. +if you are using a relayhost that supports TLS but discouraged otherwise. ``DEFER_ON_TLS_ERROR`` (default: True) controls whether incomplete policies (DANE without DNSSEC or "testing" MTA-STS policies) will be taken into account and whether emails will be defered if the additional checks enforced by those policies fail. Similarily by default nginx uses "opportunistic TLS" for inbound mail. This can be changed by setting ``INBOUND_TLS_ENFORCE`` to ``True``. Please note that this is forbidden for From 05b57c972ec3a6fbe6a8f9cc048246e24747390c Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 30 Aug 2021 14:44:13 +0200 Subject: [PATCH 08/23] remove the static policy as it will override MTA-STS and DANE --- core/postfix/start.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/postfix/start.py b/core/postfix/start.py index 5e439bdb..69855e29 100755 --- a/core/postfix/start.py +++ b/core/postfix/start.py @@ -80,7 +80,7 @@ conf.jinja("/etc/mta-sts-daemon.yml", os.environ, "/etc/mta-sts-daemon.yml") if not os.path.exists("/etc/postfix/tls_policy.map.db"): with open("/etc/postfix/tls_policy.map", "w") as f: - for domain in ['yahoo.com', 'hotmail.com', 'aol.com', 'outlook.com', 'icloud.com', 'msn.com', 'hotmail.co.uk', 'live.com', 'yahoo.co.in', 'me.com', 'cox.net', 'yahoo.co.uk', 'verizon.net', 'ymail.com', 'hotmail.it', 'kw.com', 'yahoo.com.tw', 'mac.com', 'live.se', 'live.nl', 'yahoo.com.br', 'libero.it', 'web.de', 'allstate.com', 'btinternet.com', 'online.no', 'yahoo.com.au', 'live.dk', 'earthlink.net', 'yahoo.fr', 'yahoo.it', 'gmx.de', 'hotmail.fr', 'shawinc.com', 'yahoo.de', 'moe.edu.sg', 'naver.com', 'bigpond.com', 'statefarm.com', 'remax.net', 'rocketmail.com', 'live.no', 'yahoo.ca', 'bigpond.net.au', 'hotmail.se', 'gmx.at', 'live.co.uk', 'mail.com', 'yahoo.in', 'yandex.ru', 'qq.com', 'charter.net', 'indeedemail.com', 'alice.it', 'hotmail.de', 'bluewin.ch', 'optonline.net', 'yahoo.es', 'hotmail.no', 'pindotmedia.com', 'orange.fr', 'live.it', 'yahoo.co.id', 'yahoo.no', 'hotmail.es', 'morganstanley.com', 'wellsfargo.com', 'wanadoo.fr', 'facebook.com', 'yahoo.se', 'fema.dhs.gov', 'rogers.com', 'yahoo.com.hk', 'live.com.au', 'nic.in', 'nab.com.au', 'ubs.com', 'shaw.ca', 'umich.edu', 'westpac.com.au', 'yahoo.com.mx', 'yahoo.com.sg', 'farmersagent.com', 'yahoo.dk', 'dhs.gov']: + for domain in ['example.com']: f.write(f'{domain}\tsecure\n') os.system("postmap /etc/postfix/tls_policy.map") From 67db72d7743a5f2850e6c6a7d19b2ff99e8ea6d5 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 30 Aug 2021 17:00:12 +0200 Subject: [PATCH 09/23] Behave like documented --- core/postfix/conf/main.cf | 2 +- docs/configuration.rst | 8 ++++++-- towncrier/newsfragments/1798.feature | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/core/postfix/conf/main.cf b/core/postfix/conf/main.cf index 78ffcee1..ae54326d 100644 --- a/core/postfix/conf/main.cf +++ b/core/postfix/conf/main.cf @@ -59,7 +59,7 @@ tls_ssl_options = NO_COMPRESSION, NO_TICKET smtp_tls_mandatory_protocols = !SSLv2, !SSLv3 smtp_tls_protocols =!SSLv2,!SSLv3 smtp_tls_security_level = {{ OUTBOUND_TLS_LEVEL|default('dane') }} -smtp_tls_dane_insecure_mx_policy = dane +smtp_tls_dane_insecure_mx_policy = {% if DEFER_ON_TLS_ERROR == 'false' %}may{% else %}dane{% endif %} smtp_tls_policy_maps=hash:/etc/postfix/tls_policy.map, socketmap:unix:/tmp/mta-sts.socket:postfix smtp_tls_CApath = /etc/ssl/certs smtp_tls_session_cache_database = lmdb:/dev/shm/postfix/smtp_scache diff --git a/docs/configuration.rst b/docs/configuration.rst index 4fd84c07..7cf3c926 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -72,8 +72,12 @@ mail in following format: ``[HOST]:PORT``. ``RELAYUSER`` and ``RELAYPASSWORD`` can be used when authentication is needed. By default postfix uses "opportunistic TLS" for outbound mail. This can be changed -by setting ``OUTBOUND_TLS_LEVEL`` to ``encrypt`` or ``secure``. This setting is highly recommended -if you are using a relayhost that supports TLS but discouraged otherwise. ``DEFER_ON_TLS_ERROR`` (default: True) controls whether incomplete policies (DANE without DNSSEC or "testing" MTA-STS policies) will be taken into account and whether emails will be defered if the additional checks enforced by those policies fail. +by setting ``OUTBOUND_TLS_LEVEL`` to ``encrypt`` or ``secure``. This setting is +highly recommended if you are using a relayhost that supports TLS but discouraged +otherwise. ``DEFER_ON_TLS_ERROR`` (default: True) controls whether incomplete +policies (DANE without DNSSEC or "testing" MTA-STS policies) will be taken into +account and whether emails will be defered if the additional checks enforced by +those policies fail. Similarily by default nginx uses "opportunistic TLS" for inbound mail. This can be changed by setting ``INBOUND_TLS_ENFORCE`` to ``True``. Please note that this is forbidden for diff --git a/towncrier/newsfragments/1798.feature b/towncrier/newsfragments/1798.feature index 1b63a85c..125b1767 100644 --- a/towncrier/newsfragments/1798.feature +++ b/towncrier/newsfragments/1798.feature @@ -1 +1 @@ -Implement MTA-STS (use published policies) +Implement MTA-STS and DANE validation. Introduce DEFER_ON_TLS_ERROR (default: True) to harden or loosen the policy enforcement. From fccb0cc57f82f88235d3d10d58be4aff7762a483 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 30 Aug 2021 17:16:41 +0200 Subject: [PATCH 10/23] Add a longer max_age (15days) --- docs/faq.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.rst b/docs/faq.rst index 92b208de..b5627743 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -383,7 +383,7 @@ Mailu can serve an `MTA-STS policy`_; To configure it you will need to: location ^~ /.well-known/mta-sts.txt { return 200 "version: STSv1 mode: enforce - max_age: 86401 + max_age: 1296000 mx: mailu.example.com\r\n"; } From fb34f5349348ebc22af08737a6ef641bd6a156a7 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 30 Aug 2021 17:18:19 +0200 Subject: [PATCH 11/23] Do operations in the right (safe) order --- docs/faq.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index b5627743..fb6f66df 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -374,7 +374,7 @@ How do I setup a MTA-STS policy? Mailu can serve an `MTA-STS policy`_; To configure it you will need to: -1. setup the appropriate DNS/CNAME record (``mta-sts.example.com`` -> ``mailu.example.com``) and DNS/TXT record (``_mta-sts.example.com`` -> ``v=STSv1; id=1``) paying attention to the ``TTL`` as this is used by MTA-STS. +1. add ``mta-sts.example.com`` to the ``HOSTNAMES`` configuration variable (and ensure that a valid SSL certificate is available for it) 2. configure an override with the policy itself; for example, your ``overrides/nginx/mta-sts.conf`` could read: @@ -387,7 +387,7 @@ Mailu can serve an `MTA-STS policy`_; To configure it you will need to: mx: mailu.example.com\r\n"; } -3. add ``mta-sts.example.com`` to the ``HOSTNAMES`` configuration variable (and ensure that a valid SSL certificate is available for it) +3. setup the appropriate DNS/CNAME record (``mta-sts.example.com`` -> ``mailu.example.com``) and DNS/TXT record (``_mta-sts.example.com`` -> ``v=STSv1; id=1``) paying attention to the ``TTL`` as this is used by MTA-STS. *issue reference:* `1798`_. From d607ba0ef2b345a44f9f95cd227a73f0a63f9489 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 30 Aug 2021 17:52:31 +0200 Subject: [PATCH 12/23] Clarify that a restart may be required --- docs/faq.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.rst b/docs/faq.rst index fb6f66df..43fb8606 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -374,7 +374,7 @@ How do I setup a MTA-STS policy? Mailu can serve an `MTA-STS policy`_; To configure it you will need to: -1. add ``mta-sts.example.com`` to the ``HOSTNAMES`` configuration variable (and ensure that a valid SSL certificate is available for it) +1. add ``mta-sts.example.com`` to the ``HOSTNAMES`` configuration variable (and ensure that a valid SSL certificate is available for it; this may mean restarting your smtp container) 2. configure an override with the policy itself; for example, your ``overrides/nginx/mta-sts.conf`` could read: From a1da4daa4c30668d98325cebd640ae3cf2f5fc97 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Tue, 31 Aug 2021 20:24:06 +0200 Subject: [PATCH 13/23] Implement the DANE-only lookup policyd https://github.com/Snawoot/postfix-mta-sts-resolver/issues/67 for context --- core/admin/mailu/internal/views/postfix.py | 3 +++ core/admin/mailu/utils.py | 24 +++++++++++++++++++++- core/postfix/conf/main.cf | 2 +- core/postfix/start.py | 1 + 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/core/admin/mailu/internal/views/postfix.py b/core/admin/mailu/internal/views/postfix.py index 2e7d0b9b..330fed5b 100644 --- a/core/admin/mailu/internal/views/postfix.py +++ b/core/admin/mailu/internal/views/postfix.py @@ -7,6 +7,9 @@ import idna import re import srslib +@internal.route("/postfix/dane/") +def postfix_dane_map(domain_name): + return flask.jsonify('dane-only') if utils.has_dane_record(domain_name) else flask.abort(404) @internal.route("/postfix/domain/") def postfix_mailbox_domain(domain_name): diff --git a/core/admin/mailu/utils.py b/core/admin/mailu/utils.py index 02150754..914638fa 100644 --- a/core/admin/mailu/utils.py +++ b/core/admin/mailu/utils.py @@ -6,6 +6,9 @@ try: except ImportError: import pickle +import dns +import dns.resolver + import hmac import secrets import time @@ -25,7 +28,6 @@ from itsdangerous.encoding import want_bytes from werkzeug.datastructures import CallbackDict from werkzeug.contrib import fixers - # Login configuration login = flask_login.LoginManager() login.login_view = "ui.login" @@ -37,6 +39,26 @@ def handle_needs_login(): flask.url_for('ui.login', next=flask.request.endpoint) ) +# DNS stub configured to do DNSSEC enabled queries +resolver = dns.resolver.Resolver() +resolver.use_edns(0, 0, 1500) +resolver.flags = dns.flags.AD | dns.flags.RD + +def has_dane_record(domain, timeout=5): + try: + result = resolver.query(f'_25._tcp.{domain}', dns.rdatatype.TLSA,dns.rdataclass.IN, lifetime=timeout) + if (result.response.flags & dns.flags.AD) == dns.flags.AD: + for record in result: + if isinstance(record, dns.rdtypes.ANY.TLSA.TLSA): + record.validate() + if record.usage in [2,3]: # postfix wants DANE-only + return True + except dns.resolver.NoNameservers: + # this could be an attack / a failed DNSSEC lookup + return True + except: + pass + # Rate limiter limiter = limiter.LimitWraperFactory() diff --git a/core/postfix/conf/main.cf b/core/postfix/conf/main.cf index ae54326d..16fdfa6e 100644 --- a/core/postfix/conf/main.cf +++ b/core/postfix/conf/main.cf @@ -60,7 +60,7 @@ smtp_tls_mandatory_protocols = !SSLv2, !SSLv3 smtp_tls_protocols =!SSLv2,!SSLv3 smtp_tls_security_level = {{ OUTBOUND_TLS_LEVEL|default('dane') }} smtp_tls_dane_insecure_mx_policy = {% if DEFER_ON_TLS_ERROR == 'false' %}may{% else %}dane{% endif %} -smtp_tls_policy_maps=hash:/etc/postfix/tls_policy.map, socketmap:unix:/tmp/mta-sts.socket:postfix +smtp_tls_policy_maps=hash:/etc/postfix/tls_policy.map, ${podop}dane, socketmap:unix:/tmp/mta-sts.socket:postfix smtp_tls_CApath = /etc/ssl/certs smtp_tls_session_cache_database = lmdb:/dev/shm/postfix/smtp_scache smtpd_tls_session_cache_database = lmdb:/dev/shm/postfix/smtpd_scache diff --git a/core/postfix/start.py b/core/postfix/start.py index 69855e29..03dca93c 100755 --- a/core/postfix/start.py +++ b/core/postfix/start.py @@ -21,6 +21,7 @@ def start_podop(): run_server(0, "postfix", "/tmp/podop.socket", [ ("transport", "url", url + "transport/§"), ("alias", "url", url + "alias/§"), + ("dane", "url", url + "dane/§"), ("domain", "url", url + "domain/§"), ("mailbox", "url", url + "mailbox/§"), ("recipientmap", "url", url + "recipient/map/§"), From 9f66e2672b3c646570fba1f644f1b35e44ebaeec Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Tue, 31 Aug 2021 20:44:57 +0200 Subject: [PATCH 14/23] Use DEFER_ON_TLS_ERROR here too We just don't know whether the lookup failed because we are under attack or whether it's a glitch; the safe behaviour is to defer --- core/admin/mailu/configuration.py | 1 + core/admin/mailu/utils.py | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/admin/mailu/configuration.py b/core/admin/mailu/configuration.py index 7cd3a56b..4c48fcc4 100644 --- a/core/admin/mailu/configuration.py +++ b/core/admin/mailu/configuration.py @@ -35,6 +35,7 @@ DEFAULT_CONFIG = { 'WILDCARD_SENDERS': '', 'TLS_FLAVOR': 'cert', 'INBOUND_TLS_ENFORCE': False, + 'DEFER_ON_TLS_ERROR': True, 'AUTH_RATELIMIT': '1000/minute;10000/hour', 'AUTH_RATELIMIT_SUBNET': False, 'DISABLE_STATISTICS': False, diff --git a/core/admin/mailu/utils.py b/core/admin/mailu/utils.py index 914638fa..2313a1e6 100644 --- a/core/admin/mailu/utils.py +++ b/core/admin/mailu/utils.py @@ -54,8 +54,10 @@ def has_dane_record(domain, timeout=5): if record.usage in [2,3]: # postfix wants DANE-only return True except dns.resolver.NoNameservers: - # this could be an attack / a failed DNSSEC lookup - return True + # If the DNSSEC data is invalid and the DNS resolver is DNSSEC enabled + # we will receive this non-specific exception. The safe behaviour is to + # accept to defer the email. + return app.config['DEFER_ON_TLS_ERROR'] except: pass From 489520f0673a25482294cb5b1b7d6bb28a3b8dd1 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Wed, 1 Sep 2021 08:41:39 +0200 Subject: [PATCH 15/23] forgot about alpine/lmdb --- core/postfix/conf/main.cf | 2 +- core/postfix/start.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/postfix/conf/main.cf b/core/postfix/conf/main.cf index 16fdfa6e..6152388c 100644 --- a/core/postfix/conf/main.cf +++ b/core/postfix/conf/main.cf @@ -60,7 +60,7 @@ smtp_tls_mandatory_protocols = !SSLv2, !SSLv3 smtp_tls_protocols =!SSLv2,!SSLv3 smtp_tls_security_level = {{ OUTBOUND_TLS_LEVEL|default('dane') }} smtp_tls_dane_insecure_mx_policy = {% if DEFER_ON_TLS_ERROR == 'false' %}may{% else %}dane{% endif %} -smtp_tls_policy_maps=hash:/etc/postfix/tls_policy.map, ${podop}dane, socketmap:unix:/tmp/mta-sts.socket:postfix +smtp_tls_policy_maps=lmdb:/etc/postfix/tls_policy.map, ${podop}dane, socketmap:unix:/tmp/mta-sts.socket:postfix smtp_tls_CApath = /etc/ssl/certs smtp_tls_session_cache_database = lmdb:/dev/shm/postfix/smtp_scache smtpd_tls_session_cache_database = lmdb:/dev/shm/postfix/smtpd_scache diff --git a/core/postfix/start.py b/core/postfix/start.py index 03dca93c..8aa279ef 100755 --- a/core/postfix/start.py +++ b/core/postfix/start.py @@ -20,7 +20,7 @@ def start_podop(): # TODO: Remove verbosity setting from Podop? run_server(0, "postfix", "/tmp/podop.socket", [ ("transport", "url", url + "transport/§"), - ("alias", "url", url + "alias/§"), + ("alias", "url", url + "alias/§"), ("dane", "url", url + "dane/§"), ("domain", "url", url + "domain/§"), ("mailbox", "url", url + "mailbox/§"), @@ -79,7 +79,7 @@ if os.path.exists("/overrides/mta-sts-daemon.yml"): shutil.copyfile("/overrides/mta-sts-daemon.yml", "/etc/mta-sts-daemon.yml") conf.jinja("/etc/mta-sts-daemon.yml", os.environ, "/etc/mta-sts-daemon.yml") -if not os.path.exists("/etc/postfix/tls_policy.map.db"): +if not os.path.exists("/etc/postfix/tls_policy.map.lmdb"): with open("/etc/postfix/tls_policy.map", "w") as f: for domain in ['example.com']: f.write(f'{domain}\tsecure\n') From 92cc664e82f3f40f9a247093d440c3b75ff768d8 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Wed, 1 Sep 2021 08:41:59 +0200 Subject: [PATCH 16/23] Cosmetic change --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8c7d1640..4c19ad78 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Main features include: - **Web access**, multiple Webmails and administration interface - **User features**, aliases, auto-reply, auto-forward, fetched accounts - **Admin features**, global admins, announcements, per-domain delegation, quotas -- **Security**, enforced TLS, MTA-STS, Letsencrypt!, outgoing DKIM, anti-virus scanner +- **Security**, enforced TLS, DANE, MTA-STS, Letsencrypt!, outgoing DKIM, anti-virus scanner - **Antispam**, auto-learn, greylisting, DMARC and SPF - **Freedom**, all FOSS components, no tracker included From c1d94bb72563430d151916de0e3d9e31708348fe Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Wed, 1 Sep 2021 09:01:04 +0200 Subject: [PATCH 17/23] Ensure that postfix will be able to use the TLSA records see https://www.huque.com/dane/testsite/ for the testcases --- core/admin/mailu/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/admin/mailu/utils.py b/core/admin/mailu/utils.py index 2313a1e6..66cf0476 100644 --- a/core/admin/mailu/utils.py +++ b/core/admin/mailu/utils.py @@ -44,14 +44,14 @@ resolver = dns.resolver.Resolver() resolver.use_edns(0, 0, 1500) resolver.flags = dns.flags.AD | dns.flags.RD -def has_dane_record(domain, timeout=5): +def has_dane_record(domain, timeout=10): try: result = resolver.query(f'_25._tcp.{domain}', dns.rdatatype.TLSA,dns.rdataclass.IN, lifetime=timeout) if (result.response.flags & dns.flags.AD) == dns.flags.AD: for record in result: if isinstance(record, dns.rdtypes.ANY.TLSA.TLSA): record.validate() - if record.usage in [2,3]: # postfix wants DANE-only + if record.usage in [2,3] and record.selector in [0,1] and record.mtype in [0,1,2]: return True except dns.resolver.NoNameservers: # If the DNSSEC data is invalid and the DNS resolver is DNSSEC enabled From 4abf49edf429e2ebb594a002c9f5e922a6e824e7 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Wed, 1 Sep 2021 09:15:13 +0200 Subject: [PATCH 18/23] indent --- core/postfix/start.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/postfix/start.py b/core/postfix/start.py index 8aa279ef..19c23c19 100755 --- a/core/postfix/start.py +++ b/core/postfix/start.py @@ -19,10 +19,10 @@ def start_podop(): url = "http://" + os.environ["ADMIN_ADDRESS"] + "/internal/postfix/" # TODO: Remove verbosity setting from Podop? run_server(0, "postfix", "/tmp/podop.socket", [ - ("transport", "url", url + "transport/§"), - ("alias", "url", url + "alias/§"), - ("dane", "url", url + "dane/§"), - ("domain", "url", url + "domain/§"), + ("transport", "url", url + "transport/§"), + ("alias", "url", url + "alias/§"), + ("dane", "url", url + "dane/§"), + ("domain", "url", url + "domain/§"), ("mailbox", "url", url + "mailbox/§"), ("recipientmap", "url", url + "recipient/map/§"), ("sendermap", "url", url + "sender/map/§"), From a9a1b3e55e36f9dc90713c7bff1b24252da307c2 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sun, 5 Sep 2021 15:28:59 +0200 Subject: [PATCH 19/23] Reduce the EDNS0 size to 1232 @see https://github.com/dns-violations/dnsflagday/issues/125 --- core/admin/mailu/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/admin/mailu/utils.py b/core/admin/mailu/utils.py index e8e58100..b6495a53 100644 --- a/core/admin/mailu/utils.py +++ b/core/admin/mailu/utils.py @@ -41,7 +41,7 @@ def handle_needs_login(): # DNS stub configured to do DNSSEC enabled queries resolver = dns.resolver.Resolver() -resolver.use_edns(0, 0, 1500) +resolver.use_edns(0, 0, 1232) resolver.flags = dns.flags.AD | dns.flags.RD def has_dane_record(domain, timeout=10): From 9888efe55dddbb789e40be1bb7da5412361653fe Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sun, 5 Sep 2021 18:23:08 +0200 Subject: [PATCH 20/23] Document as suggested on #mailu-dev --- core/postfix/start.py | 5 ++--- docs/faq.rst | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/core/postfix/start.py b/core/postfix/start.py index 19c23c19..9f35cf73 100755 --- a/core/postfix/start.py +++ b/core/postfix/start.py @@ -80,9 +80,8 @@ if os.path.exists("/overrides/mta-sts-daemon.yml"): conf.jinja("/etc/mta-sts-daemon.yml", os.environ, "/etc/mta-sts-daemon.yml") if not os.path.exists("/etc/postfix/tls_policy.map.lmdb"): - with open("/etc/postfix/tls_policy.map", "w") as f: - for domain in ['example.com']: - f.write(f'{domain}\tsecure\n') + with open("/etc/postfix/tls_policy.map", "a") as f: + pass os.system("postmap /etc/postfix/tls_policy.map") if "RELAYUSER" in os.environ: diff --git a/docs/faq.rst b/docs/faq.rst index 43fb8606..01557237 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -422,6 +422,22 @@ Any mail related connection is proxied by nginx. Therefore the SMTP Banner is al .. _`1368`: https://github.com/Mailu/Mailu/issues/1368 +My emails are getting defered, what can I do? +````````````````````````````````````````````` + +Emails are asynchronous and it's not abnormal for them to be defered sometimes. That being said, Mailu enforces secure connections where possible using DANE and MTA-STS, both of which have the potential to delay indefinitely delivery if something is misconfigured. + +If delivery to a specific domain fails because their DANE records are invalid or their TLS configuration inadequate (expired certificate, ...), you can assist delivery by downgrading the security level for that domain by creating an override at ``overrides/postfix/tls_policy.map`` as follow: + +.. code-block:: bash + + domain.example.com may + domain.example.org encrypt + +The syntax and options are as described in `postfix's documentation`_. Re-creating the smtp container will be required for changes to take effect. + +.. _`postfix's documentation`: http://www.postfix.org/postconf.5.html#smtp_tls_policy_maps + 403 - Access Denied Errors --------------------------- From 0f0459e9b2e445e27fead761d90460e68b3ea96c Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sun, 5 Sep 2021 18:49:07 +0200 Subject: [PATCH 21/23] suggestions from @ghostwheel42 --- core/admin/mailu/utils.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/core/admin/mailu/utils.py b/core/admin/mailu/utils.py index b6495a53..96368b57 100644 --- a/core/admin/mailu/utils.py +++ b/core/admin/mailu/utils.py @@ -47,7 +47,7 @@ resolver.flags = dns.flags.AD | dns.flags.RD def has_dane_record(domain, timeout=10): try: result = resolver.query(f'_25._tcp.{domain}', dns.rdatatype.TLSA,dns.rdataclass.IN, lifetime=timeout) - if (result.response.flags & dns.flags.AD) == dns.flags.AD: + if result.response.flags & dns.flags.AD): for record in result: if isinstance(record, dns.rdtypes.ANY.TLSA.TLSA): record.validate() @@ -57,8 +57,14 @@ def has_dane_record(domain, timeout=10): # If the DNSSEC data is invalid and the DNS resolver is DNSSEC enabled # we will receive this non-specific exception. The safe behaviour is to # accept to defer the email. + flask.current_app.logger.warn(f'Unable to lookup the TLSA record for {domain}. Is the DNSSEC zone okay on https://dnsviz.net/d/{domain}/dnssec/?') return app.config['DEFER_ON_TLS_ERROR'] - except: + except dns.exception.Timeout: + flask.current_app.logger.warn(f'Timeout while resolving the TLSA record for {domain} ({timeout}s).') + except dns.resolver.NXDOMAIN: + pass # this is expected, not TLSA record is fine + except Exception as e: + flask.current_app.logger.error(f'Error while looking up the TLSA record for {domain} {e}') pass # Rate limiter From 0ee52ba65b867b70fce9e2e276564823deb14de9 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sun, 5 Sep 2021 19:03:54 +0200 Subject: [PATCH 22/23] Doh --- core/admin/mailu/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/admin/mailu/utils.py b/core/admin/mailu/utils.py index 96368b57..506b3e7e 100644 --- a/core/admin/mailu/utils.py +++ b/core/admin/mailu/utils.py @@ -47,7 +47,7 @@ resolver.flags = dns.flags.AD | dns.flags.RD def has_dane_record(domain, timeout=10): try: result = resolver.query(f'_25._tcp.{domain}', dns.rdatatype.TLSA,dns.rdataclass.IN, lifetime=timeout) - if result.response.flags & dns.flags.AD): + if result.response.flags & dns.flags.AD: for record in result: if isinstance(record, dns.rdtypes.ANY.TLSA.TLSA): record.validate() From 7aa403573d61ed8a2492089d0bbeae8da7859bdd Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sun, 5 Sep 2021 19:06:20 +0200 Subject: [PATCH 23/23] no with here --- core/postfix/start.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/postfix/start.py b/core/postfix/start.py index 9f35cf73..3de83a63 100755 --- a/core/postfix/start.py +++ b/core/postfix/start.py @@ -80,8 +80,7 @@ if os.path.exists("/overrides/mta-sts-daemon.yml"): conf.jinja("/etc/mta-sts-daemon.yml", os.environ, "/etc/mta-sts-daemon.yml") if not os.path.exists("/etc/postfix/tls_policy.map.lmdb"): - with open("/etc/postfix/tls_policy.map", "a") as f: - pass + open("/etc/postfix/tls_policy.map", "a").close() os.system("postmap /etc/postfix/tls_policy.map") if "RELAYUSER" in os.environ: