mailserver: Separate into multiple files

This commit is contained in:
Simon Bruder 2023-05-31 13:11:12 +02:00
parent f84e6d9bee
commit 5b39654159
Signed by: simon
GPG key ID: 8D3C82F9F309F8EC
10 changed files with 630 additions and 585 deletions

View file

@ -30,7 +30,7 @@
./initrd-ssh.nix ./initrd-ssh.nix
./locales.nix ./locales.nix
./logitech.nix ./logitech.nix
./mailserver.nix ./mailserver
./media-mount.nix ./media-mount.nix
./media-proxy.nix ./media-proxy.nix
./mullvad ./mullvad

View file

@ -1,584 +0,0 @@
{ config, lib, pkgs, ... }:
let
cfg = config.sbruder.mailserver;
certDir = config.security.acme.certs."${cfg.fqdn}".directory;
in
{
options.sbruder.mailserver = with lib; with lib.types; {
enable = mkEnableOption "simple mail server";
fqdn = mkOption {
type = str;
description = ''
FQDN of the mail server
It needs to have a matching reverse DNS record. Also, an acme
certificate with this name has to be present.
'';
example = "mail.example.com";
};
storage = mkOption {
type = path;
description = "Location of the storage for mails";
default = "/var/vmail";
};
domains = mkOption {
type = listOf str;
description = "Domains to serve";
example = [ "example.com" "example.org" ];
};
autoconfig = {
enable = mkEnableOption "autoconfiguration of compatible clients. Requires autoconfig.<domain> to exist for all specified domains";
};
users = mkOption {
type = listOf (submodule {
options = {
address = mkOption {
type = str;
description = "Primary e-mail address of the user";
example = "jdoe@example.com";
};
passwordHash = mkOption {
type = str;
description = ''
Bcrypt hash of the users password. Please note that it will be
world-readable in the nix store.
You can generate a password with `nix run nixpkgs.apacheHttpd -c
htpasswd -nBC 12 "" | cut -d: -f2`
'';
example = "$2y$05$SHxhwVGx.XCd19HAcb1NKuidUxW1BwU7GeO0ZIcMTc5t2uZoYLVRK";
};
aliases = mkOption {
type = listOf str;
description = ''
A list of aliases for the user.
If multiple users have the same alias defined, mail will be
delivered to both of them.
'';
default = [ ];
example = [
"j.doe@example.com"
"jane.doe@example.com"
"postmaster@example.com"
];
};
};
});
description = "Users of the mail server";
};
cleanHeaders = mkOption {
type = listOf str;
description = "A list of regular expressions that define what headers are filtered";
default = [
"/^\\s*Received:/"
"/^\\s*User-Agent:/"
"/^\\s*X-Mailer:/"
"/^\\s*X-Originating-IP:/"
];
};
rejectSenders = mkOption {
type = listOf str;
description = "A list of senders to reject mails from";
default = [ ];
example = [
"newsletter@example.com"
"spammer@example.com"
];
};
spam = {
enable = (lib.mkEnableOption "spam filtering") // { default = true; };
};
dkim = {
enable = (lib.mkEnableOption "DKIM signing") // { default = true; };
selector = lib.mkOption {
type = str;
description = "DKIM Selector to use";
default = "mail";
};
};
};
config = lib.mkIf cfg.enable {
# Users and groups
users.users.vmail = {
uid = 10000;
group = "vmail";
home = cfg.storage;
createHome = true;
};
users.groups.vmail.gid = 10000;
# Firewall
networking.firewall.allowedTCPPorts = [
143 # IMAP
993 # IMAP (implicit TLS)
25 # SMTP
587 # SMTP submission
465 # SMTP submission (implicit TLS)
];
# Service dependencies
systemd.services.dovecot2 = {
wants = [ "acme-finished-${cfg.fqdn}.target" ];
after = [ "acme-finished-${cfg.fqdn}.target" ];
};
systemd.services.postfix = {
wants = [ "acme-finished-${cfg.fqdn}.target" ];
requires = [ "dovecot2.service" ];
after = [ "acme-finished-${cfg.fqdn}.target" "dovecot2.service" ];
};
# Reload on certificate renewal
security.acme.certs."${cfg.fqdn}".postRun = ''
if systemctl is-active dovecot2; then
systemctl --no-block reload dovecot2
fi
if systemctl is-active postfix; then
systemctl --no-block reload postfix
fi
'';
# Postfix
security.dhparams.params.postfix = { };
services.postfix =
let
listToString = lib.concatStringsSep ",";
valiases =
let
# List of attribute sets with single key-value pair
plainAliases = (lib.flatten
(map
({ address, aliases, ... }:
map
(alias: { "${alias}" = address; })
(aliases ++ lib.singleton address))
cfg.users));
# Attribute set with every alias mapped to a list of receivers
mergedAliases = (lib.attrsets.foldAttrs
(val: col: lib.singleton val ++ col)
[ ]
plainAliases);
# Contents of the aliases file
aliasesString = (lib.concatStringsSep
"\n"
(lib.mapAttrsToList
(alias: addresses: "${alias} ${listToString addresses}")
mergedAliases));
in
pkgs.writeText
"valiases"
aliasesString;
access_sender = pkgs.writeText
"access_sender"
(lib.concatMapStringsSep
"\n"
(sender: "${sender} REJECT")
cfg.rejectSenders);
submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules"
(lib.concatMapStringsSep
"\n"
(regex: "${regex} IGNORE")
cfg.cleanHeaders);
in
{
enable = true;
enableSubmission = true; # plain/STARTTLS (latter is forced in submissionOptions)
enableSubmissions = true; # submission with implicit TLS (TCP/465)
hostname = cfg.fqdn;
networksStyle = "host";
sslCert = "${certDir}/fullchain.pem";
sslKey = "${certDir}/key.pem";
recipientDelimiter = "+";
mapFiles = {
inherit access_sender valiases;
};
config = {
# General
smtpd_banner = "${cfg.fqdn} ESMTP NO UCE";
disable_vrfy_command = true; # disable check if mailbox exists
enable_long_queue_ids = true; # better for debugging
strict_rfc821_envelopes = true; # only accept properly formatted envelope
message_size_limit = "50331648"; # 48MiB
virtual_mailbox_domains = listToString cfg.domains;
virtual_mailbox_maps = "hash:/var/lib/postfix/conf/valiases";
virtual_alias_maps = "hash:/var/lib/postfix/conf/valiases";
virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
smtpd_recipient_restrictions = listToString [
"reject_non_fqdn_recipient"
"reject_rbl_client ix.dnsbl.manitu.net"
"reject_unknown_recipient_domain"
"reject_unverified_recipient"
];
smtpd_client_restrictions = listToString [
"reject_rbl_client ix.dnsbl.manitu.net"
"reject_unknown_client_hostname"
];
smtpd_sender_restrictions = listToString [
"check_sender_access hash:/var/lib/postfix/conf/access_sender"
"reject_non_fqdn_sender"
"reject_unknown_sender_domain"
];
# generated 2021-02-04, Mozilla Guideline v5.6, Postfix 3.5.6, OpenSSL 1.1.1i, intermediate configuration
# https://ssl-config.mozilla.org/#server=postfix&version=3.5.6&config=intermediate&openssl=1.1.1i&guideline=5.6
smtpd_tls_security_level = "may";
smtpd_tls_auth_only = "yes";
smtpd_tls_mandatory_protocols = "!SSLv2, !SSLv3, !TLSv1, !TLSv1.1";
smtpd_tls_protocols = "!SSLv2, !SSLv3, !TLSv1, !TLSv1.1";
smtpd_tls_mandatory_ciphers = "medium";
smtpd_tls_loglevel = "1";
tls_medium_cipherlist = listToString [
"ECDHE-ECDSA-AES128-GCM-SHA256"
"ECDHE-RSA-AES128-GCM-SHA256"
"ECDHE-ECDSA-AES256-GCM-SHA384"
"ECDHE-RSA-AES256-GCM-SHA384"
"ECDHE-ECDSA-CHACHA20-POLY1305"
"ECDHE-RSA-CHACHA20-POLY1305"
"DHE-RSA-AES128-GCM-SHA256"
"DHE-RSA-AES256-GCM-SHA384"
];
tls_preempt_cipherlist = "no";
smtpd_tls_dh1024_param_file = config.security.dhparams.params.postfix.path;
smtpd_milters = lib.mkIf cfg.dkim.enable (lib.singleton "unix:/run/opendkim/opendkim.sock");
non_smtpd_milters = lib.mkIf cfg.dkim.enable (lib.singleton "unix:/run/opendkim/opendkim.sock");
};
# plain/STARTTLS (forced with smtpd_tls_security_level)
submissionOptions = {
smtpd_tls_security_level = "encrypt";
smtpd_sasl_auth_enable = "yes";
smtpd_sasl_type = "dovecot";
smtpd_sasl_path = "/run/dovecot2/auth";
smtpd_sender_login_maps = "hash:/etc/postfix/valiases";
smtpd_recipient_restrictions = listToString [ ];
smtpd_client_restrictions = listToString [
"permit_sasl_authenticated"
"reject"
];
smtpd_sender_restrictions = listToString [
"reject_sender_login_mismatch"
];
cleanup_service_name = "submission-header-cleanup";
};
# implicit TLS
submissionsOptions = config.services.postfix.submissionOptions;
masterConfig = {
submission-header-cleanup = {
private = false;
maxproc = 0;
command = "cleanup";
args = [ "-o" "header_checks=pcre:${submissionHeaderCleanupRules}" ];
};
};
};
# Dovecot
services.dovecot2 =
let
postfixCfg = config.services.postfix;
passdb = pkgs.writeText "dovecot-users"
(lib.concatMapStringsSep
"\n"
({ address, passwordHash, ... }: "${address}:{BLF-CRYPT}${passwordHash}")
cfg.users);
in
{
enable = true;
modules = with pkgs; [ dovecot_pigeonhole ];
enableLmtp = true;
enablePAM = false;
mailUser = "vmail";
mailGroup = "vmail";
mailLocation = "maildir:${cfg.storage}/%d/%n";
sslServerCert = "${certDir}/fullchain.pem";
sslServerKey = "${certDir}/key.pem";
mailboxes = {
Archive = { specialUse = "Archive"; auto = "subscribe"; };
Sent = { specialUse = "Sent"; auto = "subscribe"; };
Drafts = { specialUse = "Drafts"; auto = "subscribe"; };
Trash = { specialUse = "Trash"; auto = "subscribe"; };
Spam = { specialUse = "Junk"; auto = "subscribe"; };
};
sieveScripts = {
before = pkgs.writeText "spam.sieve" ''
require "fileinto";
if header :is "X-Spam" "Yes" {
fileinto "Spam";
}
'';
};
extraConfig = ''
# generated 2021-02-04, Mozilla Guideline v5.6, Dovecot 2.3.13, OpenSSL 1.1.1i, intermediate configuration
# https://ssl-config.mozilla.org/#server=dovecot&version=2.3.13&config=intermediate&openssl=1.1.1i&guideline=5.6
ssl = required
ssl_min_protocol = TLSv1.2
ssl_cipher_list = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
ssl_prefer_server_ciphers = no
protocol imap {
mail_plugins = $mail_plugins imap_sieve
}
protocol lmtp {
mail_plugins = $mail_plugins sieve
}
service imap-login {
inet_listener imap {
}
}
service lmtp {
unix_listener dovecot-lmtp {
mode = 0600
user = ${postfixCfg.user}
group = ${postfixCfg.group}
}
}
passdb {
driver = passwd-file
args = username_format=%u ${passdb}
}
userdb {
driver = static
args = uid=vmail gid=vmail home=${cfg.storage}/%d/%n
}
service auth {
unix_listener auth {
mode = 0660
user = ${postfixCfg.user}
group = ${postfixCfg.group}
}
}
lda_mailbox_autosubscribe = yes
lda_mailbox_autocreate = yes
plugin {
sieve_plugins = sieve_imapsieve sieve_extprograms
${lib.optionalString cfg.spam.enable ''
imapsieve_mailbox1_name = Spam
imapsieve_mailbox1_causes = COPY
imapsieve_mailbox1_before = file:/var/lib/dovecot/sieve/learn-spam.sieve
imapsieve_mailbox2_name = *
imapsieve_mailbox2_from = Spam
imapsieve_mailbox2_causes = COPY
imapsieve_mailbox2_before = file:/var/lib/dovecot/sieve/learn-ham.sieve
sieve_pipe_bin_dir = ${pkgs.symlinkJoin { name = "sieve-pipe-bin-dir"; paths = with pkgs; [ rspamd ]; } }/bin
''}
sieve_global_extensions = +vnd.dovecot.pipe
}
'';
};
systemd.services.dovecot2.preStart = lib.mkIf cfg.spam.enable
(lib.mkAfter
(lib.concatStrings
(lib.mapAttrsToList
(name: content: ''
cp ${pkgs.writeText name content} /var/lib/dovecot/sieve/${name}
'')
{
"learn-spam.sieve" = ''
require ["vnd.dovecot.pipe", "copy", "imapsieve"];
pipe :copy "rspamc" ["learn_spam"];
'';
"learn-ham.sieve" = ''
require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
if environment :matches "imap.mailbox" "*" {
set "mailbox" "''${1}";
}
if string "''${mailbox}" "Trash" {
stop;
}
pipe :copy "rspamc" ["learn_ham"];
'';
})));
# DNS (recursor for DNSBLs etc.)
services.resolved.enable = false;
services.unbound = {
enable = true;
settings = {
server = {
interface = [ "127.0.0.53" ];
};
};
resolveLocalQueries = false; # does not work with .53
};
networking.resolvconf.extraConfig = ''
name_servers='127.0.0.53'
'';
# rspamd
sops.secrets.rspamd-worker-controller = lib.mkIf cfg.spam.enable {
owner = config.users.users.rspamd.name;
sopsFile = ../machines + "/${config.networking.hostName}/secrets.yaml";
};
services.rspamd = {
enable = cfg.spam.enable;
postfix.enable = true;
workers = {
normal = {
includes = [ "$CONFDIR/worker-normal.inc" ];
bindSockets = lib.singleton {
socket = "/run/rspamd/rspamd.sock";
mode = "0660";
owner = "${config.services.rspamd.user}";
group = "${config.services.rspamd.group}";
};
};
controller = {
includes = [ "$CONFDIR/worker-controller.inc" ];
bindSockets = [ "127.0.0.1:11334" ] ++ lib.optional config.sbruder.wireguard.home.enable "${config.sbruder.wireguard.home.address}:11334";
};
};
locals = {
"dkim_signing.conf".text = ''
enabled = false;
'';
"logging.inc".text = ''
# starts at info, drops to notice once started up
level = "silent";
'';
"milter_headers.conf".text = ''
extended_spam_headers = true;
'';
"multimap.conf".text = ''
SENDER_DOMAIN_BLOCKED {
type = "from";
filter = "email:domain:tld";
map = "/var/lib/rspamd/blocked_sender_domains.map";
symbol = "SENDER_DOMAIN_BLOCKED";
description = "Senders effective second level domain is manually blocked";
score = 8.0;
}
'';
"redis.conf".text = ''
servers = "127.0.0.1:${toString config.services.redis.servers.rspamd.port}"
'';
"worker-controller.inc".source = config.sops.secrets.rspamd-worker-controller.path; # includes password
};
};
services.redis = lib.mkIf cfg.spam.enable {
vmOverCommit = true;
servers.rspamd = {
enable = true;
port = 6379;
};
};
# DKIM
services.opendkim = lib.mkIf cfg.dkim.enable {
enable = true;
selector = cfg.dkim.selector;
domains = "csl:${lib.concatStringsSep "," cfg.domains}";
configFile = pkgs.writeText "opendkim.conf" ''
UMask 0002
'';
};
systemd.services.opendkim = lib.mkIf cfg.dkim.enable {
# changed to use larger key size
preStart =
let
inherit (config.services.opendkim) keyPath selector;
in
lib.mkForce ''
cd "${keyPath}"
if ! test -f ${selector}.private; then
${pkgs.opendkim}/bin/opendkim-genkey \
-s ${selector} \
-d all-domains-generic-key \
-b 4096
echo "Generated OpenDKIM key! Please update your DNS settings:\n"
echo "-------------------------------------------------------------"
cat ${selector}.txt
echo "-------------------------------------------------------------"
fi
'';
};
users.users.postfix.extraGroups = lib.mkIf cfg.dkim.enable (lib.singleton config.users.users.opendkim.group);
# Autoconfig
services.nginx = lib.mkIf cfg.autoconfig.enable {
enable = true;
virtualHosts = lib.listToAttrs (map
(domain: lib.nameValuePair "autoconfig.${domain}" {
enableACME = true;
forceSSL = true;
locations."=/mail/config-v1.1.xml".alias = pkgs.writeText "config-v1.1.xml" ''
<?xml version="1.0" encoding="UTF-8"?>
<clientConfig version="1.1">
<emailProvider id="${lib.escapeXML domain}">
<domain>${lib.escapeXML domain}</domain>
<displayName>${lib.escapeXML domain}</displayName>
<displayShortName>${lib.escapeXML domain}</displayShortName>
<incomingServer type="imap">
<hostname>${lib.escapeXML cfg.fqdn}</hostname>
<port>993</port>
<socketType>SSL</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</incomingServer>
<outgoingServer type="smtp">
<hostname>${lib.escapeXML cfg.fqdn}</hostname>
<port>465</port>
<socketType>SSL</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</outgoingServer>
</emailProvider>
</clientConfig>
'';
})
cfg.domains);
};
};
}

