]> git.immae.eu Git - perso/Immae/Config/Nix.git/blobdiff - systems/eldiron/mail/postfix.nix
Squash changes containing private information
[perso/Immae/Config/Nix.git] / systems / eldiron / mail / postfix.nix
diff --git a/systems/eldiron/mail/postfix.nix b/systems/eldiron/mail/postfix.nix
new file mode 100644 (file)
index 0000000..f95ee1b
--- /dev/null
@@ -0,0 +1,497 @@
+{ lib, pkgs, config, options, ... }:
+let
+  getDomains = p: lib.mapAttrsToList (n: v: v.fqdn) (lib.filterAttrs (n: v: v.receive) p.emailPolicies);
+  bydomain = builtins.mapAttrs (n: getDomains) config.myServices.dns.zones;
+  receiving_domains = lib.flatten (builtins.attrValues bydomain);
+in
+{
+  options.services.postfix.submissionOptions' = options.services.postfix.submissionOptions // {
+    type = with lib.types; attrsOf (either str (listOf str));
+    apply = builtins.mapAttrs (n: v: if builtins.isList v then builtins.concatStringsSep "," v else v);
+  };
+  config = lib.mkIf config.myServices.mail.enable {
+    myServices.dns.zones."immae.eu" = with config.myServices.dns.helpers; lib.mkMerge [
+      mailMX
+      (mailCommon "immae.eu")
+      mailSend
+      {
+        # Virtual forwards and mailboxes for real users
+        emailPolicies."mail".receive = true;
+        # multi-domain generic mails:
+        #     hostmaster, cron, httpd, naemon, postmaster
+        # system virtual mailboxes:
+        #     devnull, printer, testconnect
+        emailPolicies."".receive = true;
+        subdomains.mail = lib.mkMerge [ (mailCommon "immae.eu") mailSend ];
+        subdomains.smtp = ips servers.eldiron.ips.main;
+
+        # DMARC reports
+        subdomains._dmarc.subdomains._report.subdomains = let
+          getDomains = p: lib.mapAttrsToList (n: v: v.fqdn) p.emailPolicies;
+          bydomain = builtins.mapAttrs (n: getDomains) config.myServices.dns.zones;
+          hostsWithMail = lib.flatten (builtins.attrValues bydomain);
+          nvpairs = builtins.map (e: { name = e; value = { TXT = [ "v=DMARC1;" ]; }; }) hostsWithMail;
+        in
+          builtins.listToAttrs nvpairs;
+      }
+    ];
+
+    myServices.chatonsProperties.hostings.mx-backup = {
+      file.datetime = "2022-08-22T01:00:00";
+      hosting = {
+        name = "MX Backup";
+        description = "Serveur e-mail secondaire";
+        logo = "https://www.postfix.org/favicon.ico";
+        website = "https://mail.immae.eu/";
+        status.level = "OK";
+        status.description = "OK";
+        registration.load = "OPEN";
+        install.type = "PACKAGE";
+      };
+      software = {
+        name = "Postfix";
+        website = "http://www.postfix.org/";
+        license.url = "http://postfix.mirrors.ovh.net/postfix-release/LICENSE";
+        license.name = "Eclipse Public license (EPL 2.0) and IBM Public License (IPL 1.0)";
+        version = pkgs.postfix.version;
+        source.url = "http://www.postfix.org/download.html";
+      };
+    };
+    secrets.keys = {
+      "postfix/mysql_alias_maps" = {
+        user = config.services.postfix.user;
+        group = config.services.postfix.group;
+        permissions = "0440";
+        text = ''
+          # We need to specify that option to trigger ssl connection
+          tls_ciphers = TLSv1.2
+          user = ${config.myEnv.mail.postfix.mysql.user}
+          password = ${config.myEnv.mail.postfix.mysql.password}
+          hosts = unix:${config.myEnv.mail.postfix.mysql.socket}
+          dbname = ${config.myEnv.mail.postfix.mysql.database}
+          query = SELECT DISTINCT destination
+            FROM forwardings
+            WHERE
+              ((regex = 1 AND '%s' REGEXP CONCAT('^',source,'$') ) OR (regex = 0 AND source = '%s'))
+              AND active = 1
+              AND '%s' NOT IN
+              (
+                SELECT source
+                FROM forwardings_blacklisted
+                WHERE source = '%s'
+              ) UNION
+              SELECT 'devnull@immae.eu'
+              FROM forwardings_blacklisted
+              WHERE source = '%s'
+          '';
+      };
+      "postfix/ldap_mailboxes" = {
+        user = config.services.postfix.user;
+        group = config.services.postfix.group;
+        permissions = "0440";
+        text = ''
+          server_host = ldaps://${config.myEnv.mail.dovecot.ldap.host}:636
+          search_base = ${config.myEnv.mail.dovecot.ldap.base}
+          query_filter = ${config.myEnv.mail.dovecot.ldap.postfix_mailbox_filter}
+          bind_dn = ${config.myEnv.mail.dovecot.ldap.dn}
+          bind_pw = ${config.myEnv.mail.dovecot.ldap.password}
+          result_attribute = immaePostfixAddress
+          result_format = dummy
+          version = 3
+        '';
+      };
+      "postfix/mysql_sender_login_maps" = {
+        user = config.services.postfix.user;
+        group = config.services.postfix.group;
+        permissions = "0440";
+        text = ''
+          # We need to specify that option to trigger ssl connection
+          tls_ciphers = TLSv1.2
+          user = ${config.myEnv.mail.postfix.mysql.user}
+          password = ${config.myEnv.mail.postfix.mysql.password}
+          hosts = unix:${config.myEnv.mail.postfix.mysql.socket}
+          dbname = ${config.myEnv.mail.postfix.mysql.database}
+          query = SELECT DISTINCT destination
+            FROM forwardings
+            WHERE
+              (
+                (regex = 1 AND CONCAT(SUBSTRING_INDEX('%u', '+', 1), '@%d') REGEXP CONCAT('^',source,'$') )
+              OR
+                (regex = 0 AND source = CONCAT(SUBSTRING_INDEX('%u', '+', 1), '@%d'))
+              )
+              AND active = 1
+            UNION SELECT CONCAT(SUBSTRING_INDEX('%u', '+', 1), '@%d') AS destination
+          '';
+      };
+      "postfix/mysql_sender_relays_maps" = {
+        user = config.services.postfix.user;
+        group = config.services.postfix.group;
+        permissions = "0440";
+        text = ''
+          # We need to specify that option to trigger ssl connection
+          tls_ciphers = TLSv1.2
+          user = ${config.myEnv.mail.postfix.mysql.user}
+          password = ${config.myEnv.mail.postfix.mysql.password}
+          hosts = unix:${config.myEnv.mail.postfix.mysql.socket}
+          dbname = ${config.myEnv.mail.postfix.mysql.database}
+          # INSERT INTO sender_relays
+          #   (`from`, owner, relay, login, password, regex, active)
+          # VALUES
+          #   ( 'sender@otherhost.org'
+          #   , 'me@mail.immae.eu'
+          #   , '[otherhost.org]:587'
+          #   , 'otherhostlogin'
+          #   , AES_ENCRYPT('otherhostpassword', '${config.myEnv.mail.postfix.mysql.password_encrypt}')
+          #   , '0'
+          #   , '1');
+
+          query = SELECT DISTINCT `owner`
+            FROM sender_relays
+            WHERE
+              ((regex = 1 AND '%s' REGEXP CONCAT('^',`from`,'$') ) OR (regex = 0 AND `from` = '%s'))
+              AND active = 1
+          '';
+      };
+      "postfix/mysql_sender_relays_hosts" = {
+        user = config.services.postfix.user;
+        group = config.services.postfix.group;
+        permissions = "0440";
+        text = ''
+          # We need to specify that option to trigger ssl connection
+          tls_ciphers = TLSv1.2
+          user = ${config.myEnv.mail.postfix.mysql.user}
+          password = ${config.myEnv.mail.postfix.mysql.password}
+          hosts = unix:${config.myEnv.mail.postfix.mysql.socket}
+          dbname = ${config.myEnv.mail.postfix.mysql.database}
+
+          query = SELECT DISTINCT relay
+            FROM sender_relays
+            WHERE
+              ((regex = 1 AND '%s' REGEXP CONCAT('^',`from`,'$') ) OR (regex = 0 AND `from` = '%s'))
+              AND active = 1
+          '';
+      };
+      "postfix/mysql_sender_relays_creds" = {
+        user = config.services.postfix.user;
+        group = config.services.postfix.group;
+        permissions = "0440";
+        text = ''
+          # We need to specify that option to trigger ssl connection
+          tls_ciphers = TLSv1.2
+          user = ${config.myEnv.mail.postfix.mysql.user}
+          password = ${config.myEnv.mail.postfix.mysql.password}
+          hosts = unix:${config.myEnv.mail.postfix.mysql.socket}
+          dbname = ${config.myEnv.mail.postfix.mysql.database}
+
+          query = SELECT DISTINCT CONCAT(`login`, ':', AES_DECRYPT(`password`, '${config.myEnv.mail.postfix.mysql.password_encrypt}'))
+            FROM sender_relays
+            WHERE
+              ((regex = 1 AND '%s' REGEXP CONCAT('^',`from`,'$') ) OR (regex = 0 AND `from` = '%s'))
+              AND active = 1
+          '';
+      };
+      "postfix/ldap_ejabberd_users_immae_fr" = {
+        user = config.services.postfix.user;
+        group = config.services.postfix.group;
+        permissions = "0440";
+        text = ''
+          server_host = ldaps://${config.myEnv.jabber.ldap.host}:636
+          search_base = ${config.myEnv.jabber.ldap.base}
+          query_filter = ${config.myEnv.jabber.postfix_user_filter}
+          domain = immae.fr
+          bind_dn = ${config.myEnv.jabber.ldap.dn}
+          bind_pw = ${config.myEnv.jabber.ldap.password}
+          result_attribute = immaeXmppUid
+          result_format = ejabberd@localhost
+          version = 3
+          '';
+      };
+    };
+
+    networking.firewall.allowedTCPPorts = [ 25 465 587 ];
+
+    users.users.postfixscripts = {
+      group = "keys";
+      uid = config.ids.uids.postfixscripts;
+      description = "Postfix scripts user";
+    };
+    users.users."${config.services.postfix.user}".extraGroups = [ "keys" ];
+    services.filesWatcher.postfix = {
+      restart = true;
+      paths = [
+        config.secrets.fullPaths."postfix/mysql_alias_maps"
+        config.secrets.fullPaths."postfix/ldap_mailboxes"
+        config.secrets.fullPaths."postfix/mysql_sender_login_maps"
+        config.secrets.fullPaths."postfix/ldap_ejabberd_users_immae_fr"
+      ];
+    };
+    services.postfix = {
+      extraAliases = let
+        testmail = pkgs.writeScript "testmail" ''
+          #! ${pkgs.stdenv.shell}
+          ${pkgs.coreutils}/bin/touch \
+            "/var/lib/naemon/checks/email/$(${pkgs.procmail}/bin/formail -x To: | ${pkgs.coreutils}/bin/tr -d ' <>')"
+          '';
+      in
+        ''testmail: "|${testmail}"'';
+      mapFiles = let
+        virtual_map = {
+          virtual = let
+            cfg = config.myEnv.monitoring.email_check.eldiron;
+            address = "${cfg.mail_address}@${cfg.mail_domain}";
+            aliases = config.myEnv.mail.postfix.common_aliases;
+            admins = builtins.concatStringsSep "," config.myEnv.mail.postfix.admins;
+          in pkgs.writeText "postfix-virtual" (
+            builtins.concatStringsSep "\n" (
+              [ "${address} testmail@localhost"
+              ] ++
+              map (a: "${a} ${admins}") config.myEnv.mail.postfix.other_aliases
+              ++ lib.lists.flatten (
+                map (domain:
+                  map (alias: "${alias}@${domain} ${admins}") aliases
+                ) receiving_domains
+                )
+          ));
+        };
+        sasl_access = {
+          host_sender_login = with lib.attrsets; let
+            addresses = zipAttrs (lib.flatten (mapAttrsToList
+              (n: v: (map (e: { "${e}" = "${n}@immae.eu"; }) v.emails)) config.myEnv.servers));
+            aliases = config.myEnv.mail.postfix.common_aliases;
+            joined = builtins.concatStringsSep ",";
+            admins = joined config.myEnv.mail.postfix.admins;
+          in pkgs.writeText "host-sender-login"
+            (builtins.concatStringsSep "\n" (
+              mapAttrsToList (n: v: "${n} ${joined v}") addresses
+              ++ lib.lists.flatten (
+                map (domain:
+                  map (alias: "${alias}@${domain} ${admins}") aliases
+                ) receiving_domains
+                )
+              ++ map (a: "${a} ${admins}") config.myEnv.mail.postfix.other_aliases
+          ));
+        };
+      in
+        virtual_map // sasl_access;
+      config = {
+        ### postfix module overrides
+        readme_directory = "${pkgs.postfix}/share/postfix/doc";
+        smtp_tls_CAfile = lib.mkForce "";
+        smtp_tls_cert_file = lib.mkForce "";
+        smtp_tls_key_file = lib.mkForce "";
+
+        message_size_limit = "1073741824"; # Don't put 0 here, it's not equivalent to "unlimited"
+        mailbox_size_limit = "1073741825"; # Workaround, local delivered mails should all go through scripts
+        alias_database = "\$alias_maps";
+
+        ### Aliases scripts user
+        default_privs = "postfixscripts";
+
+        ### Virtual mailboxes config
+        virtual_alias_maps = [
+          "hash:/etc/postfix/virtual"
+          "mysql:${config.secrets.fullPaths."postfix/mysql_alias_maps"}"
+          "ldap:${config.secrets.fullPaths."postfix/ldap_ejabberd_users_immae_fr"}"
+        ];
+        virtual_mailbox_domains = receiving_domains;
+        virtual_mailbox_maps = [
+          "ldap:${config.secrets.fullPaths."postfix/ldap_mailboxes"}"
+        ];
+        dovecot_destination_recipient_limit = "1";
+        virtual_transport = "dovecot";
+
+        ### Relay domains
+        smtpd_relay_restrictions = [
+          "defer_unauth_destination"
+        ];
+
+        ### Additional smtpd configuration
+        smtpd_tls_received_header = "yes";
+        smtpd_tls_loglevel = "1";
+
+        ### Email sending configuration
+        smtp_tls_security_level = "may";
+        smtp_tls_loglevel = "1";
+
+        ### Force ip bind for smtp
+        smtp_bind_address  = builtins.head config.hostEnv.ips.main.ip4;
+        smtp_bind_address6 = builtins.head config.hostEnv.ips.main.ip6;
+
+        # Use some relays when authorized senders are not myself
+        smtp_sasl_mechanism_filter = [
+          "plain"
+          "login"
+        ]; # GSSAPI Not correctly supported by postfix
+        smtp_sasl_auth_enable = "yes";
+        smtp_sasl_password_maps = [
+          "mysql:${config.secrets.fullPaths."postfix/mysql_sender_relays_creds"}"
+        ];
+        smtp_sasl_security_options = "noanonymous";
+        smtp_sender_dependent_authentication = "yes";
+        sender_dependent_relayhost_maps = [
+          "mysql:${config.secrets.fullPaths."postfix/mysql_sender_relays_hosts"}"
+        ];
+
+        ### opendkim, opendmarc, openarc milters
+        non_smtpd_milters = [
+          "unix:${config.myServices.mail.milters.sockets.opendkim}"
+        ];
+        smtpd_milters = [
+          "unix:${config.myServices.mail.milters.sockets.opendkim}"
+          "unix:${config.myServices.mail.milters.sockets.openarc}"
+          "unix:${config.myServices.mail.milters.sockets.opendmarc}"
+        ];
+
+        smtp_use_tls = true;
+        smtpd_use_tls = true;
+        smtpd_tls_chain_files = [
+          "/var/lib/acme/mail/full.pem"
+          "/var/lib/acme/mail-rsa/full.pem"
+        ];
+
+        maximal_queue_lifetime = "6w";
+        bounce_queue_lifetime = "6w";
+      };
+      enable = true;
+      enableSmtp = true;
+      enableSubmission = true;
+      submissionOptions = config.services.postfix.submissionOptions';
+      submissionOptions' = {
+        # Don’t use "long form", only commas (cf
+        # http://www.postfix.org/master.5.html long form is not handled
+        # well by the submission function)
+        smtpd_tls_security_level = "encrypt";
+        smtpd_sasl_auth_enable = "yes";
+        smtpd_tls_auth_only = "yes";
+        smtpd_sasl_tls_security_options = "noanonymous";
+        smtpd_sasl_type = "dovecot";
+        smtpd_sasl_path = "private/auth";
+        smtpd_reject_unlisted_recipient = "no";
+        smtpd_client_restrictions = [
+          "permit_sasl_authenticated"
+          "reject"
+        ];
+        smtpd_relay_restrictions = [
+          "permit_sasl_authenticated"
+          "reject"
+        ];
+        # Refuse to send e-mails with a From that is not handled
+        smtpd_sender_restrictions = [
+          "reject_sender_login_mismatch"
+          "reject_unlisted_sender"
+          "permit_sasl_authenticated,reject"
+        ];
+        smtpd_sender_login_maps = [
+          "hash:/etc/postfix/host_sender_login"
+          "mysql:${config.secrets.fullPaths."postfix/mysql_sender_relays_maps"}"
+          "mysql:${config.secrets.fullPaths."postfix/mysql_sender_login_maps"}"
+        ];
+        smtpd_recipient_restrictions = [
+          "permit_sasl_authenticated"
+          "reject"
+        ];
+        milter_macro_daemon_name = "ORIGINATING";
+        smtpd_milters = [
+          # FIXME: put it back when opensmtpd is upgraded and able to
+          # rewrite the from header
+          #"unix:/run/milter_verify_from/verify_from.sock"
+          "unix:${config.myServices.mail.milters.sockets.opendkim}"
+        ];
+      };
+      destination = ["localhost"];
+      # This needs to reverse DNS
+      hostname = config.hostEnv.fqdn;
+      setSendmail = true;
+      recipientDelimiter = "+";
+      masterConfig = {
+        submissions = {
+          type = "inet";
+          private = false;
+          command = "smtpd";
+          args = ["-o" "smtpd_tls_wrappermode=yes" ] ++ (let
+            mkKeyVal = opt: val: [ "-o" (opt + "=" + val) ];
+          in lib.concatLists (lib.mapAttrsToList mkKeyVal config.services.postfix.submissionOptions)
+          );
+        };
+        dovecot = {
+          type = "unix";
+          privileged = true;
+          chroot = false;
+          command = "pipe";
+          args = let
+            # rspamd could be used as a milter, but then it cannot apply
+            # its checks "per user" (milter is not yet dispatched to
+            # users), so we wrap dovecot-lda inside rspamc per recipient
+            # here.
+            rspamc_dovecot = pkgs.writeScriptBin "rspamc_dovecot" ''
+              #! ${pkgs.stdenv.shell}
+              set -o pipefail
+              sender="$1"
+              original_recipient="$2"
+              user="$3"
+
+              ${pkgs.coreutils}/bin/cat - | \
+                ${pkgs.rspamd}/bin/rspamc -h ${config.myServices.mail.rspamd.sockets.worker-controller} -c bayes -d "$user" --mime | \
+                ${pkgs.dovecot}/libexec/dovecot/dovecot-lda -f "$sender" -a "$original_recipient" -d "$user"
+              if echo ''${PIPESTATUS[@]} | ${pkgs.gnugrep}/bin/grep -qE '^[0 ]+$'; then
+                exit 0
+              else
+                # src/global/sys_exits.h to retry
+                exit 75
+              fi
+              '';
+          in [
+            "flags=ODRhu" "user=vhost:vhost"
+            "argv=${rspamc_dovecot}/bin/rspamc_dovecot \${sender} \${original_recipient} \${user}@\${nexthop}"
+          ];
+        };
+      };
+    };
+    security.acme.certs."mail" = {
+      postRun = ''
+        systemctl restart postfix.service
+        '';
+      extraDomainNames = [ "smtp.immae.eu" ];
+    };
+    security.acme.certs."mail-rsa" = {
+      postRun = ''
+        systemctl restart postfix.service
+        '';
+      extraDomainNames = [ "smtp.immae.eu" ];
+    };
+    system.activationScripts.testmail = {
+      deps = [ "users" ];
+      text = let
+        allCfg = config.myEnv.monitoring.email_check;
+        cfg = allCfg.eldiron;
+        reverseTargets = builtins.attrNames (lib.attrsets.filterAttrs (k: v: builtins.elem "eldiron" v.targets) allCfg);
+        to_email = cfg': host':
+          let sep = if lib.strings.hasInfix "+" cfg'.mail_address then "_" else "+";
+          in "${cfg'.mail_address}${sep}${host'}@${cfg'.mail_domain}";
+        mails_to_receive = builtins.concatStringsSep " " (map (to_email cfg) reverseTargets);
+      in ''
+        install -m 0555 -o postfixscripts -g keys -d /var/lib/naemon/checks/email
+        for f in ${mails_to_receive}; do
+          if [ ! -f /var/lib/naemon/checks/email/$f ]; then
+            install -m 0644 -o postfixscripts -g keys /dev/null -T /var/lib/naemon/checks/email/$f
+            touch -m -d @0 /var/lib/naemon/checks/email/$f
+          fi
+        done
+        '';
+    };
+    systemd.services.postfix.serviceConfig.Slice = "mail.slice";
+
+    myServices.monitoring.fromMasterObjects.service = [
+      {
+        service_description = "postfix SSL is up to date";
+        host_name = config.hostEnv.fqdn;
+        use = "external-service";
+        check_command = "check_smtp";
+
+        servicegroups = "webstatus-ssl";
+        _webstatus_name = "SMTP";
+        _webstatus_url = "smtp.immae.eu";
+      }
+    ];
+  };
+}