2021-11-01 19:50:30 +00:00
|
|
|
{ config, pkgs, lib, ... }:
|
|
|
|
|
|
|
|
with lib;
|
|
|
|
let
|
|
|
|
cfg = config.cloud.mail;
|
|
|
|
|
|
|
|
name = "maddy";
|
|
|
|
|
|
|
|
stateDir = "/var/lib/${name}";
|
|
|
|
runtimeDir = "/run/${name}";
|
|
|
|
|
|
|
|
smtpPort = 58787;
|
|
|
|
smtpsPort = 46565;
|
|
|
|
imapPort = 9333;
|
|
|
|
mtaStsPort = 8003;
|
|
|
|
in
|
|
|
|
{
|
|
|
|
options.cloud.mail = {
|
|
|
|
enable = mkEnableOption "Enable the email server";
|
|
|
|
|
2021-12-01 19:38:53 +00:00
|
|
|
debug = mkEnableOption "Enable debugging";
|
|
|
|
|
2021-11-01 19:50:30 +00:00
|
|
|
package = mkOption {
|
|
|
|
type = types.package;
|
|
|
|
default = pkgs.maddy;
|
|
|
|
};
|
|
|
|
|
|
|
|
hostname = mkOption {
|
|
|
|
type = types.str;
|
|
|
|
default = "mx1.nkagami.me";
|
|
|
|
description = "The hostname where the server is run on";
|
|
|
|
};
|
|
|
|
|
|
|
|
primaryDomain = mkOption {
|
|
|
|
type = types.str;
|
|
|
|
default = "nkagami.me";
|
|
|
|
description = "The primary email domain";
|
|
|
|
};
|
|
|
|
|
|
|
|
additionalDomains = mkOption {
|
|
|
|
type = types.listOf types.str;
|
|
|
|
default = [ ];
|
|
|
|
description = "Additional domain names to be used";
|
|
|
|
};
|
|
|
|
|
|
|
|
tls = {
|
|
|
|
certFile = mkOption {
|
|
|
|
type = types.str;
|
|
|
|
description = "Path to the certificate file.";
|
|
|
|
};
|
|
|
|
keyFile = mkOption {
|
|
|
|
type = types.str;
|
|
|
|
description = "Path to the key file.";
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
usersFile = mkOption {
|
|
|
|
type = types.path;
|
|
|
|
description = ''
|
|
|
|
The file containing user:hashed-password pairs to be used in the mail server.
|
|
|
|
Check `maddyctl hash` to generate hashed passwords.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
config =
|
|
|
|
let
|
|
|
|
configFile = pkgs.writeText "maddy.conf" ''
|
|
|
|
# Globals
|
|
|
|
state_dir ${stateDir}
|
|
|
|
runtime_dir ${runtimeDir}
|
|
|
|
|
|
|
|
# Base variables
|
|
|
|
|
|
|
|
$(hostname) = ${cfg.hostname}
|
|
|
|
$(primary_domain) = ${cfg.primaryDomain}
|
|
|
|
$(local_domains) = ${cfg.primaryDomain} ${lib.strings.concatStringsSep " " cfg.additionalDomains}
|
|
|
|
|
|
|
|
tls file "${cfg.tls.certFile}" "${cfg.tls.keyFile}"
|
|
|
|
|
|
|
|
# Authentication
|
|
|
|
auth.pass_table local_authdb {
|
|
|
|
table file ${cfg.usersFile}
|
|
|
|
}
|
|
|
|
|
|
|
|
# Local storage
|
|
|
|
storage.imapsql local_mailboxes {
|
|
|
|
driver "postgres"
|
|
|
|
dsn "dbname='${name}' user='${name}' host='/run/postgresql' sslmode=disable"
|
|
|
|
appendlimit 256M
|
|
|
|
}
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
|
|
# SMTP endpoints + message routing
|
|
|
|
|
|
|
|
hostname $(hostname)
|
|
|
|
|
|
|
|
msgpipeline local_routing {
|
|
|
|
# Insert handling for special-purpose local domains here.
|
|
|
|
# e.g.
|
|
|
|
# destination lists.example.org {
|
|
|
|
# deliver_to lmtp tcp://127.0.0.1:8024
|
|
|
|
# }
|
|
|
|
|
|
|
|
destination postmaster $(local_domains) {
|
|
|
|
modify {
|
|
|
|
replace_rcpt regexp "(.+)\+(.+)@(.+)" "$1@$3"
|
|
|
|
replace_rcpt file /etc/maddy/aliases
|
|
|
|
}
|
|
|
|
|
|
|
|
deliver_to &local_mailboxes
|
|
|
|
}
|
|
|
|
|
|
|
|
default_destination {
|
|
|
|
reject 550 5.1.1 "User doesn't exist"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
smtp tcp://0.0.0.0:25 {
|
|
|
|
limits {
|
|
|
|
# Up to 20 msgs/sec across max. 10 SMTP connections.
|
|
|
|
all rate 20 1s
|
|
|
|
all concurrency 10
|
|
|
|
}
|
|
|
|
|
|
|
|
dmarc yes
|
|
|
|
check {
|
|
|
|
require_mx_record
|
|
|
|
dkim
|
|
|
|
spf
|
|
|
|
}
|
|
|
|
|
|
|
|
source $(local_domains) {
|
|
|
|
reject 501 5.1.8 "Use Submission for outgoing SMTP"
|
|
|
|
}
|
|
|
|
default_source {
|
|
|
|
destination postmaster $(local_domains) {
|
|
|
|
deliver_to &local_routing
|
|
|
|
}
|
|
|
|
default_destination {
|
|
|
|
reject 550 5.1.1 "User doesn't exist"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
submission tcp://0.0.0.0:${toString smtpPort} tls://0.0.0.0:${toString smtpsPort} {
|
|
|
|
limits {
|
|
|
|
# Up to 50 msgs/sec across any amount of SMTP connections.
|
|
|
|
all rate 50 1s
|
|
|
|
}
|
|
|
|
|
|
|
|
auth &local_authdb
|
|
|
|
|
|
|
|
source $(local_domains) {
|
|
|
|
destination postmaster $(local_domains) {
|
|
|
|
deliver_to &local_routing
|
|
|
|
}
|
|
|
|
default_destination {
|
|
|
|
modify {
|
|
|
|
dkim $(primary_domain) $(local_domains) default
|
|
|
|
}
|
|
|
|
deliver_to &remote_queue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
default_source {
|
|
|
|
reject 501 5.1.8 "Non-local sender domain"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
target.remote outbound_delivery {
|
|
|
|
limits {
|
|
|
|
# Up to 20 msgs/sec across max. 10 SMTP connections
|
|
|
|
# for each recipient domain.
|
|
|
|
destination rate 20 1s
|
|
|
|
destination concurrency 10
|
|
|
|
}
|
|
|
|
mx_auth {
|
|
|
|
dane
|
|
|
|
mtasts {
|
|
|
|
cache fs
|
|
|
|
fs_dir mtasts_cache/
|
|
|
|
}
|
|
|
|
local_policy {
|
|
|
|
min_tls_level encrypted
|
|
|
|
min_mx_level none
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
target.queue remote_queue {
|
|
|
|
target &outbound_delivery
|
|
|
|
|
|
|
|
autogenerated_msg_domain $(primary_domain)
|
|
|
|
bounce {
|
|
|
|
destination postmaster $(local_domains) {
|
|
|
|
deliver_to &local_routing
|
|
|
|
}
|
|
|
|
default_destination {
|
|
|
|
reject 550 5.0.0 "Refusing to send DSNs to non-local addresses"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
|
|
# IMAP endpoints
|
|
|
|
|
|
|
|
imap tls://0.0.0.0:${toString imapPort} {
|
|
|
|
auth &local_authdb
|
|
|
|
storage &local_mailboxes
|
|
|
|
}
|
|
|
|
'';
|
|
|
|
|
|
|
|
mtaStsDir = pkgs.writeTextDir ".well-known/mta-sts.txt" ''
|
|
|
|
version: STSv1
|
|
|
|
mode: enforce
|
|
|
|
max_age: 604800
|
|
|
|
mx: ${cfg.hostname}
|
|
|
|
'';
|
|
|
|
in
|
|
|
|
mkIf cfg.enable {
|
|
|
|
# users
|
|
|
|
users.users."${name}" = {
|
|
|
|
group = "${name}";
|
|
|
|
isSystemUser = true;
|
|
|
|
};
|
|
|
|
users.groups."${name}" = { };
|
|
|
|
|
|
|
|
# database
|
|
|
|
cloud.postgresql.databases = [ name ];
|
|
|
|
|
|
|
|
# MTA-STS server
|
|
|
|
services.nginx.enable = true;
|
|
|
|
services.nginx.virtualHosts.maddy-mta-sts = {
|
|
|
|
listen = [{ addr = "127.0.0.1"; port = mtaStsPort; }];
|
|
|
|
root = mtaStsDir;
|
|
|
|
};
|
|
|
|
|
|
|
|
# Firewall
|
|
|
|
networking.firewall.allowedTCPPorts = [ 25 ];
|
|
|
|
|
|
|
|
# traefik
|
|
|
|
cloud.traefik.hosts.maddy-smtp = {
|
|
|
|
protocol = "tcp";
|
|
|
|
port = smtpPort;
|
|
|
|
host = "mx1.nkagami.me";
|
|
|
|
tlsPassthrough = false;
|
|
|
|
entrypoints = [ "smtp-submission" ];
|
|
|
|
};
|
|
|
|
cloud.traefik.hosts.maddy-smtps = {
|
|
|
|
protocol = "tcp";
|
|
|
|
port = smtpsPort;
|
|
|
|
host = "mx1.nkagami.me";
|
|
|
|
entrypoints = [ "smtp-submission-ssl" ];
|
|
|
|
};
|
|
|
|
cloud.traefik.hosts.maddy-imap = {
|
|
|
|
protocol = "tcp";
|
|
|
|
port = imapPort;
|
|
|
|
host = "mx1.nkagami.me";
|
|
|
|
entrypoints = [ "imap" ];
|
|
|
|
};
|
|
|
|
cloud.traefik.hosts.maddy-mta-sts = {
|
|
|
|
port = mtaStsPort;
|
|
|
|
host = "mta-sts.nkagami.me";
|
|
|
|
};
|
|
|
|
|
|
|
|
# maddy itself
|
|
|
|
systemd.services."${name}" = {
|
|
|
|
after = [ "network.target" "traefik-certs-dumper.service" ];
|
|
|
|
wantedBy = [ "multi-user.target" ];
|
|
|
|
|
|
|
|
description = "maddy mail server";
|
|
|
|
documentation = [
|
|
|
|
"man:maddy(1)"
|
|
|
|
"man:maddy.conf(5)"
|
|
|
|
"https://maddy.email"
|
|
|
|
];
|
|
|
|
|
|
|
|
serviceConfig = {
|
|
|
|
Type = "notify";
|
2021-12-01 19:38:53 +00:00
|
|
|
NotifyAccess = "exec";
|
2021-11-01 19:50:30 +00:00
|
|
|
|
|
|
|
User = name;
|
|
|
|
Group = name;
|
|
|
|
|
|
|
|
WorkingDirectory = "/var/lib/${name}";
|
|
|
|
|
|
|
|
ConfigurationDirectory = name;
|
|
|
|
RuntimeDirectory = name;
|
|
|
|
StateDirectory = name;
|
|
|
|
LogsDirectory = name;
|
|
|
|
ReadOnlyPaths = "/usr/lib/${name} ${cfg.tls.keyFile} ${cfg.tls.certFile}";
|
|
|
|
ReadWritePaths = "/var/lib/${name}";
|
|
|
|
|
|
|
|
# Strict sandboxing. You have no reason to trust code written by strangers from GitHub.
|
|
|
|
PrivateTmp = true;
|
|
|
|
ProtectHome = true;
|
|
|
|
ProtectSystem = "strict";
|
|
|
|
ProtectKernelTunables = true;
|
|
|
|
ProtectHostname = true;
|
|
|
|
ProtectClock = true;
|
|
|
|
ProtectControlGroups = true;
|
|
|
|
RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
|
|
|
|
|
|
|
|
# Additional sandboxing. You need to disable all of these options
|
|
|
|
# for privileged helper binaries (for system auth) to work correctly.
|
|
|
|
NoNewPrivileges = true;
|
|
|
|
PrivateDevices = true;
|
|
|
|
DeviceAllow = "/dev/syslog";
|
|
|
|
RestrictSUIDSGID = true;
|
|
|
|
ProtectKernelModules = true;
|
|
|
|
MemoryDenyWriteExecute = true;
|
|
|
|
RestrictNamespaces = true;
|
|
|
|
RestrictRealtime = true;
|
|
|
|
LockPersonality = true;
|
|
|
|
|
|
|
|
# Graceful shutdown with a reasonable timeout.
|
|
|
|
TimeoutStopSec = "7s";
|
|
|
|
KillMode = "mixed";
|
|
|
|
KillSignal = "SIGTERM";
|
|
|
|
|
|
|
|
|
|
|
|
# Required to bind on ports lower than 1024.
|
|
|
|
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
|
|
|
|
CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
|
|
|
|
|
|
|
|
# Bump FD limitations. Even idle mail server can have a lot of FDs open (think
|
|
|
|
# of idle IMAP connections, especially ones abandoned on the other end and
|
|
|
|
# slowly timing out).
|
|
|
|
LimitNOFILE = 131072;
|
|
|
|
|
|
|
|
# Limit processes count to something reasonable to
|
|
|
|
# prevent resources exhausting due to big amounts of helper
|
|
|
|
# processes launched.
|
|
|
|
LimitNPROC = 512;
|
|
|
|
|
|
|
|
# Restart server on any problem.
|
|
|
|
Restart = "on-failure";
|
|
|
|
# ... Unless it is a configuration problem.
|
|
|
|
RestartPreventExitStatus = 2;
|
2021-12-01 19:38:53 +00:00
|
|
|
|
|
|
|
ExecStart = "${cfg.package}/bin/maddy ${if cfg.debug then "-debug " else ""}-config ${configFile}";
|
2021-11-01 19:50:30 +00:00
|
|
|
};
|
2021-12-01 19:38:53 +00:00
|
|
|
reload = ''
|
2022-05-31 16:45:33 +00:00
|
|
|
/bin/kill -USR1 $MAINPID
|
|
|
|
/bin/kill -USR2 $MAINPID
|
2021-12-01 19:38:53 +00:00
|
|
|
'';
|
2021-11-01 19:50:30 +00:00
|
|
|
};
|
|
|
|
};
|
|
|
|
}
|