mailserver: Add module
This commit is contained in:
parent
e45b18abd0
commit
9c62905442
|
@ -43,6 +43,7 @@ in
|
||||||
./initrd-ssh.nix
|
./initrd-ssh.nix
|
||||||
./libvirt.nix
|
./libvirt.nix
|
||||||
./locales.nix
|
./locales.nix
|
||||||
|
./mailserver.nix
|
||||||
./media-proxy.nix
|
./media-proxy.nix
|
||||||
./network-manager.nix
|
./network-manager.nix
|
||||||
./office.nix
|
./office.nix
|
||||||
|
|
344
modules/mailserver.nix
Normal file
344
modules/mailserver.nix
Normal file
|
@ -0,0 +1,344 @@
|
||||||
|
{ 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" ];
|
||||||
|
};
|
||||||
|
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 user’s 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"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
25 # SMTP
|
||||||
|
587 # SMTP submission
|
||||||
|
];
|
||||||
|
|
||||||
|
# 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;
|
||||||
|
|
||||||
|
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"; # 48 MiB
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
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_client_restrictions = listToString [
|
||||||
|
"permit_sasl_authenticated"
|
||||||
|
"reject"
|
||||||
|
];
|
||||||
|
|
||||||
|
smtpd_sender_restrictions = listToString [
|
||||||
|
"reject_sender_login_mismatch"
|
||||||
|
];
|
||||||
|
|
||||||
|
cleanup_service_name = "submission-header-cleanup";
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
enableLmtp = true;
|
||||||
|
enablePAM = false;
|
||||||
|
|
||||||
|
mailUser = "vmail";
|
||||||
|
mailGroup = "vmail";
|
||||||
|
mailLocation = "maildir:${cfg.storage}/%d/%n";
|
||||||
|
|
||||||
|
sslServerCert = "${certDir}/fullchain.pem";
|
||||||
|
sslServerKey = "${certDir}/key.pem";
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in a new issue