View file

@ -0,0 +1,48 @@
{ config, lib, pkgs, ... }:
let
cfg = config.sbruder.mailserver;
mkConfigFile = domain: pkgs.writeText "config-v1.1.xml" ''
<?xml version="1.0" encoding="UTF-8"?>
<clientConfig version="1.1">
<emailProvider id="${lib.escapeXML domain}">
<domain>${lib.escapeXML domain}</domain>
<displayName>${lib.escapeXML domain}</displayName>
<displayShortName>${lib.escapeXML domain}</displayShortName>
<incomingServer type="imap">
<hostname>${lib.escapeXML cfg.fqdn}</hostname>
<port>993</port>
<socketType>SSL</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</incomingServer>
<outgoingServer type="smtp">
<hostname>${lib.escapeXML cfg.fqdn}</hostname>
<port>465</port>
<socketType>SSL</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</outgoingServer>
</emailProvider>
</clientConfig>
'';
in
{
options.sbruder.mailserver.autoconfig = {
enable = lib.mkEnableOption "autoconfiguration of compatible clients. Requires autoconfig.<domain> to exist for all specified domains";
};
config = lib.mkIf cfg.enable {
services.nginx = lib.mkIf cfg.autoconfig.enable {
enable = true;
virtualHosts = lib.listToAttrs (map
(domain: lib.nameValuePair "autoconfig.${domain}" {
enableACME = true;
forceSSL = true;
locations."=/mail/config-v1.1.xml".alias = mkConfigFile domain;
})
cfg.domains);
};
};
}

