move mailserver to module
This commit is contained in:
parent
5a3628dac0
commit
ca7f4f5811
|
@ -4,20 +4,22 @@
|
||||||
imports = with tree; [
|
imports = with tree; [
|
||||||
users.root
|
users.root
|
||||||
|
|
||||||
|
hosts.hetzner-vm.modules.mailserver
|
||||||
|
|
||||||
profiles.base
|
profiles.base
|
||||||
profiles.sshd
|
profiles.sshd
|
||||||
profiles.nginx
|
profiles.nginx
|
||||||
profiles.nix-gc
|
profiles.nix-gc
|
||||||
|
|
||||||
hosts.hetzner-vm.services.restic
|
hosts.hetzner-vm.profiles.restic
|
||||||
hosts.hetzner-vm.services.invidious
|
hosts.hetzner-vm.profiles.invidious
|
||||||
hosts.hetzner-vm.services.quassel
|
hosts.hetzner-vm.profiles.quassel
|
||||||
hosts.hetzner-vm.services.mpd
|
hosts.hetzner-vm.profiles.mpd
|
||||||
hosts.hetzner-vm.services.mail
|
hosts.hetzner-vm.profiles.mailserver
|
||||||
hosts.hetzner-vm.services.gitlab-static-sites
|
hosts.hetzner-vm.profiles.gitlab-static-sites
|
||||||
hosts.hetzner-vm.services.lappy-dev
|
hosts.hetzner-vm.profiles.lappy-dev
|
||||||
hosts.hetzner-vm.services.misskey
|
hosts.hetzner-vm.profiles.misskey
|
||||||
hosts.hetzner-vm.services.wireguard
|
hosts.hetzner-vm.profiles.wireguard
|
||||||
|
|
||||||
./networking.nix
|
./networking.nix
|
||||||
./hardware.nix
|
./hardware.nix
|
||||||
|
|
85
hosts/hetzner-vm/modules/mailserver/default.nix
Normal file
85
hosts/hetzner-vm/modules/mailserver/default.nix
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
{ config, lib, ... }:
|
||||||
|
with lib;
|
||||||
|
let cfg = config.mailserver;
|
||||||
|
in {
|
||||||
|
options.mailserver = {
|
||||||
|
enable = mkEnableOption "mailserver";
|
||||||
|
|
||||||
|
fqdn = mkOption { type = types.str; };
|
||||||
|
|
||||||
|
domains = mkOption { type = types.listOf types.str; };
|
||||||
|
|
||||||
|
ssl_config = mkOption {
|
||||||
|
type = (types.submodule {
|
||||||
|
options = {
|
||||||
|
useACME = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
};
|
||||||
|
cert = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "/var/lib/acme/${cfg.fqdn}/fullchain.pem";
|
||||||
|
};
|
||||||
|
key = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "/var/lib/acme/${cfg.fqdn}/key.pem";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
default = { };
|
||||||
|
};
|
||||||
|
debug_mode = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
accounts = mkOption {
|
||||||
|
# where name = email for login
|
||||||
|
type = types.attrsOf (types.submodule ({ config, name, ... }: {
|
||||||
|
options = {
|
||||||
|
name = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = name;
|
||||||
|
};
|
||||||
|
passwordFile = mkOption { type = types.str; };
|
||||||
|
aliases = mkOption { type = types.listOf types.str; };
|
||||||
|
sieveScript = mkOption { type = types.nullOr types.lines; };
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
sieve_directory = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "/var/sieve";
|
||||||
|
};
|
||||||
|
dkim_directory = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "/var/dkim";
|
||||||
|
};
|
||||||
|
|
||||||
|
policyd_config = mkOption {
|
||||||
|
type = types.lines;
|
||||||
|
default = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
vmail_config = mkOption {
|
||||||
|
type = (types.submodule {
|
||||||
|
options = {
|
||||||
|
user_group_name = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "vmail";
|
||||||
|
};
|
||||||
|
user_group_id = mkOption {
|
||||||
|
type = types.number;
|
||||||
|
default = 5000;
|
||||||
|
};
|
||||||
|
directory = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "/home/${cfg.vmail_config.user_group_name}";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
default = {};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
217
hosts/hetzner-vm/modules/mailserver/dovecot.nix
Normal file
217
hosts/hetzner-vm/modules/mailserver/dovecot.nix
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
let
|
||||||
|
mail_config = config.mailserver;
|
||||||
|
passwdDir = "/run/dovecot2";
|
||||||
|
passwdFile = "${passwdDir}/passwd";
|
||||||
|
|
||||||
|
bool2int = x: if x then "1" else "0";
|
||||||
|
|
||||||
|
# maildir in format "/${domain}/${user}"
|
||||||
|
dovecotMaildir = "maildir:${mail_config.vmail_config.directory}/%d/%n";
|
||||||
|
|
||||||
|
postfixCfg = config.services.postfix;
|
||||||
|
dovecot2Cfg = config.services.dovecot2;
|
||||||
|
|
||||||
|
stateDir = "/var/lib/dovecot";
|
||||||
|
|
||||||
|
passwordFiles =
|
||||||
|
lib.mapAttrs (name: value: value.passwordFile) mail_config.accounts;
|
||||||
|
|
||||||
|
genPasswdScript = pkgs.writeScript "generate-password-file" ''
|
||||||
|
#!${pkgs.stdenv.shell}
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if (! test -d "${passwdDir}"); then
|
||||||
|
mkdir "${passwdDir}"
|
||||||
|
chmod 755 "${passwdDir}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for f in ${
|
||||||
|
builtins.toString
|
||||||
|
(lib.mapAttrsToList (name: value: passwordFiles."${name}")
|
||||||
|
mail_config.accounts)
|
||||||
|
}; do
|
||||||
|
if [ ! -f "$f" ]; then
|
||||||
|
echo "Expected password hash file $f does not exist!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
cat <<EOF > ${passwdFile}
|
||||||
|
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
|
||||||
|
"${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}:${
|
||||||
|
builtins.toString mail_config.vmail_config.user_group_id
|
||||||
|
}:${
|
||||||
|
builtins.toString mail_config.vmail_config.user_group_id
|
||||||
|
}::${mail_config.vmail_config.directory}:/run/current-system/sw/bin/nologin:")
|
||||||
|
mail_config.accounts)}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod 600 ${passwdFile}
|
||||||
|
'';
|
||||||
|
|
||||||
|
pipeBin = pkgs.stdenv.mkDerivation {
|
||||||
|
name = "pipe_bin";
|
||||||
|
src = ./pipe_bin;
|
||||||
|
buildInputs = with pkgs; [ makeWrapper coreutils bash rspamd ];
|
||||||
|
buildCommand = ''
|
||||||
|
mkdir -p $out/pipe/bin
|
||||||
|
cp $src/* $out/pipe/bin/
|
||||||
|
chmod a+x $out/pipe/bin/*
|
||||||
|
patchShebangs $out/pipe/bin
|
||||||
|
|
||||||
|
for file in $out/pipe/bin/*; do
|
||||||
|
wrapProgram $file \
|
||||||
|
--set PATH "${pkgs.coreutils}/bin:${pkgs.rspamd}/bin"
|
||||||
|
done
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
in {
|
||||||
|
config = (lib.mkIf (mail_config.enable) {
|
||||||
|
services.dovecot2 = {
|
||||||
|
enable = true;
|
||||||
|
enableImap = true;
|
||||||
|
enablePop3 = false;
|
||||||
|
enablePAM = false;
|
||||||
|
enableQuota = true;
|
||||||
|
mailGroup = mail_config.vmail_config.user_group_name;
|
||||||
|
mailUser = mail_config.vmail_config.user_group_name;
|
||||||
|
mailLocation = dovecotMaildir;
|
||||||
|
sslServerCert = mail_config.ssl_config.cert;
|
||||||
|
sslServerKey = mail_config.ssl_config.key;
|
||||||
|
enableLmtp = true;
|
||||||
|
modules = [ pkgs.dovecot_pigeonhole ];
|
||||||
|
protocols = [ "sieve" ];
|
||||||
|
|
||||||
|
sieveScripts = {
|
||||||
|
after = builtins.toFile "spam.sieve" ''
|
||||||
|
require "fileinto";
|
||||||
|
|
||||||
|
if header :is "X-Spam" "Yes" {
|
||||||
|
fileinto "Junk";
|
||||||
|
stop;
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
mailboxes = {
|
||||||
|
Trash = {
|
||||||
|
auto = "no";
|
||||||
|
specialUse = "Trash";
|
||||||
|
};
|
||||||
|
Junk = {
|
||||||
|
auto = "subscribe";
|
||||||
|
specialUse = "Junk";
|
||||||
|
};
|
||||||
|
Drafts = {
|
||||||
|
auto = "subscribe";
|
||||||
|
specialUse = "Drafts";
|
||||||
|
};
|
||||||
|
Sent = {
|
||||||
|
auto = "subscribe";
|
||||||
|
specialUse = "Sent";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
extraConfig = ''
|
||||||
|
${lib.optionalString mail_config.debug_mode ''
|
||||||
|
mail_debug = yes
|
||||||
|
auth_debug = yes
|
||||||
|
verbose_ssl = yes
|
||||||
|
''}
|
||||||
|
|
||||||
|
service imap-login {
|
||||||
|
inet_listener imap {
|
||||||
|
port = 143
|
||||||
|
}
|
||||||
|
inet_listener imaps {
|
||||||
|
port = 993
|
||||||
|
ssl = yes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol imap {
|
||||||
|
mail_max_userip_connections = 100
|
||||||
|
mail_plugins = $mail_plugins imap_sieve
|
||||||
|
}
|
||||||
|
|
||||||
|
mail_access_groups = "${mail_config.vmail_config.user_group_name}"
|
||||||
|
ssl = required
|
||||||
|
ssl_min_protocol = TLSv1.2
|
||||||
|
ssl_prefer_server_ciphers = yes
|
||||||
|
|
||||||
|
service lmtp {
|
||||||
|
unix_listener dovecot-lmtp {
|
||||||
|
group = ${postfixCfg.group}
|
||||||
|
mode = 0600
|
||||||
|
user = ${postfixCfg.user}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recipient_delimiter = "+"
|
||||||
|
lmtp_save_to_detail_mailbox = "no"
|
||||||
|
|
||||||
|
protocol lmtp {
|
||||||
|
mail_plugins = $mail_plugins sieve
|
||||||
|
}
|
||||||
|
|
||||||
|
passdb {
|
||||||
|
driver = passwd-file
|
||||||
|
args = ${passwdFile}
|
||||||
|
}
|
||||||
|
|
||||||
|
userdb {
|
||||||
|
driver = passwd-file
|
||||||
|
args = ${passwdFile}
|
||||||
|
}
|
||||||
|
|
||||||
|
service auth {
|
||||||
|
unix_listener auth {
|
||||||
|
mode = 0660
|
||||||
|
user = ${postfixCfg.user}
|
||||||
|
group = ${postfixCfg.group}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auth_mechanisms = plain login
|
||||||
|
|
||||||
|
namespace inbox {
|
||||||
|
separator = "."
|
||||||
|
inbox = yes
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin {
|
||||||
|
sieve_plugins = sieve_imapsieve sieve_extprograms
|
||||||
|
sieve = file:${mail_config.sieve_directory}/%u/scripts;active=${mail_config.sieve_directory}/%u/active.sieve
|
||||||
|
sieve_default = file:${mail_config.sieve_directory}/%u/default.sieve
|
||||||
|
sieve_default_name = default
|
||||||
|
|
||||||
|
# From elsewhere to Spam folder
|
||||||
|
imapsieve_mailbox1_name = Junk
|
||||||
|
imapsieve_mailbox1_causes = COPY
|
||||||
|
imapsieve_mailbox1_before = file:${./spam_sieve/report-spam.sieve}
|
||||||
|
|
||||||
|
# From Spam folder to elsewhere
|
||||||
|
imapsieve_mailbox2_name = *
|
||||||
|
imapsieve_mailbox2_from = Junk
|
||||||
|
imapsieve_mailbox2_causes = COPY
|
||||||
|
imapsieve_mailbox2_before = file:${./spam_sieve/report-ham.sieve}
|
||||||
|
|
||||||
|
sieve_pipe_bin_dir = ${pipeBin}/pipe/bin
|
||||||
|
sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment
|
||||||
|
}
|
||||||
|
lda_mailbox_autosubscribe = yes
|
||||||
|
lda_mailbox_autocreate = yes
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.dovecot2 = {
|
||||||
|
preStart = ''
|
||||||
|
${genPasswdScript}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.postfix.restartTriggers = [ genPasswdScript ];
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
{ config, lib, pkgs, ... }:
|
{ config, lib, pkgs, ... }:
|
||||||
let
|
let
|
||||||
mail_config = (import ./config.nix { config = config; });
|
mail_config = config.mailserver;
|
||||||
dkimUser = config.services.opendkim.user;
|
dkimUser = config.services.opendkim.user;
|
||||||
dkimGroup = config.services.opendkim.group;
|
dkimGroup = config.services.opendkim.group;
|
||||||
|
|
||||||
|
@ -40,6 +40,7 @@ let
|
||||||
args = [ "-f" "-l" ]
|
args = [ "-f" "-l" ]
|
||||||
++ lib.optionals (dkim.configFile != null) [ "-x" dkim.configFile ];
|
++ lib.optionals (dkim.configFile != null) [ "-x" dkim.configFile ];
|
||||||
in {
|
in {
|
||||||
|
config = (lib.mkIf (mail_config.enable) {
|
||||||
services.opendkim = {
|
services.opendkim = {
|
||||||
enable = true;
|
enable = true;
|
||||||
selector = selector;
|
selector = selector;
|
||||||
|
@ -58,17 +59,19 @@ in {
|
||||||
''));
|
''));
|
||||||
};
|
};
|
||||||
|
|
||||||
users.users = lib.optionalAttrs (config.services.postfix.user == "postfix") {
|
users.users =
|
||||||
|
lib.optionalAttrs (config.services.postfix.user == "postfix") {
|
||||||
postfix.extraGroups = [ "${dkimGroup}" ];
|
postfix.extraGroups = [ "${dkimGroup}" ];
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.opendkim = {
|
systemd.services.opendkim = {
|
||||||
preStart = lib.mkForce createAllCerts;
|
preStart = lib.mkForce createAllCerts;
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ExecStart =
|
ExecStart = lib.mkForce
|
||||||
lib.mkForce "${pkgs.opendkim}/bin/opendkim ${lib.escapeShellArgs args}";
|
"${pkgs.opendkim}/bin/opendkim ${lib.escapeShellArgs args}";
|
||||||
PermissionsStartOnly = lib.mkForce false;
|
PermissionsStartOnly = lib.mkForce false;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
systemd.tmpfiles.rules = [ "d '${keyDir}' - ${dkimUser} ${dkimGroup} - -" ];
|
systemd.tmpfiles.rules = [ "d '${keyDir}' - ${dkimUser} ${dkimGroup} - -" ];
|
||||||
|
});
|
||||||
}
|
}
|
195
hosts/hetzner-vm/modules/mailserver/postfix.nix
Normal file
195
hosts/hetzner-vm/modules/mailserver/postfix.nix
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
mail_config = config.mailserver;
|
||||||
|
submissionHeaderCleanupRules =
|
||||||
|
pkgs.writeText "submission_header_cleanup_rules" (''
|
||||||
|
/^Received:/ IGNORE
|
||||||
|
/^X-Originating-IP:/ IGNORE
|
||||||
|
/^X-Mailer:/ IGNORE
|
||||||
|
/^User-Agent:/ IGNORE
|
||||||
|
/^X-Enigmail:/ IGNORE
|
||||||
|
/^Message-ID:\s+<(.*?)@.*?>/ REPLACE Message-ID: <$1@${mail_config.fqdn}>
|
||||||
|
'');
|
||||||
|
|
||||||
|
inetSocket = addr: port: "inet:[${toString port}@${addr}]";
|
||||||
|
unixSocket = sock: "unix:${sock}";
|
||||||
|
|
||||||
|
# Merge several lookup tables. A lookup table is a attribute set where
|
||||||
|
# - the key is an address (user@example.com) or a domain (@example.com)
|
||||||
|
# - the value is a list of addresses
|
||||||
|
mergeLookupTables = tables: lib.zipAttrsWith (n: v: lib.flatten v) tables;
|
||||||
|
|
||||||
|
# valiases_postfix :: Map String [String]
|
||||||
|
valiases_postfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
|
||||||
|
(name: value:
|
||||||
|
let to = name;
|
||||||
|
in map (from: { "${from}" = to; }) (value.aliases ++ lib.singleton name))
|
||||||
|
mail_config.accounts));
|
||||||
|
|
||||||
|
# all_valiases_postfix :: Map String [String]
|
||||||
|
all_valiases_postfix = mergeLookupTables [ valiases_postfix ];
|
||||||
|
|
||||||
|
# lookupTableToString :: Map String [String] -> String
|
||||||
|
lookupTableToString = attrs:
|
||||||
|
let valueToString = value: lib.concatStringsSep ", " value;
|
||||||
|
in lib.concatStringsSep "\n"
|
||||||
|
(lib.mapAttrsToList (name: value: "${name} ${valueToString value}") attrs);
|
||||||
|
|
||||||
|
# valiases_file :: Path
|
||||||
|
valiases_file = let
|
||||||
|
content = lookupTableToString (mergeLookupTables [ all_valiases_postfix ]);
|
||||||
|
in builtins.toFile "valias" content;
|
||||||
|
|
||||||
|
# vhosts_file :: Path
|
||||||
|
vhosts_file =
|
||||||
|
builtins.toFile "vhosts" (lib.concatStringsSep "\n" mail_config.domains);
|
||||||
|
vaccounts_file =
|
||||||
|
builtins.toFile "vaccounts" (lookupTableToString all_valiases_postfix);
|
||||||
|
|
||||||
|
mappedFile = name: "hash:/var/lib/postfix/conf/${name}";
|
||||||
|
|
||||||
|
policyd-spf = pkgs.writeText "policyd-spf.conf" mail_config.policyd_config;
|
||||||
|
|
||||||
|
submissionOptions = {
|
||||||
|
smtpd_tls_security_level = "encrypt";
|
||||||
|
smtpd_sasl_auth_enable = "yes";
|
||||||
|
smtpd_sasl_type = "dovecot";
|
||||||
|
smtpd_sasl_path = "/run/dovecot2/auth";
|
||||||
|
smtpd_sasl_security_options = "noanonymous";
|
||||||
|
smtpd_sasl_local_domain = "$myhostname";
|
||||||
|
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
|
||||||
|
smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts";
|
||||||
|
smtpd_sender_restrictions = "reject_sender_login_mismatch";
|
||||||
|
smtpd_recipient_restrictions =
|
||||||
|
"reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject";
|
||||||
|
cleanup_service_name = "submission-header-cleanup";
|
||||||
|
};
|
||||||
|
|
||||||
|
tls_allowed = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
|
||||||
|
tls_disallow = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
|
||||||
|
in {
|
||||||
|
config = (lib.mkIf (mail_config.enable) {
|
||||||
|
services.postfix = {
|
||||||
|
enable = true;
|
||||||
|
hostname = "${mail_config.fqdn}";
|
||||||
|
networksStyle = "host";
|
||||||
|
mapFiles."valias" = valiases_file;
|
||||||
|
mapFiles."vaccounts" = vaccounts_file;
|
||||||
|
sslCert = mail_config.ssl_config.cert;
|
||||||
|
sslKey = mail_config.ssl_config.key;
|
||||||
|
enableSubmission = true;
|
||||||
|
enableSubmissions = true;
|
||||||
|
virtual =
|
||||||
|
lookupTableToString (mergeLookupTables [ all_valiases_postfix ]);
|
||||||
|
|
||||||
|
config = {
|
||||||
|
# Extra Config
|
||||||
|
mydestination = "";
|
||||||
|
recipient_delimiter = "+";
|
||||||
|
smtpd_banner = "${mail_config.fqdn} ESMTP NO UCE";
|
||||||
|
disable_vrfy_command = true;
|
||||||
|
message_size_limit = "20971520";
|
||||||
|
|
||||||
|
virtual_uid_maps =
|
||||||
|
"static:${toString mail_config.vmail_config.user_group_id}";
|
||||||
|
virtual_gid_maps =
|
||||||
|
"static:${toString mail_config.vmail_config.user_group_id}";
|
||||||
|
virtual_mailbox_base = "${mail_config.vmail_config.directory}";
|
||||||
|
virtual_mailbox_domains = vhosts_file;
|
||||||
|
virtual_mailbox_maps = mappedFile "valias";
|
||||||
|
virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
|
||||||
|
lmtp_destination_recipient_limit = "1";
|
||||||
|
|
||||||
|
smtpd_sasl_type = "dovecot";
|
||||||
|
smtpd_sasl_path = "/run/dovecot2/auth";
|
||||||
|
smtpd_sasl_auth_enable = true;
|
||||||
|
smtpd_relay_restrictions = [
|
||||||
|
"permit_mynetworks"
|
||||||
|
"permit_sasl_authenticated"
|
||||||
|
"reject_unauth_destination"
|
||||||
|
];
|
||||||
|
|
||||||
|
policy-spf_time_limit = "3600s";
|
||||||
|
|
||||||
|
smtpd_recipient_restrictions = [
|
||||||
|
#"check_recipient_access ${mappedFile "denied_recipients"}"
|
||||||
|
#"check_recipient_access ${mappedFile "reject_recipients"}"
|
||||||
|
"check_policy_service unix:private/policy-spf"
|
||||||
|
];
|
||||||
|
|
||||||
|
# TLS settings, inspired by https://github.com/jeaye/nix-files
|
||||||
|
# Submission by mail clients is handled in submissionOptions
|
||||||
|
smtpd_tls_security_level = "may";
|
||||||
|
|
||||||
|
# strong might suffice and is computationally less expensive
|
||||||
|
smtpd_tls_eecdh_grade = "ultra";
|
||||||
|
|
||||||
|
# Only Alow Modern TLS
|
||||||
|
smtp_tls_protocols = tls_allowed;
|
||||||
|
smtpd_tls_protocols = tls_allowed;
|
||||||
|
smtp_tls_mandatory_protocols = tls_allowed;
|
||||||
|
smtpd_tls_mandatory_protocols = tls_allowed;
|
||||||
|
|
||||||
|
# Disable Old Ciphers
|
||||||
|
smtp_tls_exclude_ciphers = tls_disallow;
|
||||||
|
smtpd_tls_exclude_ciphers = tls_disallow;
|
||||||
|
smtp_tls_mandatory_exclude_ciphers = tls_disallow;
|
||||||
|
smtpd_tls_mandatory_exclude_ciphers = tls_disallow;
|
||||||
|
|
||||||
|
smtp_tls_ciphers = "high";
|
||||||
|
smtpd_tls_ciphers = "high";
|
||||||
|
smtp_tls_mandatory_ciphers = "high";
|
||||||
|
smtpd_tls_mandatory_ciphers = "high";
|
||||||
|
|
||||||
|
tls_preempt_cipherlist = true;
|
||||||
|
|
||||||
|
smtpd_tls_auth_only = true;
|
||||||
|
smtpd_tls_loglevel = "1";
|
||||||
|
|
||||||
|
tls_random_source = "dev:/dev/urandom";
|
||||||
|
|
||||||
|
smtpd_milters = [
|
||||||
|
"unix:/run/opendkim/opendkim.sock"
|
||||||
|
"unix:/run/rspamd/rspamd-milter.sock"
|
||||||
|
];
|
||||||
|
non_smtpd_milters = [ "unix:/run/opendkim/opendkim.sock" ];
|
||||||
|
|
||||||
|
milter_protocol = "6";
|
||||||
|
milter_mail_macros =
|
||||||
|
"i {mail_addr} {client_addr} {client_name} {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}";
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
submissionOptions = submissionOptions;
|
||||||
|
submissionsOptions = submissionOptions;
|
||||||
|
|
||||||
|
masterConfig = {
|
||||||
|
"lmtp" = {
|
||||||
|
# Add headers when delivering, see http://www.postfix.org/smtp.8.html
|
||||||
|
# D => Delivered-To, O => X-Original-To, R => Return-Path
|
||||||
|
args = [ "flags=O" ];
|
||||||
|
};
|
||||||
|
"policy-spf" = {
|
||||||
|
type = "unix";
|
||||||
|
privileged = true;
|
||||||
|
chroot = false;
|
||||||
|
command = "spawn";
|
||||||
|
args = [
|
||||||
|
"user=nobody"
|
||||||
|
"argv=${pkgs.pypolicyd-spf}/bin/policyd-spf"
|
||||||
|
"${policyd-spf}"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
"submission-header-cleanup" = {
|
||||||
|
type = "unix";
|
||||||
|
private = false;
|
||||||
|
chroot = false;
|
||||||
|
maxproc = 0;
|
||||||
|
command = "cleanup";
|
||||||
|
args = [ "-o" "header_checks=pcre:${submissionHeaderCleanupRules}" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
99
hosts/hetzner-vm/modules/mailserver/rspamd.nix
Normal file
99
hosts/hetzner-vm/modules/mailserver/rspamd.nix
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
mail_config = config.mailserver;
|
||||||
|
|
||||||
|
ports = (import ../../ports.nix { });
|
||||||
|
|
||||||
|
postfixCfg = config.services.postfix;
|
||||||
|
rspamdCfg = config.services.rspamd;
|
||||||
|
rspamdSocket = "rspamd.service";
|
||||||
|
in {
|
||||||
|
config = (lib.mkIf (mail_config.enable) {
|
||||||
|
|
||||||
|
services.rspamd = {
|
||||||
|
enable = true;
|
||||||
|
debug = mail_config.debug_mode;
|
||||||
|
locals = {
|
||||||
|
"milter_headers.conf" = {
|
||||||
|
text = ''
|
||||||
|
extended_spam_headers = yes;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
"redis.conf" = {
|
||||||
|
text = ''
|
||||||
|
servers = "127.0.0.1:${toString ports.rspamd-redis}";
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
"classifier-bayes.conf" = {
|
||||||
|
text = ''
|
||||||
|
cache {
|
||||||
|
backend = "redis";
|
||||||
|
}
|
||||||
|
min_learns = 5;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
"dkim_signing.conf" = {
|
||||||
|
text = ''
|
||||||
|
# opendkim does this
|
||||||
|
enabled = false;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
overrides = {
|
||||||
|
"milter_headers.conf" = {
|
||||||
|
text = ''
|
||||||
|
extended_spam_headers = true;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
workers.rspamd_proxy = {
|
||||||
|
type = "rspamd_proxy";
|
||||||
|
bindSockets = [{
|
||||||
|
socket = "/run/rspamd/rspamd-milter.sock";
|
||||||
|
mode = "0664";
|
||||||
|
}];
|
||||||
|
count = 1;
|
||||||
|
extraConfig = ''
|
||||||
|
milter = yes;
|
||||||
|
timeout = 120s;
|
||||||
|
|
||||||
|
upstream "local" {
|
||||||
|
default = yes;
|
||||||
|
self_scan = yes;
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
workers.controller = {
|
||||||
|
type = "controller";
|
||||||
|
count = 1;
|
||||||
|
bindSockets = [{
|
||||||
|
socket = "/run/rspamd/worker-controller.sock";
|
||||||
|
mode = "0666";
|
||||||
|
}];
|
||||||
|
includes = [ ];
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
services.redis.servers.rspamd = {
|
||||||
|
enable = true;
|
||||||
|
port = ports.rspamd-redis;
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.rspamd = {
|
||||||
|
requires = [ "redis-rspamd.service" ];
|
||||||
|
after = [ "redis-rspamd.service" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.postfix = {
|
||||||
|
after = [ rspamdSocket ];
|
||||||
|
requires = [ rspamdSocket ];
|
||||||
|
};
|
||||||
|
|
||||||
|
users.extraUsers.${postfixCfg.user}.extraGroups = [ rspamdCfg.group ];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
23
hosts/hetzner-vm/modules/mailserver/ssl.nix
Normal file
23
hosts/hetzner-vm/modules/mailserver/ssl.nix
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
let
|
||||||
|
mail_config = config.mailserver;
|
||||||
|
acmeRoot = "/var/lib/acme/acme-challenge";
|
||||||
|
|
||||||
|
in {
|
||||||
|
config = (lib.mkIf (mail_config.enable && mail_config.ssl_config.useACME) {
|
||||||
|
services.nginx = {
|
||||||
|
enable = true;
|
||||||
|
virtualHosts."${mail_config.fqdn}" = {
|
||||||
|
serverName = mail_config.fqdn;
|
||||||
|
serverAliases = mail_config.domains;
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
acmeRoot = acmeRoot;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
security.acme.certs."${mail_config.fqdn}" = {
|
||||||
|
reloadServices = [ "postfix.service" "dovecot2.service" ];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
{ config, pkgs, lib, ... }:
|
{ config, pkgs, lib, ... }:
|
||||||
let
|
let
|
||||||
mail_config = (import ./config.nix { config = config; });
|
mail_config = config.mailserver;
|
||||||
|
|
||||||
v = mail_config.vmail_config;
|
v = mail_config.vmail_config;
|
||||||
sieve_directory = mail_config.sieve_directory;
|
sieve_directory = mail_config.sieve_directory;
|
||||||
|
@ -43,6 +43,7 @@ let
|
||||||
'';
|
'';
|
||||||
|
|
||||||
in {
|
in {
|
||||||
|
config = (lib.mkIf (mail_config.enable) {
|
||||||
users.users."${v.user_group_name}" = {
|
users.users."${v.user_group_name}" = {
|
||||||
name = "${v.user_group_name}";
|
name = "${v.user_group_name}";
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
|
@ -58,4 +59,5 @@ in {
|
||||||
serviceConfig = { ExecStart = virtualMailUsersActivationScript; };
|
serviceConfig = { ExecStart = virtualMailUsersActivationScript; };
|
||||||
enable = true;
|
enable = true;
|
||||||
};
|
};
|
||||||
|
});
|
||||||
}
|
}
|
19
hosts/hetzner-vm/modules/mailserver/webmail.nix
Normal file
19
hosts/hetzner-vm/modules/mailserver/webmail.nix
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{ config, lib, ... }:
|
||||||
|
let mail_config = config.mailserver;
|
||||||
|
in {
|
||||||
|
config = (lib.mkIf (mail_config.enable) {
|
||||||
|
services.roundcube = {
|
||||||
|
enable = true;
|
||||||
|
hostName = "mail.owo.monster";
|
||||||
|
extraConfig = ''
|
||||||
|
$config['smtp_server'] = "tls://${mail_config.fqdn}";
|
||||||
|
$config['smtp_user'] = "%u";
|
||||||
|
$config['smtp_pass'] = "%p";
|
||||||
|
$config['plugins'] = ["managesieve"];
|
||||||
|
$config['managesieve_host'] = 'tls://${mail_config.fqdn}';
|
||||||
|
$config['session_lifetime'] = 168;
|
||||||
|
$config['product_name'] = 'Chaos Mail';
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
28
hosts/hetzner-vm/profiles/mailserver.nix
Normal file
28
hosts/hetzner-vm/profiles/mailserver.nix
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{ config, ... }:
|
||||||
|
let secrets = config.services.secrets.secrets;
|
||||||
|
in {
|
||||||
|
config.mailserver = {
|
||||||
|
enable = true;
|
||||||
|
fqdn = "mail.owo.monster";
|
||||||
|
domains = [ "owo.monster" "kitteh.pw" ];
|
||||||
|
|
||||||
|
debug_mode = false;
|
||||||
|
|
||||||
|
accounts = {
|
||||||
|
"chaoticryptidz@owo.monster" = {
|
||||||
|
name = "chaoticryptidz@owo.monster";
|
||||||
|
passwordFile = "${secrets.chaos_mail_passwd.path}";
|
||||||
|
aliases = [
|
||||||
|
"all@owo.monster"
|
||||||
|
# for sending from
|
||||||
|
"chaos@owo.monster"
|
||||||
|
|
||||||
|
# TODO: legacy - to be deprecated by 2023-01-01
|
||||||
|
"kitteh@owo.monster"
|
||||||
|
"kitteh@kitteh.pw"
|
||||||
|
];
|
||||||
|
sieveScript = null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,5 +1,3 @@
|
||||||
|
|
||||||
|
|
||||||
{ lib, stdenv, fetchFromGitHub }:
|
{ lib, stdenv, fetchFromGitHub }:
|
||||||
|
|
||||||
let
|
let
|
|
@ -1,7 +1,7 @@
|
||||||
{ lib, config, pkgs, ... }:
|
{ lib, config, pkgs, ... }:
|
||||||
let
|
let
|
||||||
secrets = config.services.secrets.secrets;
|
secrets = config.services.secrets.secrets;
|
||||||
mail_config = (import ./mailserver/config.nix { config = config; });
|
mail_config = config.mailserver;
|
||||||
|
|
||||||
backupPrepareCommand = "${
|
backupPrepareCommand = "${
|
||||||
(pkgs.writeShellScriptBin "backupPrepareCommand" ''
|
(pkgs.writeShellScriptBin "backupPrepareCommand" ''
|
|
@ -1,12 +0,0 @@
|
||||||
{ ... }: {
|
|
||||||
imports = [
|
|
||||||
./mailserver/postfix.nix
|
|
||||||
./mailserver/vmail.nix
|
|
||||||
./mailserver/ssl.nix
|
|
||||||
./mailserver/dovecot.nix
|
|
||||||
./mailserver/firewall.nix
|
|
||||||
./mailserver/webmail.nix
|
|
||||||
./mailserver/opendkim.nix
|
|
||||||
./mailserver/rspamd.nix
|
|
||||||
];
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
{ config }:
|
|
||||||
let secrets = config.services.secrets.secrets;
|
|
||||||
in rec {
|
|
||||||
fqdn = "mail.owo.monster";
|
|
||||||
domains = [
|
|
||||||
"owo.monster"
|
|
||||||
"kitteh.pw"
|
|
||||||
# "mailchaos.net"
|
|
||||||
];
|
|
||||||
|
|
||||||
debug_mode = false;
|
|
||||||
|
|
||||||
ssl_config = {
|
|
||||||
cert = "/var/lib/acme/${fqdn}/fullchain.pem";
|
|
||||||
key = "/var/lib/acme/${fqdn}/key.pem";
|
|
||||||
};
|
|
||||||
|
|
||||||
# generate password files with:
|
|
||||||
# nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "password" | cut -d: -f2
|
|
||||||
|
|
||||||
accounts = {
|
|
||||||
"chaoticryptidz@owo.monster" = {
|
|
||||||
name = "chaoticryptidz@owo.monster";
|
|
||||||
passwordFile = "${secrets.chaos_mail_passwd.path}";
|
|
||||||
aliases = [
|
|
||||||
"all@owo.monster"
|
|
||||||
# for sending from
|
|
||||||
"chaos@owo.monster"
|
|
||||||
|
|
||||||
# TODO: legacy - to be deprecated by 2023-01-01
|
|
||||||
"kitteh@owo.monster"
|
|
||||||
"kitteh@kitteh.pw"
|
|
||||||
];
|
|
||||||
sieveScript = null;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
sieve_directory = "/var/sieve";
|
|
||||||
dkim_directory = "/var/dkim";
|
|
||||||
|
|
||||||
policyd_config = "";
|
|
||||||
|
|
||||||
vmail_config = {
|
|
||||||
user_group_name = "vmail";
|
|
||||||
user_group_id = 5000;
|
|
||||||
directory = "/home/vmail";
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,215 +0,0 @@
|
||||||
{ config, pkgs, lib, ... }:
|
|
||||||
let
|
|
||||||
mail_config = (import ./config.nix { config = config; });
|
|
||||||
passwdDir = "/run/dovecot2";
|
|
||||||
passwdFile = "${passwdDir}/passwd";
|
|
||||||
|
|
||||||
bool2int = x: if x then "1" else "0";
|
|
||||||
|
|
||||||
# maildir in format "/${domain}/${user}"
|
|
||||||
dovecotMaildir = "maildir:${mail_config.vmail_config.directory}/%d/%n";
|
|
||||||
|
|
||||||
postfixCfg = config.services.postfix;
|
|
||||||
dovecot2Cfg = config.services.dovecot2;
|
|
||||||
|
|
||||||
stateDir = "/var/lib/dovecot";
|
|
||||||
|
|
||||||
passwordFiles =
|
|
||||||
lib.mapAttrs (name: value: value.passwordFile) mail_config.accounts;
|
|
||||||
|
|
||||||
genPasswdScript = pkgs.writeScript "generate-password-file" ''
|
|
||||||
#!${pkgs.stdenv.shell}
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
if (! test -d "${passwdDir}"); then
|
|
||||||
mkdir "${passwdDir}"
|
|
||||||
chmod 755 "${passwdDir}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
for f in ${
|
|
||||||
builtins.toString
|
|
||||||
(lib.mapAttrsToList (name: value: passwordFiles."${name}")
|
|
||||||
mail_config.accounts)
|
|
||||||
}; do
|
|
||||||
if [ ! -f "$f" ]; then
|
|
||||||
echo "Expected password hash file $f does not exist!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
cat <<EOF > ${passwdFile}
|
|
||||||
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
|
|
||||||
"${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}:${
|
|
||||||
builtins.toString mail_config.vmail_config.user_group_id
|
|
||||||
}:${
|
|
||||||
builtins.toString mail_config.vmail_config.user_group_id
|
|
||||||
}::${mail_config.vmail_config.directory}:/run/current-system/sw/bin/nologin:")
|
|
||||||
mail_config.accounts)}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chmod 600 ${passwdFile}
|
|
||||||
'';
|
|
||||||
|
|
||||||
pipeBin = pkgs.stdenv.mkDerivation {
|
|
||||||
name = "pipe_bin";
|
|
||||||
src = ./pipe_bin;
|
|
||||||
buildInputs = with pkgs; [ makeWrapper coreutils bash rspamd ];
|
|
||||||
buildCommand = ''
|
|
||||||
mkdir -p $out/pipe/bin
|
|
||||||
cp $src/* $out/pipe/bin/
|
|
||||||
chmod a+x $out/pipe/bin/*
|
|
||||||
patchShebangs $out/pipe/bin
|
|
||||||
|
|
||||||
for file in $out/pipe/bin/*; do
|
|
||||||
wrapProgram $file \
|
|
||||||
--set PATH "${pkgs.coreutils}/bin:${pkgs.rspamd}/bin"
|
|
||||||
done
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
in {
|
|
||||||
services.dovecot2 = {
|
|
||||||
enable = true;
|
|
||||||
enableImap = true;
|
|
||||||
enablePop3 = false;
|
|
||||||
enablePAM = false;
|
|
||||||
enableQuota = true;
|
|
||||||
mailGroup = mail_config.vmail_config.user_group_name;
|
|
||||||
mailUser = mail_config.vmail_config.user_group_name;
|
|
||||||
mailLocation = dovecotMaildir;
|
|
||||||
sslServerCert = mail_config.ssl_config.cert;
|
|
||||||
sslServerKey = mail_config.ssl_config.key;
|
|
||||||
enableLmtp = true;
|
|
||||||
modules = [ pkgs.dovecot_pigeonhole ];
|
|
||||||
protocols = [ "sieve" ];
|
|
||||||
|
|
||||||
sieveScripts = {
|
|
||||||
after = builtins.toFile "spam.sieve" ''
|
|
||||||
require "fileinto";
|
|
||||||
|
|
||||||
if header :is "X-Spam" "Yes" {
|
|
||||||
fileinto "Junk";
|
|
||||||
stop;
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
mailboxes = {
|
|
||||||
Trash = {
|
|
||||||
auto = "no";
|
|
||||||
specialUse = "Trash";
|
|
||||||
};
|
|
||||||
Junk = {
|
|
||||||
auto = "subscribe";
|
|
||||||
specialUse = "Junk";
|
|
||||||
};
|
|
||||||
Drafts = {
|
|
||||||
auto = "subscribe";
|
|
||||||
specialUse = "Drafts";
|
|
||||||
};
|
|
||||||
Sent = {
|
|
||||||
auto = "subscribe";
|
|
||||||
specialUse = "Sent";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
extraConfig = ''
|
|
||||||
${lib.optionalString mail_config.debug_mode ''
|
|
||||||
mail_debug = yes
|
|
||||||
auth_debug = yes
|
|
||||||
verbose_ssl = yes
|
|
||||||
''}
|
|
||||||
|
|
||||||
service imap-login {
|
|
||||||
inet_listener imap {
|
|
||||||
port = 143
|
|
||||||
}
|
|
||||||
inet_listener imaps {
|
|
||||||
port = 993
|
|
||||||
ssl = yes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol imap {
|
|
||||||
mail_max_userip_connections = 100
|
|
||||||
mail_plugins = $mail_plugins imap_sieve
|
|
||||||
}
|
|
||||||
|
|
||||||
mail_access_groups = "${mail_config.vmail_config.user_group_name}"
|
|
||||||
ssl = required
|
|
||||||
ssl_min_protocol = TLSv1.2
|
|
||||||
ssl_prefer_server_ciphers = yes
|
|
||||||
|
|
||||||
service lmtp {
|
|
||||||
unix_listener dovecot-lmtp {
|
|
||||||
group = ${postfixCfg.group}
|
|
||||||
mode = 0600
|
|
||||||
user = ${postfixCfg.user}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
recipient_delimiter = "+"
|
|
||||||
lmtp_save_to_detail_mailbox = "no"
|
|
||||||
|
|
||||||
protocol lmtp {
|
|
||||||
mail_plugins = $mail_plugins sieve
|
|
||||||
}
|
|
||||||
|
|
||||||
passdb {
|
|
||||||
driver = passwd-file
|
|
||||||
args = ${passwdFile}
|
|
||||||
}
|
|
||||||
|
|
||||||
userdb {
|
|
||||||
driver = passwd-file
|
|
||||||
args = ${passwdFile}
|
|
||||||
}
|
|
||||||
|
|
||||||
service auth {
|
|
||||||
unix_listener auth {
|
|
||||||
mode = 0660
|
|
||||||
user = ${postfixCfg.user}
|
|
||||||
group = ${postfixCfg.group}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auth_mechanisms = plain login
|
|
||||||
|
|
||||||
namespace inbox {
|
|
||||||
separator = "."
|
|
||||||
inbox = yes
|
|
||||||
}
|
|
||||||
|
|
||||||
plugin {
|
|
||||||
sieve_plugins = sieve_imapsieve sieve_extprograms
|
|
||||||
sieve = file:${mail_config.sieve_directory}/%u/scripts;active=${mail_config.sieve_directory}/%u/active.sieve
|
|
||||||
sieve_default = file:${mail_config.sieve_directory}/%u/default.sieve
|
|
||||||
sieve_default_name = default
|
|
||||||
|
|
||||||
# From elsewhere to Spam folder
|
|
||||||
imapsieve_mailbox1_name = Junk
|
|
||||||
imapsieve_mailbox1_causes = COPY
|
|
||||||
imapsieve_mailbox1_before = file:${./spam_sieve/report-spam.sieve}
|
|
||||||
|
|
||||||
# From Spam folder to elsewhere
|
|
||||||
imapsieve_mailbox2_name = *
|
|
||||||
imapsieve_mailbox2_from = Junk
|
|
||||||
imapsieve_mailbox2_causes = COPY
|
|
||||||
imapsieve_mailbox2_before = file:${./spam_sieve/report-ham.sieve}
|
|
||||||
|
|
||||||
sieve_pipe_bin_dir = ${pipeBin}/pipe/bin
|
|
||||||
sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment
|
|
||||||
}
|
|
||||||
lda_mailbox_autosubscribe = yes
|
|
||||||
lda_mailbox_autocreate = yes
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.dovecot2 = {
|
|
||||||
preStart = ''
|
|
||||||
${genPasswdScript}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.postfix.restartTriggers = [ genPasswdScript ];
|
|
||||||
}
|
|
|
@ -1,193 +0,0 @@
|
||||||
{ config, pkgs, lib, ... }:
|
|
||||||
|
|
||||||
let
|
|
||||||
mail_config = (import ./config.nix { config = config; });
|
|
||||||
submissionHeaderCleanupRules =
|
|
||||||
pkgs.writeText "submission_header_cleanup_rules" (''
|
|
||||||
/^Received:/ IGNORE
|
|
||||||
/^X-Originating-IP:/ IGNORE
|
|
||||||
/^X-Mailer:/ IGNORE
|
|
||||||
/^User-Agent:/ IGNORE
|
|
||||||
/^X-Enigmail:/ IGNORE
|
|
||||||
/^Message-ID:\s+<(.*?)@.*?>/ REPLACE Message-ID: <$1@${mail_config.fqdn}>
|
|
||||||
'');
|
|
||||||
|
|
||||||
inetSocket = addr: port: "inet:[${toString port}@${addr}]";
|
|
||||||
unixSocket = sock: "unix:${sock}";
|
|
||||||
|
|
||||||
# Merge several lookup tables. A lookup table is a attribute set where
|
|
||||||
# - the key is an address (user@example.com) or a domain (@example.com)
|
|
||||||
# - the value is a list of addresses
|
|
||||||
mergeLookupTables = tables: lib.zipAttrsWith (n: v: lib.flatten v) tables;
|
|
||||||
|
|
||||||
# valiases_postfix :: Map String [String]
|
|
||||||
valiases_postfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
|
|
||||||
(name: value:
|
|
||||||
let to = name;
|
|
||||||
in map (from: { "${from}" = to; }) (value.aliases ++ lib.singleton name))
|
|
||||||
mail_config.accounts));
|
|
||||||
|
|
||||||
# all_valiases_postfix :: Map String [String]
|
|
||||||
all_valiases_postfix = mergeLookupTables [ valiases_postfix ];
|
|
||||||
|
|
||||||
# lookupTableToString :: Map String [String] -> String
|
|
||||||
lookupTableToString = attrs:
|
|
||||||
let valueToString = value: lib.concatStringsSep ", " value;
|
|
||||||
in lib.concatStringsSep "\n"
|
|
||||||
(lib.mapAttrsToList (name: value: "${name} ${valueToString value}") attrs);
|
|
||||||
|
|
||||||
# valiases_file :: Path
|
|
||||||
valiases_file = let
|
|
||||||
content = lookupTableToString (mergeLookupTables [ all_valiases_postfix ]);
|
|
||||||
in builtins.toFile "valias" content;
|
|
||||||
|
|
||||||
# vhosts_file :: Path
|
|
||||||
vhosts_file =
|
|
||||||
builtins.toFile "vhosts" (lib.concatStringsSep "\n" mail_config.domains);
|
|
||||||
vaccounts_file =
|
|
||||||
builtins.toFile "vaccounts" (lookupTableToString all_valiases_postfix);
|
|
||||||
|
|
||||||
mappedFile = name: "hash:/var/lib/postfix/conf/${name}";
|
|
||||||
|
|
||||||
policyd-spf = pkgs.writeText "policyd-spf.conf" mail_config.policyd_config;
|
|
||||||
|
|
||||||
submissionOptions = {
|
|
||||||
smtpd_tls_security_level = "encrypt";
|
|
||||||
smtpd_sasl_auth_enable = "yes";
|
|
||||||
smtpd_sasl_type = "dovecot";
|
|
||||||
smtpd_sasl_path = "/run/dovecot2/auth";
|
|
||||||
smtpd_sasl_security_options = "noanonymous";
|
|
||||||
smtpd_sasl_local_domain = "$myhostname";
|
|
||||||
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
|
|
||||||
smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts";
|
|
||||||
smtpd_sender_restrictions = "reject_sender_login_mismatch";
|
|
||||||
smtpd_recipient_restrictions =
|
|
||||||
"reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject";
|
|
||||||
cleanup_service_name = "submission-header-cleanup";
|
|
||||||
};
|
|
||||||
|
|
||||||
tls_allowed = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
|
|
||||||
tls_disallow = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
|
|
||||||
in {
|
|
||||||
|
|
||||||
services.postfix = {
|
|
||||||
enable = true;
|
|
||||||
hostname = "${mail_config.fqdn}";
|
|
||||||
networksStyle = "host";
|
|
||||||
mapFiles."valias" = valiases_file;
|
|
||||||
mapFiles."vaccounts" = vaccounts_file;
|
|
||||||
sslCert = mail_config.ssl_config.cert;
|
|
||||||
sslKey = mail_config.ssl_config.key;
|
|
||||||
enableSubmission = true;
|
|
||||||
enableSubmissions = true;
|
|
||||||
virtual = lookupTableToString (mergeLookupTables [ all_valiases_postfix ]);
|
|
||||||
|
|
||||||
config = {
|
|
||||||
# Extra Config
|
|
||||||
mydestination = "";
|
|
||||||
recipient_delimiter = "+";
|
|
||||||
smtpd_banner = "${mail_config.fqdn} ESMTP NO UCE";
|
|
||||||
disable_vrfy_command = true;
|
|
||||||
message_size_limit = "20971520";
|
|
||||||
|
|
||||||
virtual_uid_maps =
|
|
||||||
"static:${toString mail_config.vmail_config.user_group_id}";
|
|
||||||
virtual_gid_maps =
|
|
||||||
"static:${toString mail_config.vmail_config.user_group_id}";
|
|
||||||
virtual_mailbox_base = "${mail_config.vmail_config.directory}";
|
|
||||||
virtual_mailbox_domains = vhosts_file;
|
|
||||||
virtual_mailbox_maps = mappedFile "valias";
|
|
||||||
virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
|
|
||||||
lmtp_destination_recipient_limit = "1";
|
|
||||||
|
|
||||||
smtpd_sasl_type = "dovecot";
|
|
||||||
smtpd_sasl_path = "/run/dovecot2/auth";
|
|
||||||
smtpd_sasl_auth_enable = true;
|
|
||||||
smtpd_relay_restrictions = [
|
|
||||||
"permit_mynetworks"
|
|
||||||
"permit_sasl_authenticated"
|
|
||||||
"reject_unauth_destination"
|
|
||||||
];
|
|
||||||
|
|
||||||
policy-spf_time_limit = "3600s";
|
|
||||||
|
|
||||||
smtpd_recipient_restrictions = [
|
|
||||||
#"check_recipient_access ${mappedFile "denied_recipients"}"
|
|
||||||
#"check_recipient_access ${mappedFile "reject_recipients"}"
|
|
||||||
"check_policy_service unix:private/policy-spf"
|
|
||||||
];
|
|
||||||
|
|
||||||
# TLS settings, inspired by https://github.com/jeaye/nix-files
|
|
||||||
# Submission by mail clients is handled in submissionOptions
|
|
||||||
smtpd_tls_security_level = "may";
|
|
||||||
|
|
||||||
# strong might suffice and is computationally less expensive
|
|
||||||
smtpd_tls_eecdh_grade = "ultra";
|
|
||||||
|
|
||||||
# Only Alow Modern TLS
|
|
||||||
smtp_tls_protocols = tls_allowed;
|
|
||||||
smtpd_tls_protocols = tls_allowed;
|
|
||||||
smtp_tls_mandatory_protocols = tls_allowed;
|
|
||||||
smtpd_tls_mandatory_protocols = tls_allowed;
|
|
||||||
|
|
||||||
# Disable Old Ciphers
|
|
||||||
smtp_tls_exclude_ciphers = tls_disallow;
|
|
||||||
smtpd_tls_exclude_ciphers = tls_disallow;
|
|
||||||
smtp_tls_mandatory_exclude_ciphers = tls_disallow;
|
|
||||||
smtpd_tls_mandatory_exclude_ciphers = tls_disallow;
|
|
||||||
|
|
||||||
smtp_tls_ciphers = "high";
|
|
||||||
smtpd_tls_ciphers = "high";
|
|
||||||
smtp_tls_mandatory_ciphers = "high";
|
|
||||||
smtpd_tls_mandatory_ciphers = "high";
|
|
||||||
|
|
||||||
tls_preempt_cipherlist = true;
|
|
||||||
|
|
||||||
smtpd_tls_auth_only = true;
|
|
||||||
smtpd_tls_loglevel = "1";
|
|
||||||
|
|
||||||
tls_random_source = "dev:/dev/urandom";
|
|
||||||
|
|
||||||
smtpd_milters = [
|
|
||||||
"unix:/run/opendkim/opendkim.sock"
|
|
||||||
"unix:/run/rspamd/rspamd-milter.sock"
|
|
||||||
];
|
|
||||||
non_smtpd_milters = [ "unix:/run/opendkim/opendkim.sock" ];
|
|
||||||
|
|
||||||
milter_protocol = "6";
|
|
||||||
milter_mail_macros =
|
|
||||||
"i {mail_addr} {client_addr} {client_name} {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}";
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
submissionOptions = submissionOptions;
|
|
||||||
submissionsOptions = submissionOptions;
|
|
||||||
|
|
||||||
masterConfig = {
|
|
||||||
"lmtp" = {
|
|
||||||
# Add headers when delivering, see http://www.postfix.org/smtp.8.html
|
|
||||||
# D => Delivered-To, O => X-Original-To, R => Return-Path
|
|
||||||
args = [ "flags=O" ];
|
|
||||||
};
|
|
||||||
"policy-spf" = {
|
|
||||||
type = "unix";
|
|
||||||
privileged = true;
|
|
||||||
chroot = false;
|
|
||||||
command = "spawn";
|
|
||||||
args = [
|
|
||||||
"user=nobody"
|
|
||||||
"argv=${pkgs.pypolicyd-spf}/bin/policyd-spf"
|
|
||||||
"${policyd-spf}"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
"submission-header-cleanup" = {
|
|
||||||
type = "unix";
|
|
||||||
private = false;
|
|
||||||
chroot = false;
|
|
||||||
maxproc = 0;
|
|
||||||
command = "cleanup";
|
|
||||||
args = [ "-o" "header_checks=pcre:${submissionHeaderCleanupRules}" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,97 +0,0 @@
|
||||||
{ config, pkgs, lib, ... }:
|
|
||||||
|
|
||||||
let
|
|
||||||
mail_config = (import ./config.nix { config = config; });
|
|
||||||
|
|
||||||
ports = (import ../../ports.nix { });
|
|
||||||
|
|
||||||
postfixCfg = config.services.postfix;
|
|
||||||
rspamdCfg = config.services.rspamd;
|
|
||||||
rspamdSocket = "rspamd.service";
|
|
||||||
in {
|
|
||||||
|
|
||||||
services.rspamd = {
|
|
||||||
enable = true;
|
|
||||||
debug = mail_config.debug_mode;
|
|
||||||
locals = {
|
|
||||||
"milter_headers.conf" = {
|
|
||||||
text = ''
|
|
||||||
extended_spam_headers = yes;
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
"redis.conf" = {
|
|
||||||
text = ''
|
|
||||||
servers = "127.0.0.1:${toString ports.rspamd-redis}";
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
"classifier-bayes.conf" = {
|
|
||||||
text = ''
|
|
||||||
cache {
|
|
||||||
backend = "redis";
|
|
||||||
}
|
|
||||||
min_learns = 5;
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
"dkim_signing.conf" = {
|
|
||||||
text = ''
|
|
||||||
# opendkim does this
|
|
||||||
enabled = false;
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
overrides = {
|
|
||||||
"milter_headers.conf" = {
|
|
||||||
text = ''
|
|
||||||
extended_spam_headers = true;
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
workers.rspamd_proxy = {
|
|
||||||
type = "rspamd_proxy";
|
|
||||||
bindSockets = [{
|
|
||||||
socket = "/run/rspamd/rspamd-milter.sock";
|
|
||||||
mode = "0664";
|
|
||||||
}];
|
|
||||||
count = 1;
|
|
||||||
extraConfig = ''
|
|
||||||
milter = yes;
|
|
||||||
timeout = 120s;
|
|
||||||
|
|
||||||
upstream "local" {
|
|
||||||
default = yes;
|
|
||||||
self_scan = yes;
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
workers.controller = {
|
|
||||||
type = "controller";
|
|
||||||
count = 1;
|
|
||||||
bindSockets = [{
|
|
||||||
socket = "/run/rspamd/worker-controller.sock";
|
|
||||||
mode = "0666";
|
|
||||||
}];
|
|
||||||
includes = [ ];
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
services.redis.servers.rspamd = {
|
|
||||||
enable = true;
|
|
||||||
port = ports.rspamd-redis;
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.rspamd = {
|
|
||||||
requires = [ "redis-rspamd.service" ];
|
|
||||||
after = [ "redis-rspamd.service" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.postfix = {
|
|
||||||
after = [ rspamdSocket ];
|
|
||||||
requires = [ rspamdSocket ];
|
|
||||||
};
|
|
||||||
|
|
||||||
users.extraUsers.${postfixCfg.user}.extraGroups = [ rspamdCfg.group ];
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
{ config, pkgs, ... }:
|
|
||||||
let
|
|
||||||
mail_config = (import ./config.nix { config = config; });
|
|
||||||
acmeRoot = "/var/lib/acme/acme-challenge";
|
|
||||||
|
|
||||||
in {
|
|
||||||
services.nginx = {
|
|
||||||
enable = true;
|
|
||||||
virtualHosts."${mail_config.fqdn}" = {
|
|
||||||
serverName = mail_config.fqdn;
|
|
||||||
serverAliases = mail_config.domains;
|
|
||||||
forceSSL = true;
|
|
||||||
enableACME = true;
|
|
||||||
acmeRoot = acmeRoot;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
security.acme.certs."${mail_config.fqdn}" = {
|
|
||||||
reloadServices = [ "postfix.service" "dovecot2.service" ];
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
{ config, ... }:
|
|
||||||
let mail_config = (import ./config.nix { config = config; });
|
|
||||||
in {
|
|
||||||
services.roundcube = {
|
|
||||||
enable = true;
|
|
||||||
hostName = "mail.owo.monster";
|
|
||||||
extraConfig = ''
|
|
||||||
$config['smtp_server'] = "tls://${mail_config.fqdn}";
|
|
||||||
$config['smtp_user'] = "%u";
|
|
||||||
$config['smtp_pass'] = "%p";
|
|
||||||
$config['plugins'] = ["managesieve"];
|
|
||||||
$config['managesieve_host'] = 'tls://${mail_config.fqdn}';
|
|
||||||
$config['session_lifetime'] = 168;
|
|
||||||
$config['product_name'] = 'Chaos Mail';
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -24,8 +24,21 @@ in {
|
||||||
systemd.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
"d /caches - storage storage"
|
"d /caches - storage storage"
|
||||||
"d /caches/main_webdav_serve - storage storage"
|
"d /caches/main_webdav_serve - storage storage"
|
||||||
|
|
||||||
|
"d /root/.config - root root"
|
||||||
|
"d /root/.config/rclone - root root"
|
||||||
|
"L /root/.config/rclone/rclone.conf - - - - ${secrets.rclone_config.path}"
|
||||||
|
|
||||||
|
"d /home/storage/.config - storage storage"
|
||||||
|
"d /home/storage/.config/rclone - storage storage"
|
||||||
|
"L /home/storage/.config/rclone/rclone.conf - - - - ${secrets.rclone_config.path}"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
home-manager.users.root = {
|
||||||
|
imports = with tree; [ home.base home.dev.small ];
|
||||||
|
home.stateVersion = "22.05";
|
||||||
|
};
|
||||||
|
|
||||||
users.groups.storage = { };
|
users.groups.storage = { };
|
||||||
users.users.storage = {
|
users.users.storage = {
|
||||||
isNormalUser = true;
|
isNormalUser = true;
|
||||||
|
@ -46,10 +59,6 @@ in {
|
||||||
VAULT_ADDR="https://vault.owo.monster" \
|
VAULT_ADDR="https://vault.owo.monster" \
|
||||||
vault login -no-print -method=userpass username=${vault_username} password=$(cat ${vault_password_file})
|
vault login -no-print -method=userpass username=${vault_username} password=$(cat ${vault_password_file})
|
||||||
/run/current-system/sw/bin/secrets-init
|
/run/current-system/sw/bin/secrets-init
|
||||||
|
|
||||||
mkdir -p ${config_dir}
|
|
||||||
rm ${config_file} || true
|
|
||||||
ln -s ${secrets.rclone_config.path} ${config_file}
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -63,7 +72,7 @@ in {
|
||||||
set -e
|
set -e
|
||||||
umount /storage -fl || true
|
umount /storage -fl || true
|
||||||
sleep 2
|
sleep 2
|
||||||
rclone --config /home/storage/.config/rclone/rclone.conf mount StorageBox: /storage --allow-non-empty
|
rclone --config ${secrets.rclone_config.path} mount StorageBox: /storage --allow-non-empty
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -74,11 +83,6 @@ in {
|
||||||
restic
|
restic
|
||||||
];
|
];
|
||||||
|
|
||||||
home-manager.users.root = {
|
|
||||||
imports = with tree; [ home.base home.dev.small ];
|
|
||||||
home.stateVersion = "22.05";
|
|
||||||
};
|
|
||||||
|
|
||||||
networking.hostName = "storage";
|
networking.hostName = "storage";
|
||||||
time.timeZone = "Europe/London";
|
time.timeZone = "Europe/London";
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
"extras/*".functor.enable = true;
|
"extras/*".functor.enable = true;
|
||||||
|
|
||||||
"hosts/*/services".functor.enable = true;
|
"hosts/*/services".functor.enable = true;
|
||||||
|
"hosts/hetzner-vm/modules/mailserver".functor.enable = true;
|
||||||
"hosts/raspberry/services/music-friend".functor.enable = true;
|
"hosts/raspberry/services/music-friend".functor.enable = true;
|
||||||
"hosts/*/home".functor.enable = true;
|
"hosts/*/home".functor.enable = true;
|
||||||
"hosts/*/profiles".functor.enable = true;
|
"hosts/*/profiles".functor.enable = true;
|
||||||
|
|
Loading…
Reference in a new issue