move mailserver to module

This commit is contained in:
Chaos 2022-11-17 12:06:16 +00:00
parent 5a3628dac0
commit ca7f4f5811
No known key found for this signature in database
36 changed files with 745 additions and 672 deletions

View file

@ -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

View 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 = {};
};
};
}

View 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 ];
});
}

View file

@ -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,35 +40,38 @@ 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 {
services.opendkim = { config = (lib.mkIf (mail_config.enable) {
enable = true; services.opendkim = {
selector = selector; enable = true;
keyPath = keyDir; selector = selector;
domains = "csl:${builtins.concatStringsSep "," domains}"; keyPath = keyDir;
configFile = pkgs.writeText "opendkim.conf" ('' domains = "csl:${builtins.concatStringsSep "," domains}";
Canonicalization relaxed/relaxed configFile = pkgs.writeText "opendkim.conf" (''
UMask 0002 Canonicalization relaxed/relaxed
Socket ${dkim.socket} UMask 0002
KeyTable file:${keyTable} Socket ${dkim.socket}
SigningTable file:${signingTable} KeyTable file:${keyTable}
'' + (lib.optionalString mail_config.debug_mode '' SigningTable file:${signingTable}
Syslog yes '' + (lib.optionalString mail_config.debug_mode ''
SyslogSuccess yes Syslog yes
LogWhy yes SyslogSuccess yes
'')); LogWhy yes
}; ''));
users.users = lib.optionalAttrs (config.services.postfix.user == "postfix") {
postfix.extraGroups = [ "${dkimGroup}" ];
};
systemd.services.opendkim = {
preStart = lib.mkForce createAllCerts;
serviceConfig = {
ExecStart =
lib.mkForce "${pkgs.opendkim}/bin/opendkim ${lib.escapeShellArgs args}";
PermissionsStartOnly = lib.mkForce false;
}; };
};
systemd.tmpfiles.rules = [ "d '${keyDir}' - ${dkimUser} ${dkimGroup} - -" ]; users.users =
lib.optionalAttrs (config.services.postfix.user == "postfix") {
postfix.extraGroups = [ "${dkimGroup}" ];
};
systemd.services.opendkim = {
preStart = lib.mkForce createAllCerts;
serviceConfig = {
ExecStart = lib.mkForce
"${pkgs.opendkim}/bin/opendkim ${lib.escapeShellArgs args}";
PermissionsStartOnly = lib.mkForce false;
};
};
systemd.tmpfiles.rules = [ "d '${keyDir}' - ${dkimUser} ${dkimGroup} - -" ];
});
} }

View 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}" ];
};
};
};
});
}

View 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 ];
});
}

View 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" ];
};
});
}

View file

@ -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,19 +43,21 @@ let
''; '';
in { in {
users.users."${v.user_group_name}" = { config = (lib.mkIf (mail_config.enable) {
name = "${v.user_group_name}"; users.users."${v.user_group_name}" = {
isSystemUser = true; name = "${v.user_group_name}";
uid = v.user_group_id; isSystemUser = true;
home = v.directory; uid = v.user_group_id;
createHome = true; home = v.directory;
group = "${v.user_group_name}"; createHome = true;
}; group = "${v.user_group_name}";
users.groups."${v.user_group_name}" = { gid = v.user_group_id; }; };
systemd.services.activate-virtual-mail-users = { users.groups."${v.user_group_name}" = { gid = v.user_group_id; };
wantedBy = [ "multi-user.target" ]; systemd.services.activate-virtual-mail-users = {
before = [ "dovecot2.service" ]; wantedBy = [ "multi-user.target" ];
serviceConfig = { ExecStart = virtualMailUsersActivationScript; }; before = [ "dovecot2.service" ];
enable = true; serviceConfig = { ExecStart = virtualMailUsersActivationScript; };
}; enable = true;
};
});
} }

View 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';
'';
};
});
}

View 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;
};
};
};
}

View file

@ -1,5 +1,3 @@
{ lib, stdenv, fetchFromGitHub }: { lib, stdenv, fetchFromGitHub }:
let let

View file

@ -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" ''

View file

@ -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
];
}

View file

@ -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";
};
}

View file

@ -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 ];
}

View file

@ -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}" ];
};
};
};
}

View file

@ -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 ];
}

View file

@ -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" ];
};
}

View file

@ -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';
'';
};
}

View file

@ -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";

View file

@ -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;