View file

@ -0,0 +1,102 @@
{ config, lib, pkgs, ... }:
let
cfg = config.sbruder.mailserver;
in
{
options.sbruder.mailserver = with lib; with lib.types; {
enable = mkEnableOption "simple mail server";
fqdn = mkOption {
type = str;
description = ''
FQDN of the mail server
It needs to have a matching reverse DNS record.
By default, an acme certificate with this name has to be present.
See `certDir` for more details.
'';
example = "mail.example.com";
};
storage = mkOption {
type = path;
description = "Location of the storage for mails";
default = "/var/vmail";
};
domains = mkOption {
type = listOf str;
description = "Domains to serve";
example = [ "example.com" "example.org" ];
};
certDir = mkOption {
type = path;
description = "Directory with `fullchain.pem` and `key.pem` for the FQDN. Defaults to the ACME directory of the FQDN.";
default = config.security.acme.certs."${cfg.fqdn}".directory;
};
users = mkOption {
type = listOf (submodule {
options = {
address = mkOption {
type = str;
description = "Primary e-mail address of the user";
example = "jdoe@example.com";
};
passwordHash = mkOption {
type = str;
description = ''
Bcrypt hash of the users password. Please note that it will be
world-readable in the nix store.
You can generate a password with `nix run nixpkgs.apacheHttpd -c
htpasswd -nBC 12 "" | cut -d: -f2`
'';
example = "$2y$05$SHxhwVGx.XCd19HAcb1NKuidUxW1BwU7GeO0ZIcMTc5t2uZoYLVRK";
};
aliases = mkOption {
type = listOf str;
description = ''
A list of aliases for the user.
If multiple users have the same alias defined, mail will be
delivered to both of them.
'';
default = [ ];
example = [
"j.doe@example.com"
"jane.doe@example.com"
"postmaster@example.com"
];
};
};
});
description = "Users of the mail server";
};
cleanHeaders = mkOption {
type = listOf str;
description = "A list of regular expressions that define what headers are filtered";
default = [
"/^\\s*Received:/"
"/^\\s*User-Agent:/"
"/^\\s*X-Mailer:/"
"/^\\s*X-Originating-IP:/"
];
};
rejectSenders = mkOption {
type = listOf str;
description = "A list of senders to reject mails from";
default = [ ];
example = [
"newsletter@example.com"
"spammer@example.com"
];
};
};
imports = [
./autoconfig.nix
./dkim.nix
./dns.nix
./dovecot.nix
./postfix.nix
./rspamd.nix
./users.nix
];
}

