diff --git a/plinth/modules/email_server/__init__.py b/plinth/modules/email_server/__init__.py index 8a4939021..45c841024 100644 --- a/plinth/modules/email_server/__init__.py +++ b/plinth/modules/email_server/__init__.py @@ -1,6 +1,8 @@ """FreedomBox email server app""" # SPDX-License-Identifier: AGPL-3.0-or-later +import logging + from django.urls import reverse_lazy from django.utils.translation import ugettext_lazy as _ @@ -10,7 +12,9 @@ import plinth.frontpage import plinth.menu from plinth import actions from plinth.modules.apache.components import Webserver +from plinth.modules.config import get_domainname from plinth.modules.firewall.components import Firewall +from plinth.modules.letsencrypt.components import LetsEncrypt from . import audit from . import manifest @@ -36,6 +40,7 @@ managed_services = ['postfix', 'dovecot', 'rspamd'] managed_packages = packages + packages_bloat app = None +logger = logging.getLogger(__name__) class EmailServerApp(plinth.app.App): @@ -56,6 +61,18 @@ class EmailServerApp(plinth.app.App): urls=['https://{host}/rspamd']) self.add(webserver) + # Let's Encrypt event hook + default_domain = get_domainname() + domains = [default_domain] if default_domain else [] + letsencrypt = LetsEncrypt( + 'letsencrypt-email-server', domains=domains, + daemons=['postfix', 'dovecot'], should_copy_certificates=False, + managing_app='email_server') + self.add(letsencrypt) + + if not domains: + logger.warning('Could not fetch the FreedomBox domain name!') + def _add_ui_components(self): info = plinth.app.Info( app_id=self.app_id, @@ -119,6 +136,7 @@ class EmailServerApp(plinth.app.App): results.extend([r.summarize() for r in audit.domain.get()]) results.extend([r.summarize() for r in audit.ldap.get()]) results.extend([r.summarize() for r in audit.spam.get()]) + results.extend([r.summarize() for r in audit.tls.get()]) return results @@ -128,6 +146,7 @@ def setup(helper, old_version=None): helper.install(packages_bloat, skip_recommends=True) helper.call('post', audit.ldap.repair) helper.call('post', audit.spam.repair) + helper.call('post', audit.tls.repair) for srvname in managed_services: actions.superuser_run('service', ['reload', srvname]) # Final step: expose service daemons to public internet diff --git a/plinth/modules/email_server/aliases/__init__.py b/plinth/modules/email_server/aliases/__init__.py index 760ecb0cc..e4811bb17 100644 --- a/plinth/modules/email_server/aliases/__init__.py +++ b/plinth/modules/email_server/aliases/__init__.py @@ -126,7 +126,7 @@ def schedule_hash_update(): key = alias.email_name.encode('ascii') + b'\0' if alias.enabled: value = str(alias.uid_number).encode('ascii') - value += b'@localhost\0' + value += b'@localhost\0' else: value = b'/dev/null\0' db[key] = value diff --git a/plinth/modules/email_server/audit/__init__.py b/plinth/modules/email_server/audit/__init__.py index 4adc7489b..4b548e7b4 100644 --- a/plinth/modules/email_server/audit/__init__.py +++ b/plinth/modules/email_server/audit/__init__.py @@ -7,5 +7,6 @@ from . import domain from . import home from . import ldap from . import spam +from . import tls -__all__ = ['domain', 'home', 'ldap', 'spam'] +__all__ = ['domain', 'home', 'ldap', 'spam', 'tls'] diff --git a/plinth/modules/email_server/audit/ldap.py b/plinth/modules/email_server/audit/ldap.py index f737c5278..412828531 100644 --- a/plinth/modules/email_server/audit/ldap.py +++ b/plinth/modules/email_server/audit/ldap.py @@ -15,7 +15,11 @@ default_config = { 'smtpd_sasl_type': 'dovecot', 'smtpd_sasl_path': 'private/auth', 'mailbox_transport': 'lmtp:unix:private/dovecot-lmtp', - 'virtual_transport': 'lmtp:unix:private/dovecot-lmtp' + 'virtual_transport': 'lmtp:unix:private/dovecot-lmtp', + + 'smtpd_relay_restrictions': ','.join([ + 'permit_sasl_authenticated', 'defer_unauth_destination', + ]) } submission_flags = postconf.ServiceFlags( diff --git a/plinth/modules/email_server/audit/tls.py b/plinth/modules/email_server/audit/tls.py new file mode 100644 index 000000000..8b581a311 --- /dev/null +++ b/plinth/modules/email_server/audit/tls.py @@ -0,0 +1,84 @@ +"""TLS configuration""" +# SPDX-License-Identifier: AGPL-3.0-or-later + +import logging + +from plinth import actions + +from . import models +from plinth.modules.email_server import postconf + +# Mozilla Guideline v5.6, Postfix 1.17.7, OpenSSL 1.1.1d, intermediate +# Generated 2021-08 +# https://ssl-config.mozilla.org/ +tls_medium_cipherlist = [ + 'ECDHE-ECDSA-AES128-GCM-SHA256', 'ECDHE-RSA-AES128-GCM-SHA256', + 'ECDHE-ECDSA-AES256-GCM-SHA384', 'ECDHE-RSA-AES256-GCM-SHA384', + 'ECDHE-ECDSA-CHACHA20-POLY1305', 'ECDHE-RSA-CHACHA20-POLY1305', + 'DHE-RSA-AES128-GCM-SHA256', 'DHE-RSA-AES256-GCM-SHA384' +] + +postfix_config = { + # Enable TLS + 'smtpd_tls_security_level': 'may', + 'smtpd_tls_auth_only': 'yes', + + # Debugging information + 'smtpd_tls_received_header': 'yes', + + # Use a strong hashing algorithm + 'smtp_tls_fingerprint_digest': 'sha256', + 'smtpd_tls_fingerprint_digest': 'sha256', + + # Mozilla Intermediate Configuration + 'smtpd_tls_mandatory_protocols': '!SSLv2, !SSLv3, !TLSv1, !TLSv1.1', + 'smtpd_tls_protocols': '!SSLv2, !SSLv3, !TLSv1, !TLSv1.1', + 'smtpd_tls_mandatory_ciphers': 'medium', + 'tls_medium_cipherlist': ':'.join(tls_medium_cipherlist), + 'tls_preempt_cipherlist': 'no', + + # Postfix SMTP client + 'smtp_tls_mandatory_protocols': '!SSLv2, !SSLv3, !TLSv1, !TLSv1.1', + 'smtp_tls_protocols': '!SSLv2, !SSLv3, !TLSv1, !TLSv1.1', + 'smtp_tls_mandatory_ciphers': 'medium', + + # Use DNSSEC to validate TLS certificates + 'smtp_host_lookup': 'dns', + 'smtp_dns_support_level': 'dnssec', + 'smtp_tls_security_level': 'dane', # Opportunistic DANE TLS + + # Maintain 1 cipherlist and keep it the most secure + 'tls_low_cipherlist': '$tls_medium_cipherlist', + 'tls_high_cipherlist': '$tls_medium_cipherlist', +} + +logger = logging.getLogger(__name__) + + +def get(): + results = [] + with postconf.mutex.lock_all(): + results.append(check_tls()) + return results + + +def repair(): + actions.superuser_run('email_server', ['-i', 'tls', 'set_up']) + + +def check_tls(): + diagnosis = models.MainCfDiagnosis('Postfix TLS') + current = postconf.get_many_unsafe(list(postfix_config.keys())) + diagnosis.compare_and_advise(current=current, default=postfix_config) + return diagnosis + + +def repair_tls(diagnosis): + diagnosis.assert_resolved() + logger.info('Setting postconf: %r', diagnosis.advice) + postconf.set_many_unsafe(diagnosis.advice) + + +def action_set_up(): + with postconf.mutex.lock_all(): + repair_tls(check_tls()) diff --git a/plinth/modules/email_server/data/etc/dovecot/conf.d/90-freedombox-ssl.conf b/plinth/modules/email_server/data/etc/dovecot/conf.d/90-freedombox-ssl.conf new file mode 100644 index 000000000..fa3c5af64 --- /dev/null +++ b/plinth/modules/email_server/data/etc/dovecot/conf.d/90-freedombox-ssl.conf @@ -0,0 +1,12 @@ +# Direct edits to this file will be lost! +# Manage your settings on Plinth + +# Mozilla Guideline v5.6, Dovecot 2.3.9, OpenSSL 1.1.1d, intermediate +# Generated 2021-08 +# https://ssl-config.mozilla.org/ +ssl = required + +ssl_min_protocol = TLSv1.2 +ssl_prefer_server_ciphers = no + +ssl_cipher_list = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384