diff --git a/modules/default.nix b/modules/default.nix index 49f7f8a..d5fd80e 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -43,6 +43,7 @@ in ./initrd-ssh.nix ./libvirt.nix ./locales.nix + ./mailserver.nix ./media-proxy.nix ./network-manager.nix ./office.nix diff --git a/modules/mailserver.nix b/modules/mailserver.nix new file mode 100644 index 0000000..293af23 --- /dev/null +++ b/modules/mailserver.nix @@ -0,0 +1,344 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.sbruder.mailserver; + + certDir = config.security.acme.certs."${cfg.fqdn}".directory; +in +{ + options.sbruder.mailserver = with lib; with lib.types; { + enable = mkEnableOption "simple mail server"; + fqdn = mkOption { + type = str; + description = '' + FQDN of the mail server + + It needs to have a matching reverse DNS record. Also, an acme + certificate with this name has to be present. + ''; + example = "mail.example.com"; + }; + storage = mkOption { + type = path; + description = "Location of the storage for mails"; + default = "/var/vmail"; + }; + domains = mkOption { + type = listOf str; + description = "Domains to serve"; + example = [ "example.com" "example.org" ]; + }; + users = mkOption { + type = listOf (submodule { + options = { + address = mkOption { + type = str; + description = "Primary e-mail address of the user"; + example = "jdoe@example.com"; + }; + passwordHash = mkOption { + type = str; + description = '' + Bcrypt hash of the user’s password. Please note that it will be + world-readable in the nix store. + + You can generate a password with `nix run nixpkgs.apacheHttpd -c + htpasswd -nBC 12 "" | cut -d: -f2` + ''; + example = "$2y$05$SHxhwVGx.XCd19HAcb1NKuidUxW1BwU7GeO0ZIcMTc5t2uZoYLVRK"; + }; + aliases = mkOption { + type = listOf str; + description = '' + A list of aliases for the user. + + If multiple users have the same alias defined, mail will be + delivered to both of them. + ''; + default = [ ]; + example = [ + "j.doe@example.com" + "jane.doe@example.com" + "postmaster@example.com" + ]; + }; + }; + }); + description = "Users of the mail server"; + }; + cleanHeaders = mkOption { + type = listOf str; + description = "A list of regular expressions that define what headers are filtered"; + default = [ + "/^\\s*Received:/" + "/^\\s*User-Agent:/" + "/^\\s*X-Mailer:/" + "/^\\s*X-Originating-IP:/" + ]; + }; + rejectSenders = mkOption { + type = listOf str; + description = "A list of senders to reject mails from"; + default = [ ]; + example = [ + "newsletter@example.com" + "spammer@example.com" + ]; + }; + }; + + config = lib.mkIf cfg.enable { + # Users and groups + users.users.vmail = { + uid = 10000; + group = "vmail"; + home = cfg.storage; + createHome = true; + }; + + users.groups.vmail.gid = 10000; + + # Firewall + networking.firewall.allowedTCPPorts = [ + 143 # IMAP + 25 # SMTP + 587 # SMTP submission + ]; + + # Service dependencies + systemd.services.dovecot2 = { + wants = [ "acme-finished-${cfg.fqdn}.target" ]; + after = [ "acme-finished-${cfg.fqdn}.target" ]; + }; + systemd.services.postfix = { + wants = [ "acme-finished-${cfg.fqdn}.target" ]; + requires = [ "dovecot2.service" ]; + after = [ "acme-finished-${cfg.fqdn}.target" "dovecot2.service" ]; + }; + + # Reload on certificate renewal + security.acme.certs."${cfg.fqdn}".postRun = '' + if systemctl is-active dovecot2; then + systemctl --no-block reload dovecot2 + fi + if systemctl is-active postfix; then + systemctl --no-block reload postfix + fi + ''; + + # Postfix + security.dhparams.params.postfix = { }; + services.postfix = + let + listToString = lib.concatStringsSep ","; + + valiases = + let + # List of attribute sets with single key-value pair + plainAliases = (lib.flatten + (map + ({ address, aliases, ... }: + map + (alias: { "${alias}" = address; }) + (aliases ++ lib.singleton address)) + cfg.users)); + + # Attribute set with every alias mapped to a list of receivers + mergedAliases = (lib.attrsets.foldAttrs + (val: col: lib.singleton val ++ col) + [ ] + plainAliases); + + # Contents of the aliases file + aliasesString = (lib.concatStringsSep + "\n" + (lib.mapAttrsToList + (alias: addresses: "${alias} ${listToString addresses}") + mergedAliases)); + in + pkgs.writeText + "valiases" + aliasesString; + + access_sender = pkgs.writeText + "access_sender" + (lib.concatMapStringsSep + "\n" + (sender: "${sender} REJECT") + cfg.rejectSenders); + + submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" + (lib.concatMapStringsSep + "\n" + (regex: "${regex} IGNORE") + cfg.cleanHeaders); + in + { + enable = true; + + enableSubmission = true; + + hostname = cfg.fqdn; + networksStyle = "host"; + sslCert = "${certDir}/fullchain.pem"; + sslKey = "${certDir}/key.pem"; + + recipientDelimiter = "+"; + + mapFiles = { + inherit access_sender valiases; + }; + + config = { + # General + smtpd_banner = "${cfg.fqdn} ESMTP NO UCE"; + disable_vrfy_command = true; # disable check if mailbox exists + enable_long_queue_ids = true; # better for debugging + strict_rfc821_envelopes = true; # only accept properly formatted envelope + message_size_limit = "50331648"; # 48 MiB + + virtual_mailbox_domains = listToString cfg.domains; + virtual_mailbox_maps = "hash:/var/lib/postfix/conf/valiases"; + virtual_alias_maps = "hash:/var/lib/postfix/conf/valiases"; + virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp"; + + smtpd_recipient_restrictions = listToString [ + "reject_non_fqdn_recipient" + "reject_rbl_client ix.dnsbl.manitu.net" + "reject_unknown_recipient_domain" + "reject_unverified_recipient" + ]; + + smtpd_client_restrictions = listToString [ + "reject_rbl_client ix.dnsbl.manitu.net" + "reject_unknown_client_hostname" + ]; + + smtpd_sender_restrictions = listToString [ + "check_sender_access hash:/var/lib/postfix/conf/access_sender" + "reject_non_fqdn_sender" + "reject_unknown_sender_domain" + ]; + + # generated 2021-02-04, Mozilla Guideline v5.6, Postfix 3.5.6, OpenSSL 1.1.1i, intermediate configuration + # https://ssl-config.mozilla.org/#server=postfix&version=3.5.6&config=intermediate&openssl=1.1.1i&guideline=5.6 + smtpd_tls_security_level = "may"; + smtpd_tls_auth_only = "yes"; + smtpd_tls_mandatory_protocols = "!SSLv2, !SSLv3, !TLSv1, !TLSv1.1"; + smtpd_tls_protocols = "!SSLv2, !SSLv3, !TLSv1, !TLSv1.1"; + smtpd_tls_mandatory_ciphers = "medium"; + smtpd_tls_loglevel = "1"; + + tls_medium_cipherlist = listToString [ + "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" + ]; + tls_preempt_cipherlist = "no"; + + smtpd_tls_dh1024_param_file = config.security.dhparams.params.postfix.path; + }; + + submissionOptions = { + smtpd_tls_security_level = "encrypt"; + smtpd_sasl_auth_enable = "yes"; + smtpd_sasl_type = "dovecot"; + smtpd_sasl_path = "/run/dovecot2/auth"; + + smtpd_sender_login_maps = "hash:/etc/postfix/valiases"; + + smtpd_client_restrictions = listToString [ + "permit_sasl_authenticated" + "reject" + ]; + + smtpd_sender_restrictions = listToString [ + "reject_sender_login_mismatch" + ]; + + cleanup_service_name = "submission-header-cleanup"; + }; + + masterConfig = { + submission-header-cleanup = { + private = false; + maxproc = 0; + command = "cleanup"; + args = [ "-o" "header_checks=pcre:${submissionHeaderCleanupRules}" ]; + }; + }; + }; + + # Dovecot + services.dovecot2 = + let + postfixCfg = config.services.postfix; + + passdb = pkgs.writeText "dovecot-users" + (lib.concatMapStringsSep + "\n" + ({ address, passwordHash, ... }: "${address}:{BLF-CRYPT}${passwordHash}") + cfg.users); + in + { + enable = true; + + enableLmtp = true; + enablePAM = false; + + mailUser = "vmail"; + mailGroup = "vmail"; + mailLocation = "maildir:${cfg.storage}/%d/%n"; + + sslServerCert = "${certDir}/fullchain.pem"; + sslServerKey = "${certDir}/key.pem"; + + extraConfig = '' + # generated 2021-02-04, Mozilla Guideline v5.6, Dovecot 2.3.13, OpenSSL 1.1.1i, intermediate configuration + # https://ssl-config.mozilla.org/#server=dovecot&version=2.3.13&config=intermediate&openssl=1.1.1i&guideline=5.6 + ssl = required + ssl_min_protocol = TLSv1.2 + 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 + ssl_prefer_server_ciphers = no + + service imap-login { + inet_listener imap { + } + } + + service lmtp { + unix_listener dovecot-lmtp { + mode = 0600 + user = ${postfixCfg.user} + group = ${postfixCfg.group} + } + } + + passdb { + driver = passwd-file + args = username_format=%u ${passdb} + } + + userdb { + driver = static + args = uid=vmail gid=vmail home=${cfg.storage}/%d/%n + } + + service auth { + unix_listener auth { + mode = 0660 + user = ${postfixCfg.user} + group = ${postfixCfg.group} + } + } + + lda_mailbox_autosubscribe = yes + lda_mailbox_autocreate = yes + ''; + }; + }; +}