View file

@ -0,0 +1,52 @@
{ config, lib, pkgs, ... }:
let
cfg = config.sbruder.mailserver;
in
{
options.sbruder.mailserver.dkim = {
enable = (lib.mkEnableOption "DKIM signing") // { default = true; };
selector = lib.mkOption {
type = lib.types.str;
description = "DKIM Selector to use";
default = "mail";
};
};
config = lib.mkIf (cfg.enable && cfg.dkim.enable) {
services.opendkim = {
enable = true;
selector = cfg.dkim.selector;
domains = "csl:${lib.concatStringsSep "," cfg.domains}";
configFile = pkgs.writeText "opendkim.conf" ''
UMask 0002
'';
};
systemd.services.opendkim = {
# changed to use larger key size
preStart =
let
inherit (config.services.opendkim) keyPath selector;
in
lib.mkForce ''
cd "${keyPath}"
if ! test -f ${selector}.private; then
${pkgs.opendkim}/bin/opendkim-genkey \
-s ${selector} \
-d all-domains-generic-key \
-b 4096
echo "Generated OpenDKIM key! Please update your DNS settings:\n"
echo "-------------------------------------------------------------"
cat ${selector}.txt
echo "-------------------------------------------------------------"
fi
'';
};
users.users.postfix.extraGroups = lib.mkIf cfg.dkim.enable (lib.singleton config.users.users.opendkim.group);
services.postfix.config = {
smtpd_milters = lib.singleton "unix:/run/opendkim/opendkim.sock";
non_smtpd_milters = lib.singleton "unix:/run/opendkim/opendkim.sock";
};
};
}

