mail-server/mail-server.nix

566 lines
18 KiB
Nix
Raw Normal View History

2023-09-23 15:20:13 -07:00
{ config, lib, pkgs, ... }@toplevel:
2023-09-17 09:57:55 -07:00
2023-09-17 23:18:07 -07:00
with lib;
2023-09-23 16:12:05 -07:00
let
cfg = config.fudo.mail;
hostname = config.instance.hostname;
2023-09-23 16:13:57 -07:00
hostSecrets = config.fudo.secrets.host-secrets."${hostname}";
2023-09-23 16:20:37 -07:00
metricsPort = 5034;
2023-10-13 13:25:57 -07:00
dovecotAdminPasswd =
pkgs.lib.passwd.stablerandom-passwd-file "dovecot-admin-passwd"
config.instance.build-seed;
dovecotApiKey = pkgs.lib.passwd.stablerandom-passwd-file "dovecot-api-key"
config.instance.build-seed;
2023-09-23 15:20:13 -07:00
redisPasswdFile =
pkgs.lib.passwd.stablerandom-passwd-file "mail-server-redis-passwd"
config.instance.build-seed;
2023-09-17 23:18:07 -07:00
in {
2023-09-17 09:57:55 -07:00
options.fudo.mail = with types; {
enable = mkEnableOption "Enable mail server.";
2023-09-24 11:24:09 -07:00
debug = mkEnableOption "Enable verbose logging.";
2023-09-17 09:57:55 -07:00
state-directory = mkOption {
type = str;
description = "Directory at which to store server state.";
};
mail-user = mkOption {
type = str;
description = "User as which to store mail.";
default = "fudo-mail";
};
mail-group = mkOption {
type = str;
description = "Group as which to store mail.";
default = "fudo-mail";
};
2023-09-17 09:57:55 -07:00
primary-domain = mkOption {
type = str;
description = "Primary domain name served by this server.";
};
extra-domains = mkOption {
type = listOf str;
description = "List of additional domains served by this server.";
default = [ ];
};
2023-09-25 09:23:49 -07:00
message-size-limit = mkOption {
type = int;
description = "Max allowed size of messages, in megabytes.";
default = 100;
};
2023-09-25 09:26:24 -07:00
sasl-domain = mkOption {
type = str;
description = "SASL domain to use for authentication.";
};
2023-09-25 09:16:40 -07:00
blacklist = {
senders = mkOption {
type = listOf str;
description =
"List of email addresses for which we will never send email.";
default = [ ];
};
recipients = mkOption {
type = listOf str;
description =
"List of email addresses for which we will not accept email.";
default = [ ];
};
dns = mkOption {
type = listOf str;
description = "List of DNS spam blacklists to use.";
default = [ ];
};
};
2023-09-24 23:26:41 -07:00
aliases = {
user-aliases = mkOption {
type = attrsOf (listOf str);
description =
"Map of username to list of aliases mapping to that user.";
default = { };
};
alias-users = mkOption {
type = attrsOf (listOf str);
description =
2023-10-07 18:26:26 -07:00
"Map of alias user to list of users who should receive email.";
2023-09-24 23:26:41 -07:00
default = { };
};
};
2023-09-24 23:13:58 -07:00
metrics-port = mkOption {
2023-09-24 14:33:31 -07:00
type = port;
description = "Port on which to serve metrics.";
default = metricsPort;
};
2023-09-24 23:13:58 -07:00
trusted-networks = mkOption {
type = listOf str;
description = "List of networks to be considered trusted.";
default = [ ];
};
2023-09-23 15:20:13 -07:00
ldap = {
authentik-host = mkOption {
type = str;
description = "Hostname of the LDAP outpost provider.";
default = "authentik.${toplevel.config.fudo.mail.primary-domain}";
};
outpost-token = mkOption {
type = str;
description = "Token with which to authenticate to the Authentik host.";
};
2023-09-29 14:51:54 -07:00
bind-dn = mkOption {
type = str;
description = "DN as which to bind with the LDAP server.";
};
2023-09-23 15:20:13 -07:00
2023-09-29 14:51:54 -07:00
bind-password-file = mkOption {
type = str;
description =
"File containing password with which to bind with the LDAP server.";
};
2023-09-23 15:20:13 -07:00
base = mkOption {
type = str;
description = "Base of the LDAP server.";
example = "dc=fudo,dc=org";
};
2023-10-01 14:59:27 -07:00
user-ou = mkOption {
2023-09-23 15:20:13 -07:00
type = str;
description = "Organizational unit containing users.";
2023-10-01 14:59:27 -07:00
default = "ou=users";
};
group-ou = mkOption {
type = str;
description = "Organizational unit containing users.";
default = "ou=groups";
2023-09-23 15:20:13 -07:00
};
};
2023-10-10 18:04:56 -07:00
images = {
ldap-proxy = mkOption {
type = str;
description = "Docker image to use for LDAP proxy.";
default = "ghcr.io/goauthentik/ldap";
};
2023-09-23 15:20:13 -07:00
};
2023-09-17 09:57:55 -07:00
smtp = {
hostname = mkOption {
type = str;
description =
"Hostname too use for the SMTP server. Must resolve to this host.";
default = "smtp.${config.fudo.mail.primary-domain}";
};
2023-09-23 15:20:13 -07:00
ssl-directory = mkOption {
type = str;
description =
"Directory containing SSL certificates for SMTP hostname.";
2023-09-17 09:57:55 -07:00
};
};
imap = {
hostname = mkOption {
type = str;
description =
"Hostname too use for the IMAP server. Must resolve to this host.";
default = "imap.${config.fudo.mail.primary-domain}";
};
2023-09-23 15:20:13 -07:00
ssl-directory = mkOption {
type = str;
description =
"Directory containing SSL certificates for IMAP hostname.";
2023-09-17 09:57:55 -07:00
};
2023-10-13 13:29:15 -07:00
api-port = mkOption {
type = nullOr port;
description = "Port to open for Dovecot HTTP admin API.";
default = null;
};
2023-09-17 09:57:55 -07:00
};
};
2023-09-17 23:24:13 -07:00
config = mkIf cfg.enable {
2023-09-26 23:11:04 -07:00
services = {
nginx = {
2024-01-30 14:21:27 -08:00
virtualHosts =
let mailHostnames = unique [ cfg.smtp.hostname cfg.imap.hostname ];
in genAttrs mailHostnames (hostname: {
2024-01-30 14:37:36 -08:00
locations."/metrics" = {
2024-01-30 14:21:27 -08:00
proxyPass = "http://localhost:${toString metricsPort}/metrics";
};
});
2023-09-17 09:57:55 -07:00
};
};
2023-09-21 18:01:44 -07:00
fudo.secrets.host-secrets."${hostname}" = {
2023-09-23 15:20:13 -07:00
mailLdapProxyEnv = {
source-file = pkgs.writeText "ldap-proxy.env" ''
AUTHENTIK_HOST=${cfg.ldap.authentik-host}
AUTHENTIK_TOKEN=${cfg.ldap.outpost-token}
AUTHENTIK_INSECURE=false
'';
target-file = "/run/mail-server/ldap-proxy/env";
2023-09-23 15:20:13 -07:00
};
2023-09-21 18:01:44 -07:00
dovecotLdapConfig = {
source-file = pkgs.writeText "dovecot-ldap.conf"
(concatStringsSep "\n" [
2023-09-23 15:20:13 -07:00
"uris = ldap://ldap-proxy:3389"
2023-09-21 18:01:44 -07:00
"ldap_version = 3"
2023-09-29 14:51:54 -07:00
"dn = ${cfg.ldap.bind-dn}"
"dnpass = ${readFile cfg.ldap.bind-password-file}"
2023-09-21 18:01:44 -07:00
"auth_bind = yes"
2023-10-01 15:01:53 -07:00
"auth_bind_userdn = cn=%n,${cfg.ldap.user-ou},${cfg.ldap.base}"
2023-09-21 18:01:44 -07:00
"base = ${cfg.ldap.base}"
"user_filter = (&(objectClass=organizationalPerson)(cn=%n))"
"pass_filter = (&(objectClass=organizationalPerson)(cn=%n))"
2023-10-01 08:45:31 -07:00
"pass_attrs = =user=%{ldap:cn}"
"user_attrs = =user=%{ldap:cn}"
2023-09-21 18:01:44 -07:00
]);
target-file = "/run/mail-server/dovecot-secrets/ldap.conf";
2023-09-21 18:01:44 -07:00
};
2023-10-13 13:25:57 -07:00
dovecotAdminConfig = {
source-file = pkgs.writeText "dovecot-admin.conf" (concatStringsSep "\n"
2023-10-13 13:33:13 -07:00
([ "doveadm_password = ${readFile dovecotAdminPasswd}" ]
++ (optional (cfg.imap.api-port != null)
"doveadm_api_key = ${readFile dovecotApiKey}")));
target-file = "/run/mail-server/dovecot-secrets/admin.conf";
};
redisPasswd = {
source-file = redisPasswdFile;
target-file = "/run/mail-server/redis/passwd";
2023-10-13 13:25:57 -07:00
};
2023-09-21 18:01:44 -07:00
};
systemd.tmpfiles.rules = [
"d ${cfg.state-directory}/dovecot 0700 - - - -"
"d ${cfg.state-directory}/dovecot-dhparams 0700 - - - -"
"d ${cfg.state-directory}/antivirus 0700 - - - -"
"d ${cfg.state-directory}/dkim 0700 - - - -"
2023-09-30 18:54:17 -07:00
"d ${cfg.state-directory}/mail 0700 - - - -"
2023-09-21 18:01:44 -07:00
];
2023-09-17 09:57:55 -07:00
virtualisation.arion.projects.mail-server.settings = let
2023-10-16 11:54:32 -07:00
2023-09-17 09:57:55 -07:00
image = { pkgs, ... }: {
2023-09-26 13:30:01 -07:00
project.name = "mail-server";
2023-09-27 11:39:05 -07:00
networks = {
2024-01-12 11:24:28 -08:00
external_network.internal = false;
internal_network.internal = true;
redis_network.internal = true;
ldap_network.internal = true;
2023-09-27 11:39:05 -07:00
};
2023-09-23 17:14:30 -07:00
services = let
2023-09-17 09:57:55 -07:00
antivirusPort = 15407;
antispamPort = 11335;
2023-09-23 17:09:58 -07:00
antispamControllerPort = 11336;
2023-09-17 09:57:55 -07:00
lmtpPort = 24;
authPort = 5447;
userdbPort = 5448;
2023-09-24 10:43:11 -07:00
dkimPort = 5734;
redisPort = 6379;
2023-09-21 18:01:44 -07:00
2023-09-17 09:57:55 -07:00
in {
smtp = {
2023-09-23 17:18:40 -07:00
service = {
networks = [
"internal_network"
2023-10-02 12:17:42 -07:00
# Needs access to internet to forward emails & lookup hosts
"external_network"
2023-10-02 12:17:42 -07:00
# For auth lookups
"ldap_network"
];
2023-09-23 17:18:40 -07:00
volumes = [
"${hostSecrets.dovecotLdapConfig.target-file}:/run/dovecot2/conf.d/ldap.conf:ro"
"${cfg.smtp.ssl-directory}:/run/certs/smtp"
];
2023-09-27 13:04:51 -07:00
ports = [ "25:25" "587:587" "465:465" ];
2023-09-25 14:20:06 -07:00
depends_on = [ "imap" "ldap-proxy" ];
2023-09-23 17:18:40 -07:00
};
2023-09-17 09:57:55 -07:00
nixos = {
useSystemd = true;
2023-09-24 10:12:42 -07:00
configuration = {
imports = [ ./dovecot.nix ./postfix.nix ];
boot.tmp.useTmpfs = true;
2023-09-24 10:12:42 -07:00
system.nssModules = lib.mkForce [ ];
2023-09-21 18:01:44 -07:00
2024-01-12 11:58:27 -08:00
networking.firewall.enable = false;
2023-09-24 10:12:42 -07:00
fudo.mail.postfix = {
enable = true;
debug = cfg.debug;
domain = cfg.primary-domain;
local-domains = cfg.extra-domains;
hostname = cfg.smtp.hostname;
2023-10-14 23:30:06 -07:00
trusted-networks = let
2023-10-14 23:53:20 -07:00
isIpv6 = net: !isNull (builtins.match ".+:.+" net);
2023-10-14 23:30:06 -07:00
addIpv6Escape = net:
let components = builtins.split "/" net;
2023-10-15 16:34:35 -07:00
in "[${elemAt components 0}]/${elemAt components 2}";
2023-10-14 23:30:06 -07:00
escapeIpv6 = net:
if isIpv6 net then addIpv6Escape net else net;
in map escapeIpv6 cfg.trusted-networks;
2023-09-24 10:12:42 -07:00
blacklist = {
senders = cfg.blacklist.senders;
recipients = cfg.blacklist.recipients;
dns = cfg.blacklist.dns;
2023-09-17 09:57:55 -07:00
};
2023-09-24 10:12:42 -07:00
aliases = {
2023-09-24 23:26:41 -07:00
user-aliases = cfg.aliases.user-aliases;
alias-users = cfg.aliases.alias-users;
2023-09-24 10:12:42 -07:00
};
ssl = {
certificate =
"/run/certs/smtp/fullchain.pem"; # FIXME: or just cert?
private-key = "/run/certs/smtp/key.pem";
};
sasl-domain = cfg.sasl-domain;
message-size-limit = cfg.message-size-limit;
ports = { metrics = metricsPort; };
rspamd-server = {
host = "antispam";
port = antispamPort;
};
lmtp-server = {
host = "imap";
port = lmtpPort;
};
dkim-server = {
host = "dkim";
port = dkimPort;
};
ldap-conf = "/run/dovecot2/conf.d/ldap.conf";
};
};
2023-09-17 09:57:55 -07:00
};
};
imap = {
2023-09-23 17:18:40 -07:00
service = {
2023-10-10 17:43:39 -07:00
networks = [
"internal_network"
"external_network"
2023-10-10 20:36:58 -07:00
# For authentication
2023-10-10 17:43:39 -07:00
"ldap_network"
];
2023-09-27 13:04:51 -07:00
ports = [ "143:143" "993:993" ];
2023-09-23 17:18:40 -07:00
volumes = [
"${cfg.state-directory}/dovecot:/state"
"${hostSecrets.dovecotLdapConfig.target-file}:/run/dovecot2/conf.d/ldap.conf:ro"
2023-10-13 13:25:57 -07:00
"${hostSecrets.dovecotAdminConfig.target-file}:/run/dovecot2/conf.d/admin.conf:ro"
2023-10-02 10:56:39 -07:00
"${cfg.imap.ssl-directory}:/run/certs/imap:ro"
"${cfg.state-directory}/dovecot-dhparams:/var/lib/dhparams"
2023-09-30 18:54:17 -07:00
"${cfg.state-directory}/mail:/mail"
2023-09-23 17:18:40 -07:00
];
2023-10-13 16:20:20 -07:00
depends_on = [ "antispam" "ldap-proxy" ];
2023-09-23 17:18:40 -07:00
};
2023-09-17 09:57:55 -07:00
nixos = {
useSystemd = true;
2023-09-24 10:12:42 -07:00
configuration = {
imports = [ ./dovecot.nix ];
boot.tmp.useTmpfs = true;
2023-09-24 10:12:42 -07:00
system.nssModules = lib.mkForce [ ];
2024-01-12 11:58:27 -08:00
networking.firewall.enable = false;
2023-09-24 10:12:42 -07:00
fudo.mail.dovecot = {
enable = true;
debug = cfg.debug;
state-directory = "/state";
2023-09-30 18:54:17 -07:00
mail-directory = "/mail";
2023-09-24 10:12:42 -07:00
ports = {
lmtp = lmtpPort;
auth = authPort;
userdb = userdbPort;
metrics = metricsPort;
2023-09-17 09:57:55 -07:00
};
2023-09-24 10:12:42 -07:00
mail-user = cfg.mail-user;
mail-group = cfg.mail-group;
ssl = {
certificate = "/run/certs/imap/fullchain.pem";
private-key = "/run/certs/imap/key.pem";
};
rspamd = {
host = "antispam";
port = antispamPort;
};
ldap-conf = "/run/dovecot2/conf.d/ldap.conf";
2023-10-13 13:25:57 -07:00
admin-conf = "/run/dovecot2/conf.d/admin.conf";
2023-09-24 10:12:42 -07:00
};
};
2023-09-17 09:57:55 -07:00
};
};
2023-09-24 14:23:03 -07:00
ldap-proxy.service = {
2023-09-17 09:57:55 -07:00
image = cfg.images.ldap-proxy;
restart = "always";
networks = [
2023-10-02 12:17:42 -07:00
"ldap_network"
# Needs access to external network to talk to Authentik
"external_network"
];
2023-09-24 14:27:47 -07:00
env_file = [ hostSecrets.mailLdapProxyEnv.target-file ];
2023-09-17 09:57:55 -07:00
};
antispam = {
2023-09-23 17:18:40 -07:00
service = {
networks = [
"internal_network"
# Needs external access for blacklist checks
"external_network"
"redis_network"
];
capabilities.SYS_ADMIN = true;
depends_on = [ "antivirus" "redis" ];
2023-09-23 17:18:40 -07:00
};
2023-09-17 09:57:55 -07:00
nixos = {
useSystemd = true;
2023-09-24 10:12:42 -07:00
configuration = {
imports = [ ./rspamd.nix ];
boot.tmp.useTmpfs = true;
2023-09-24 10:12:42 -07:00
system.nssModules = lib.mkForce [ ];
fudo.mail.rspamd = {
enable = true;
ports = {
milter = antispamPort;
controller = antispamControllerPort;
metrics = metricsPort;
2023-09-17 09:57:55 -07:00
};
2023-09-24 10:12:42 -07:00
antivirus = {
host = "antivirus";
port = antivirusPort;
};
2023-10-16 11:54:32 -07:00
redis.password = readFile redisPasswdFile;
2023-09-24 10:12:42 -07:00
};
};
2023-09-17 09:57:55 -07:00
};
};
antivirus = {
2023-09-23 17:18:40 -07:00
service = {
networks = [
"internal_network"
# Needs external access for database updates
"external_network"
];
2023-09-23 17:18:40 -07:00
volumes = [ "${cfg.state-directory}/antivirus:/state" ];
};
2023-09-17 09:57:55 -07:00
nixos = {
useSystemd = true;
2023-09-24 10:12:42 -07:00
configuration = {
imports = [ ./clamav.nix ];
boot.tmp.useTmpfs = true;
2023-09-24 10:12:42 -07:00
system.nssModules = lib.mkForce [ ];
2023-09-27 17:56:06 -07:00
networking.firewall = {
2023-09-27 18:00:08 -07:00
enable = true;
2023-09-27 17:56:06 -07:00
allowedTCPPorts = [ antivirusPort ];
allowedUDPPorts = [ antivirusPort ];
};
2023-09-24 10:12:42 -07:00
fudo.mail.clamav = {
enable = true;
state-directory = "/state";
2023-09-27 17:56:06 -07:00
port = antivirusPort;
2023-09-24 10:12:42 -07:00
};
};
2023-09-17 09:57:55 -07:00
};
};
dkim = {
2023-09-23 17:18:40 -07:00
service = {
networks = [ "internal_network" ];
2023-09-28 22:34:58 -07:00
volumes = [ "${cfg.state-directory}/dkim:/var/lib/opendkim" ];
2023-09-23 17:18:40 -07:00
};
2023-09-17 09:57:55 -07:00
nixos = {
useSystemd = true;
2023-09-24 10:12:42 -07:00
configuration = {
imports = [ ./dkim.nix ];
boot.tmp.useTmpfs = true;
2023-09-24 10:12:42 -07:00
system.nssModules = lib.mkForce [ ];
fudo.mail.dkim = {
enable = true;
debug = cfg.debug;
2023-09-24 10:49:53 -07:00
port = dkimPort;
state-directory = "/state";
2023-09-24 10:12:42 -07:00
domains = [ cfg.primary-domain ] ++ cfg.extra-domains;
};
};
2023-09-17 09:57:55 -07:00
};
};
redis = {
service = {
volumes = [
"${cfg.state-directory}/redis:/var/lib/redis"
"${hostSecrets.redisPasswd.target-file}:/run/redis/passwd"
];
networks = [ "redis_network" ];
};
nixos = {
useSystemd = true;
configuration = {
networking.firewall.allowedTCPPorts = [ redisPort ];
boot.tmp.useTmpfs = true;
system.nssModules = lib.mkForce [ ];
services.redis.servers."rspamd" = {
enable = true;
2023-10-16 11:54:32 -07:00
bind = null; # null -> all
port = redisPort;
2023-10-16 11:54:32 -07:00
requirePassFile = "/run/redis/passwd";
};
};
};
};
2023-09-17 09:57:55 -07:00
metrics-proxy = {
2023-09-23 17:18:40 -07:00
service = {
networks = [ "internal_network" ];
2023-09-24 23:13:58 -07:00
ports = [ "${toString cfg.metrics-port}:80" ];
depends_on = [ "smtp" "imap" "antispam" ];
2023-09-23 17:18:40 -07:00
};
2023-09-17 09:57:55 -07:00
nixos = {
useSystemd = true;
configuration = {
boot.tmp.useTmpfs = true;
2023-09-17 09:57:55 -07:00
system.nssModules = lib.mkForce [ ];
services.nginx = {
enable = true;
recommendedProxySettings = true;
recommendedGzipSettings = true;
recommendedOptimisation = true;
virtualHosts.localhost = {
default = true;
locations = {
"/postfix" = {
2023-09-23 16:23:49 -07:00
proxyPass = "http://smtp:${toString metricsPort}/";
2023-09-17 09:57:55 -07:00
};
"/dovecot" = {
2023-09-23 16:23:49 -07:00
proxyPass = "http://imap:${toString metricsPort}/";
2023-09-17 09:57:55 -07:00
};
2023-09-25 14:20:06 -07:00
"/rspamd" = {
2023-09-23 16:23:49 -07:00
proxyPass = "http://antispam:${toString metricsPort}/";
2023-09-17 09:57:55 -07:00
};
};
};
};
};
};
};
};
};
in { imports = [ image ]; };
};
}