diff --git a/nixos/doc/manual/release-notes/rl-2305.section.md b/nixos/doc/manual/release-notes/rl-2305.section.md index da4453b43e17..8458d1246add 100644 --- a/nixos/doc/manual/release-notes/rl-2305.section.md +++ b/nixos/doc/manual/release-notes/rl-2305.section.md @@ -322,7 +322,9 @@ In addition to numerous new and upgraded packages, this release has the followin replacement. It stores backups as volume dump files and thus better integrates into contemporary backup solutions. -- `services.maddy` now allows to configure users and their credentials using `services.maddy.ensureCredentials`. +- `services.maddy` got several updates: + - Configuration of users and their credentials using `services.maddy.ensureCredentials`. + - Configuration of TLS key and certificate files using `services.maddy.tls`. - The `dnsmasq` service now takes configuration via the `services.dnsmasq.settings` attribute set. The option diff --git a/nixos/modules/services/mail/maddy.nix b/nixos/modules/services/mail/maddy.nix index d0b525bcb002..e11a18cc1428 100644 --- a/nixos/modules/services/mail/maddy.nix +++ b/nixos/modules/services/mail/maddy.nix @@ -13,8 +13,6 @@ let # configuration here https://github.com/foxcpp/maddy/blob/master/maddy.conf # Do not use this in production! - tls off - auth.pass_table local_authdb { table sql_table { driver sqlite3 @@ -35,6 +33,7 @@ let } optional_step file /etc/maddy/aliases } + msgpipeline local_routing { destination postmaster $(local_domains) { modify { @@ -215,6 +214,63 @@ in { ''; }; + tls = { + loader = mkOption { + type = with types; nullOr (enum [ "file" "off" ]); + default = "off"; + description = lib.mdDoc '' + TLS certificates are obtained by modules called "certificate + loaders". Currently only the file loader is supported which reads + certificates from files specifying the options `keyPaths` and + `certPaths`. + ''; + }; + + certificates = mkOption { + type = with types; listOf (submodule { + options = { + keyPath = mkOption { + type = types.path; + example = "/etc/ssl/mx1.example.org.key"; + description = lib.mdDoc '' + Path to the private key used for TLS. + ''; + }; + certPath = mkOption { + type = types.path; + example = "/etc/ssl/mx1.example.org.crt"; + description = lib.mdDoc '' + Path to the certificate used for TLS. + ''; + }; + }; + }); + default = []; + example = lib.literalExpression '' + [{ + keyPath = "/etc/ssl/mx1.example.org.key"; + certPath = "/etc/ssl/mx1.example.org.crt"; + }] + ''; + description = lib.mdDoc '' + A list of attribute sets containing paths to TLS certificates and + keys. Maddy will use SNI if multiple pairs are selected. + ''; + }; + + extraConfig = mkOption { + type = with types; nullOr lines; + description = lib.mdDoc '' + Arguments for the specific certificate loader. Note that Maddy uses + secure defaults for the TLS configuration so there is no need to + change anything in most cases. + See [upstream manual](https://maddy.email/reference/tls/) for + available options. + ''; + default = ""; + }; + }; + openFirewall = mkOption { type = types.bool; default = false; @@ -224,7 +280,7 @@ in { }; ensureAccounts = mkOption { - type = types.listOf types.str; + type = with types; listOf str; default = []; description = lib.mdDoc '' List of IMAP accounts which get automatically created. Note that for @@ -270,6 +326,16 @@ in { config = mkIf cfg.enable { + assertions = [{ + assertion = cfg.tls.loader == "file" -> cfg.tls.certificates != []; + message = '' + If maddy is configured to use TLS, tls.certificates with attribute sets + of certPath and keyPath must be provided. + Read more about obtaining TLS certificates here: + https://maddy.email/tutorials/setting-up/#tls-certificates + ''; + }]; + systemd = { packages = [ pkgs.maddy ]; @@ -318,6 +384,17 @@ in { $(primary_domain) = ${cfg.primaryDomain} $(local_domains) = ${toString cfg.localDomains} hostname ${cfg.hostname} + + ${if (cfg.tls.loader == "file") then '' + tls file ${concatStringsSep " " ( + map (x: x.certPath + " " + x.keyPath + ) cfg.tls.certificates)} ${optionalString (cfg.tls.extraConfig != "") '' + { ${cfg.tls.extraConfig} } + ''} + '' else if (cfg.tls.loader == "off") then '' + tls off + '' else ""} + ${cfg.config} ''; }; diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index e10395f0b2cd..79058ae41f45 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -393,7 +393,7 @@ in { lxd-image-server = handleTest ./lxd-image-server.nix {}; #logstash = handleTest ./logstash.nix {}; lorri = handleTest ./lorri/default.nix {}; - maddy = handleTest ./maddy.nix {}; + maddy = discoverTests (import ./maddy { inherit handleTest; }); maestral = handleTest ./maestral.nix {}; magic-wormhole-mailbox-server = handleTest ./magic-wormhole-mailbox-server.nix {}; magnetico = handleTest ./magnetico.nix {}; diff --git a/nixos/tests/maddy/default.nix b/nixos/tests/maddy/default.nix new file mode 100644 index 000000000000..043906863e64 --- /dev/null +++ b/nixos/tests/maddy/default.nix @@ -0,0 +1,6 @@ +{ handleTest }: + +{ + unencrypted = handleTest ./unencrypted.nix { }; + tls = handleTest ./tls.nix { }; +} diff --git a/nixos/tests/maddy/tls.nix b/nixos/tests/maddy/tls.nix new file mode 100644 index 000000000000..44da4cf2a3cf --- /dev/null +++ b/nixos/tests/maddy/tls.nix @@ -0,0 +1,94 @@ +import ../make-test-python.nix ({ pkgs, ... }: +let + certs = import ../common/acme/server/snakeoil-certs.nix; + domain = certs.domain; +in { + name = "maddy-tls"; + meta = with pkgs.lib.maintainers; { maintainers = [ onny ]; }; + + nodes = { + server = { options, ... }: { + services.maddy = { + enable = true; + hostname = domain; + primaryDomain = domain; + openFirewall = true; + ensureAccounts = [ "postmaster@${domain}" ]; + ensureCredentials = { + # Do not use this in production. This will make passwords world-readable + # in the Nix store + "postmaster@${domain}".passwordFile = "${pkgs.writeText "postmaster" "test"}"; + }; + tls = { + loader = "file"; + certificates = [{ + certPath = "${certs.${domain}.cert}"; + keyPath = "${certs.${domain}.key}"; + }]; + }; + # Enable TLS listeners. Configuring this via the module is not yet + # implemented. + config = builtins.replaceStrings [ + "imap tcp://0.0.0.0:143" + "submission tcp://0.0.0.0:587" + ] [ + "imap tls://0.0.0.0:993 tcp://0.0.0.0:143" + "submission tls://0.0.0.0:465 tcp://0.0.0.0:587" + ] options.services.maddy.config.default; + }; + # Not covered by openFirewall yet + networking.firewall.allowedTCPPorts = [ 993 465 ]; + }; + + client = { nodes, ... }: { + security.pki.certificateFiles = [ + certs.ca.cert + ]; + networking.extraHosts = '' + ${nodes.server.networking.primaryIPAddress} ${domain} + ''; + environment.systemPackages = [ + (pkgs.writers.writePython3Bin "send-testmail" { } '' + import smtplib + import ssl + from email.mime.text import MIMEText + + context = ssl.create_default_context() + msg = MIMEText("Hello World") + msg['Subject'] = 'Test' + msg['From'] = "postmaster@${domain}" + msg['To'] = "postmaster@${domain}" + with smtplib.SMTP_SSL(host='${domain}', port=465, context=context) as smtp: + smtp.login('postmaster@${domain}', 'test') + smtp.sendmail( + 'postmaster@${domain}', 'postmaster@${domain}', msg.as_string() + ) + '') + (pkgs.writers.writePython3Bin "test-imap" { } '' + import imaplib + + with imaplib.IMAP4_SSL('${domain}') as imap: + imap.login('postmaster@${domain}', 'test') + imap.select() + status, refs = imap.search(None, 'ALL') + assert status == 'OK' + assert len(refs) == 1 + status, msg = imap.fetch(refs[0], 'BODY[TEXT]') + assert status == 'OK' + assert msg[0][1].strip() == b"Hello World" + '') + ]; + }; + }; + + testScript = '' + start_all() + server.wait_for_unit("maddy.service") + server.wait_for_open_port(143) + server.wait_for_open_port(993) + server.wait_for_open_port(587) + server.wait_for_open_port(465) + client.succeed("send-testmail") + client.succeed("test-imap") + ''; +}) diff --git a/nixos/tests/maddy.nix b/nixos/tests/maddy/unencrypted.nix similarity index 95% rename from nixos/tests/maddy.nix rename to nixos/tests/maddy/unencrypted.nix index 742043033337..2420d461e4e7 100644 --- a/nixos/tests/maddy.nix +++ b/nixos/tests/maddy/unencrypted.nix @@ -1,5 +1,5 @@ -import ./make-test-python.nix ({ pkgs, ... }: { - name = "maddy"; +import ../make-test-python.nix ({ pkgs, ... }: { + name = "maddy-unencrypted"; meta = with pkgs.lib.maintainers; { maintainers = [ onny ]; }; nodes = {