View file

@ -0,0 +1,18 @@
{ config, lib, ... }:
lib.mkIf config.sbruder.mailserver.enable {
# Enable recursor for DNSBLs etc.
services.resolved.enable = false;
services.unbound = {
enable = true;
settings = {
server = {
interface = [ "127.0.0.53" ];
};
};
resolveLocalQueries = false; # does not work with .53
};
networking.resolvconf.extraConfig = ''
name_servers='127.0.0.53'
'';
}

View file

@ -0,0 +1,157 @@
{ config, lib, pkgs, ... }:
let
cfg = config.sbruder.mailserver;
postfixCfg = config.services.postfix;
passdb = pkgs.writeText "dovecot-users"
(lib.concatMapStringsSep
"\n"
({ address, passwordHash, ... }: "${address}:{BLF-CRYPT}${passwordHash}")
cfg.users);
in
lib.mkIf cfg.enable {
services.dovecot2 = {
enable = true;
modules = with pkgs; [ dovecot_pigeonhole ];
enableLmtp = true;
enablePAM = false;
mailUser = "vmail";
mailGroup = "vmail";
mailLocation = "maildir:${cfg.storage}/%d/%n";
sslServerCert = "${cfg.certDir}/fullchain.pem";
sslServerKey = "${cfg.certDir}/key.pem";
mailboxes = {
Archive = { specialUse = "Archive"; auto = "subscribe"; };
Sent = { specialUse = "Sent"; auto = "subscribe"; };
Drafts = { specialUse = "Drafts"; auto = "subscribe"; };
Trash = { specialUse = "Trash"; auto = "subscribe"; };
Spam = { specialUse = "Junk"; auto = "subscribe"; };
};
sieveScripts = {
before = pkgs.writeText "spam.sieve" ''
require "fileinto";
if header :is "X-Spam" "Yes" {
fileinto "Spam";
}
'';
};
extraConfig = ''
# generated 2021-02-04, Mozilla Guideline v5.6, Dovecot 2.3.13, OpenSSL 1.1.1i, intermediate configuration
# https://ssl-config.mozilla.org/#server=dovecot&version=2.3.13&config=intermediate&openssl=1.1.1i&guideline=5.6
ssl = required
ssl_min_protocol = TLSv1.2
ssl_cipher_list = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
ssl_prefer_server_ciphers = no
protocol imap {
mail_plugins = $mail_plugins imap_sieve
}
protocol lmtp {
mail_plugins = $mail_plugins sieve
}
service imap-login {
inet_listener imap {
}
}
service lmtp {
unix_listener dovecot-lmtp {
mode = 0600
user = ${postfixCfg.user}
group = ${postfixCfg.group}
}
}
passdb {
driver = passwd-file
args = username_format=%u ${passdb}
}
userdb {
driver = static
args = uid=vmail gid=vmail home=${cfg.storage}/%d/%n
}
service auth {
unix_listener auth {
mode = 0660
user = ${postfixCfg.user}
group = ${postfixCfg.group}
}
}
lda_mailbox_autosubscribe = yes
lda_mailbox_autocreate = yes
plugin {
sieve_plugins = sieve_imapsieve sieve_extprograms
${lib.optionalString cfg.spam.enable ''
imapsieve_mailbox1_name = Spam
imapsieve_mailbox1_causes = COPY
imapsieve_mailbox1_before = file:/var/lib/dovecot/sieve/learn-spam.sieve
imapsieve_mailbox2_name = *
imapsieve_mailbox2_from = Spam
imapsieve_mailbox2_causes = COPY
imapsieve_mailbox2_before = file:/var/lib/dovecot/sieve/learn-ham.sieve
sieve_pipe_bin_dir = ${pkgs.symlinkJoin { name = "sieve-pipe-bin-dir"; paths = with pkgs; [ rspamd ]; } }/bin
''}
sieve_global_extensions = +vnd.dovecot.pipe
}
'';
};
systemd.services.dovecot2 = {
wants = [ "acme-finished-${cfg.fqdn}.target" ];
after = [ "acme-finished-${cfg.fqdn}.target" ];
preStart = lib.mkIf cfg.spam.enable
(lib.mkAfter
(lib.concatStrings
(lib.mapAttrsToList
(name: content: ''
cp ${pkgs.writeText name content} /var/lib/dovecot/sieve/${name}
'')
{
"learn-spam.sieve" = ''
require ["vnd.dovecot.pipe", "copy", "imapsieve"];
pipe :copy "rspamc" ["learn_spam"];
'';
"learn-ham.sieve" = ''
require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
if environment :matches "imap.mailbox" "*" {
set "mailbox" "''${1}";
}
if string "''${mailbox}" "Trash" {
stop;
}
pipe :copy "rspamc" ["learn_ham"];
'';
})));
};
networking.firewall.allowedTCPPorts = [
143 # IMAP
993 # IMAP (implicit TLS)
];
security.acme.certs."${cfg.fqdn}".postRun = ''
if systemctl is-active dovecot2; then
systemctl --no-block reload dovecot2
fi
'';
}

