nixos/maddy: Add tls option

Changed files
+186 -7
nixos
doc
manual
release-notes
modules
services
mail
tests
+3 -1
nixos/doc/manual/release-notes/rl-2305.section.md
···
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`.
- The `dnsmasq` service now takes configuration via the
`services.dnsmasq.settings` attribute set. The option
···
replacement. It stores backups as volume dump files and thus better integrates
into contemporary backup solutions.
+
- `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
+80 -3
nixos/modules/services/mail/maddy.nix
···
# 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
···
}
optional_step file /etc/maddy/aliases
}
msgpipeline local_routing {
destination postmaster $(local_domains) {
modify {
···
'';
};
openFirewall = mkOption {
type = types.bool;
default = false;
···
};
ensureAccounts = mkOption {
-
type = types.listOf types.str;
default = [];
description = lib.mdDoc ''
List of IMAP accounts which get automatically created. Note that for
···
config = mkIf cfg.enable {
systemd = {
packages = [ pkgs.maddy ];
···
$(primary_domain) = ${cfg.primaryDomain}
$(local_domains) = ${toString cfg.localDomains}
hostname ${cfg.hostname}
${cfg.config}
'';
};
···
# configuration here https://github.com/foxcpp/maddy/blob/master/maddy.conf
# Do not use this in production!
auth.pass_table local_authdb {
table sql_table {
driver sqlite3
···
}
optional_step file /etc/maddy/aliases
}
+
msgpipeline local_routing {
destination postmaster $(local_domains) {
modify {
···
'';
};
+
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;
···
};
ensureAccounts = mkOption {
+
type = with types; listOf str;
default = [];
description = lib.mdDoc ''
List of IMAP accounts which get automatically created. Note that for
···
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 ];
···
$(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}
'';
};
+1 -1
nixos/tests/all-tests.nix
···
lxd-image-server = handleTest ./lxd-image-server.nix {};
#logstash = handleTest ./logstash.nix {};
lorri = handleTest ./lorri/default.nix {};
-
maddy = handleTest ./maddy.nix {};
maestral = handleTest ./maestral.nix {};
magic-wormhole-mailbox-server = handleTest ./magic-wormhole-mailbox-server.nix {};
magnetico = handleTest ./magnetico.nix {};
···
lxd-image-server = handleTest ./lxd-image-server.nix {};
#logstash = handleTest ./logstash.nix {};
lorri = handleTest ./lorri/default.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 {};
+2 -2
nixos/tests/maddy.nix nixos/tests/maddy/unencrypted.nix
···
-
import ./make-test-python.nix ({ pkgs, ... }: {
-
name = "maddy";
meta = with pkgs.lib.maintainers; { maintainers = [ onny ]; };
nodes = {
···
+
import ../make-test-python.nix ({ pkgs, ... }: {
+
name = "maddy-unencrypted";
meta = with pkgs.lib.maintainers; { maintainers = [ onny ]; };
nodes = {
+6
nixos/tests/maddy/default.nix
···
···
+
{ handleTest }:
+
+
{
+
unencrypted = handleTest ./unencrypted.nix { };
+
tls = handleTest ./tls.nix { };
+
}
+94
nixos/tests/maddy/tls.nix
···
···
+
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")
+
'';
+
})