View file

@ -0,0 +1,170 @@
{ config, lib, pkgs, ... }:
let
cfg = config.sbruder.mailserver;
listToString = lib.concatStringsSep ",";
# List of attribute sets with single key-value pair
plainAliases = (lib.flatten
(map
({ address, aliases, ... }:
map
(alias: { "${alias}" = address; })
(aliases ++ lib.singleton address))
cfg.users));
# Attribute set with every alias mapped to a list of receivers
mergedAliases = (lib.attrsets.foldAttrs
(val: col: lib.singleton val ++ col)
[ ]
plainAliases);
# Contents of the aliases file
aliasesString = (lib.concatStringsSep
"\n"
(lib.mapAttrsToList
(alias: addresses: "${alias} ${listToString addresses}")
mergedAliases));
valiases = pkgs.writeText "valiases" aliasesString;
access_sender = pkgs.writeText
"access_sender"
(lib.concatMapStringsSep
"\n"
(sender: "${sender} REJECT")
cfg.rejectSenders);
submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules"
(lib.concatMapStringsSep
"\n"
(regex: "${regex} IGNORE")
cfg.cleanHeaders);
in
lib.mkIf cfg.enable {
security.dhparams.params.postfix = { };
services.postfix = {
enable = true;
enableSubmission = true; # plain/STARTTLS (latter is forced in submissionOptions)
enableSubmissions = true; # submission with implicit TLS (TCP/465)
hostname = cfg.fqdn;
networksStyle = "host";
sslCert = "${cfg.certDir}/fullchain.pem";
sslKey = "${cfg.certDir}/key.pem";
recipientDelimiter = "+";
mapFiles = {
inherit access_sender valiases;
};
config = {
# General
smtpd_banner = "${cfg.fqdn} ESMTP NO UCE";
disable_vrfy_command = true; # disable check if mailbox exists
enable_long_queue_ids = true; # better for debugging
strict_rfc821_envelopes = true; # only accept properly formatted envelope
message_size_limit = "50331648"; # 48MiB
virtual_mailbox_domains = listToString cfg.domains;
virtual_mailbox_maps = "hash:/var/lib/postfix/conf/valiases";
virtual_alias_maps = "hash:/var/lib/postfix/conf/valiases";
virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
smtpd_recipient_restrictions = listToString [
"reject_non_fqdn_recipient"
"reject_rbl_client ix.dnsbl.manitu.net"
"reject_unknown_recipient_domain"
"reject_unverified_recipient"
];
smtpd_client_restrictions = listToString [
"reject_rbl_client ix.dnsbl.manitu.net"
"reject_unknown_client_hostname"
];
smtpd_sender_restrictions = listToString [
"check_sender_access hash:/var/lib/postfix/conf/access_sender"
"reject_non_fqdn_sender"
"reject_unknown_sender_domain"
];
# generated 2021-02-04, Mozilla Guideline v5.6, Postfix 3.5.6, OpenSSL 1.1.1i, intermediate configuration
# https://ssl-config.mozilla.org/#server=postfix&version=3.5.6&config=intermediate&openssl=1.1.1i&guideline=5.6
smtpd_tls_security_level = "may";
smtpd_tls_auth_only = "yes";
smtpd_tls_mandatory_protocols = "!SSLv2, !SSLv3, !TLSv1, !TLSv1.1";
smtpd_tls_protocols = "!SSLv2, !SSLv3, !TLSv1, !TLSv1.1";
smtpd_tls_mandatory_ciphers = "medium";
smtpd_tls_loglevel = "1";
tls_medium_cipherlist = listToString [
"ECDHE-ECDSA-AES128-GCM-SHA256"
"ECDHE-RSA-AES128-GCM-SHA256"
"ECDHE-ECDSA-AES256-GCM-SHA384"
"ECDHE-RSA-AES256-GCM-SHA384"
"ECDHE-ECDSA-CHACHA20-POLY1305"
"ECDHE-RSA-CHACHA20-POLY1305"
"DHE-RSA-AES128-GCM-SHA256"
"DHE-RSA-AES256-GCM-SHA384"
];
tls_preempt_cipherlist = "no";
smtpd_tls_dh1024_param_file = config.security.dhparams.params.postfix.path;
};
# plain/STARTTLS (forced with smtpd_tls_security_level)
submissionOptions = {
smtpd_tls_security_level = "encrypt";
smtpd_sasl_auth_enable = "yes";
smtpd_sasl_type = "dovecot";
smtpd_sasl_path = "/run/dovecot2/auth";
smtpd_sender_login_maps = "hash:/etc/postfix/valiases";
smtpd_recipient_restrictions = listToString [ ];
smtpd_client_restrictions = listToString [
"permit_sasl_authenticated"
"reject"
];
smtpd_sender_restrictions = listToString [
"reject_sender_login_mismatch"
];
cleanup_service_name = "submission-header-cleanup";
};
# implicit TLS
submissionsOptions = config.services.postfix.submissionOptions;
masterConfig = {
submission-header-cleanup = {
private = false;
maxproc = 0;
command = "cleanup";
args = [ "-o" "header_checks=pcre:${submissionHeaderCleanupRules}" ];
};
};
};
networking.firewall.allowedTCPPorts = [
25 # SMTP
587 # SMTP submission
465 # SMTP submission (implicit TLS)
];
systemd.services.postfix = {
wants = [ "acme-finished-${cfg.fqdn}.target" ];
requires = [ "dovecot2.service" ];
after = [ "acme-finished-${cfg.fqdn}.target" "dovecot2.service" ];
};
security.acme.certs."${cfg.fqdn}".postRun = ''
if systemctl is-active postfix; then
systemctl --no-block reload postfix
fi
'';
}

View file

@ -0,0 +1,70 @@
{ config, lib, ... }:
let
cfg = config.sbruder.mailserver;
in
{
options.sbruder.mailserver.spam = {
enable = (lib.mkEnableOption "spam filtering") // { default = true; };
};
config = lib.mkIf (cfg.enable && cfg.spam.enable) {
sops.secrets.rspamd-worker-controller = {
owner = config.users.users.rspamd.name;
sopsFile = ../../machines + "/${config.networking.hostName}/secrets.yaml";
};
services.rspamd = {
enable = true;
postfix.enable = true;
workers = {
normal = {
includes = [ "$CONFDIR/worker-normal.inc" ];
bindSockets = lib.singleton {
socket = "/run/rspamd/rspamd.sock";
mode = "0660";
owner = "${config.services.rspamd.user}";
group = "${config.services.rspamd.group}";
};
};
controller = {
includes = [ "$CONFDIR/worker-controller.inc" ];
bindSockets = [ "127.0.0.1:11334" ] ++ lib.optional config.sbruder.wireguard.home.enable "${config.sbruder.wireguard.home.address}:11334";
};
};
locals = {
"dkim_signing.conf".text = ''
enabled = false;
'';
"logging.inc".text = ''
# starts at info, drops to notice once started up
level = "silent";
'';
"milter_headers.conf".text = ''
extended_spam_headers = true;
'';
"multimap.conf".text = ''
SENDER_DOMAIN_BLOCKED {
type = "from";
filter = "email:domain:tld";
map = "/var/lib/rspamd/blocked_sender_domains.map";
symbol = "SENDER_DOMAIN_BLOCKED";
description = "Senders effective second level domain is manually blocked";
score = 8.0;
}
'';
"redis.conf".text = ''
servers = "127.0.0.1:${toString config.services.redis.servers.rspamd.port}"
'';
"worker-controller.inc".source = config.sops.secrets.rspamd-worker-controller.path; # includes password
};
};
services.redis = {
vmOverCommit = true;
servers.rspamd = {
enable = true;
port = 6379;
};
};
};
}

View file

@ -0,0 +1,12 @@
{ config, lib, ... }:
lib.mkIf config.sbruder.mailserver.enable {
users.users.vmail = {
uid = 10000;
group = "vmail";
home = config.sbruder.mailserver.storage;
createHome = true;
};
users.groups.vmail.gid = 10000;
}