Merge branch 'pr-old-upstream-126' into 'master'

griff:my-changes

See merge request simple-nixos-mailserver/nixos-mailserver!132
This commit is contained in:
Ruben Maher 2018-06-29 12:16:19 +00:00
commit 3aecb1299d
17 changed files with 696 additions and 193 deletions

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
*.cvd filter=lfs diff=lfs merge=lfs -text

View file

@ -5,3 +5,4 @@ env:
script: script:
- nix-build tests/intern.nix - nix-build tests/intern.nix
- nix-build tests/extern.nix - nix-build tests/extern.nix
- nix-build tests/clamav.nix

View file

@ -454,6 +454,18 @@ in
''; '';
}; };
policydSPFExtraConfig = mkOption {
type = types.lines;
default = "";
example = ''
skip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1
'';
description = ''
Extra configuration options for policyd-spf. This can be use to among
other things skip spf checking for some IP addresses.
'';
};
monitoring = { monitoring = {
enable = mkEnableOption "monitoring via monit"; enable = mkEnableOption "monitoring via monit";
@ -733,8 +745,9 @@ in
./mail-server/networking.nix ./mail-server/networking.nix
./mail-server/systemd.nix ./mail-server/systemd.nix
./mail-server/dovecot.nix ./mail-server/dovecot.nix
./mail-server/opendkim.nix
./mail-server/postfix.nix ./mail-server/postfix.nix
./mail-server/rmilter.nix ./mail-server/rspamd.nix
./mail-server/nginx.nix ./mail-server/nginx.nix
./mail-server/kresd.nix ./mail-server/kresd.nix
./mail-server/post-upgrade-check.nix ./mail-server/post-upgrade-check.nix

View file

@ -1,12 +0,0 @@
{ dovecot, gawk, gnused, jq, runCommand }:
runCommand "dovecot-version" {
buildInputs = [dovecot gnused jq];
} ''
jq -n \
--arg dovecot_version "$(dovecot --version |
sed 's/\([0-9.]*\).*/\1/' |
awk -F '.' '{ print $1"."$2"."$3 }')" \
'[$dovecot_version | split("."), ["major", "minor", "patch"]]
| transpose | map( { (.[1]): .[0] | tonumber }) | add' > $out
''

View file

@ -23,11 +23,30 @@ let
maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs"; maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs";
dovecotVersion = builtins.fromJSON
(builtins.readFile (pkgs.callPackage ./dovecot-version.nix {}));
# maildir in format "/${domain}/${user}" # maildir in format "/${domain}/${user}"
dovecotMaildir = "maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}"; dovecotMaildir = "maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}";
postfixCfg = config.services.postfix;
dovecot2Cfg = config.services.dovecot2;
stateDir = "/var/lib/dovecot";
pipeBin = pkgs.stdenv.mkDerivation {
name = "pipe_bin";
src = ./dovecot/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 in
{ {
config = with cfg; lib.mkIf enable { config = with cfg; lib.mkIf enable {
@ -69,6 +88,7 @@ in
protocol imap { protocol imap {
mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser} mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser}
mail_plugins = $mail_plugins imap_sieve
} }
protocol pop3 { protocol pop3 {
@ -77,15 +97,15 @@ in
mail_access_groups = ${vmailGroupName} mail_access_groups = ${vmailGroupName}
ssl = required ssl = required
${lib.optionalString (dovecotVersion.major == 2 && dovecotVersion.minor >= 3) '' ${lib.optionalString (lib.versionAtLeast (lib.getVersion pkgs.dovecot) "2.3") ''
ssl_dh = <${certificateDirectory}/dh.pem ssl_dh = <${certificateDirectory}/dh.pem
''} ''}
service lmtp { service lmtp {
unix_listener /var/lib/postfix/queue/private/dovecot-lmtp { unix_listener dovecot-lmtp {
group = postfix group = ${postfixCfg.group}
mode = 0600 mode = 0600
user = postfix # TODO: < make variable user = ${postfixCfg.user}
} }
} }
@ -104,10 +124,10 @@ in
} }
service auth { service auth {
unix_listener /var/lib/postfix/queue/private/auth { unix_listener auth {
mode = 0660 mode = 0660
user = postfix # TODO: < make variable user = ${postfixCfg.user}
group = postfix # TODO: < make variable group = ${postfixCfg.group}
} }
} }
@ -119,14 +139,40 @@ in
} }
plugin { plugin {
sieve_plugins = sieve_imapsieve sieve_extprograms
sieve = file:/var/sieve/%u/scripts;active=/var/sieve/%u/active.sieve sieve = file:/var/sieve/%u/scripts;active=/var/sieve/%u/active.sieve
sieve_default = file:/var/sieve/%u/default.sieve sieve_default = file:/var/sieve/%u/default.sieve
sieve_default_name = default sieve_default_name = default
# From elsewhere to Spam folder
imapsieve_mailbox1_name = Junk
imapsieve_mailbox1_causes = COPY
imapsieve_mailbox1_before = file:${stateDir}/imap_sieve/report-spam.sieve
# From Spam folder to elsewhere
imapsieve_mailbox2_name = *
imapsieve_mailbox2_from = Junk
imapsieve_mailbox2_causes = COPY
imapsieve_mailbox2_before = file:${stateDir}/imap_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_autosubscribe = yes
lda_mailbox_autocreate = yes lda_mailbox_autocreate = yes
''; '';
}; };
systemd.services.dovecot2.preStart = ''
rm -rf '${stateDir}/imap_sieve'
mkdir '${stateDir}/imap_sieve'
cp -p "${./dovecot/imap_sieve}"/*.sieve '${stateDir}/imap_sieve/'
for k in "${stateDir}/imap_sieve"/*.sieve ; do
${pkgs.dovecot_pigeonhole}/bin/sievec "$k"
done
chown -R '${dovecot2Cfg.mailUser}:${dovecot2Cfg.mailGroup}' '${stateDir}/imap_sieve'
'';
}; };
} }

View file

@ -0,0 +1,15 @@
require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
if environment :matches "imap.mailbox" "*" {
set "mailbox" "${1}";
}
if string "${mailbox}" "Trash" {
stop;
}
if environment :matches "imap.user" "*" {
set "username" "${1}";
}
pipe :copy "sa-learn-ham.sh" [ "${username}" ];

View file

@ -0,0 +1,7 @@
require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
if environment :matches "imap.user" "*" {
set "username" "${1}";
}
pipe :copy "sa-learn-spam.sh" [ "${username}" ];

View file

@ -0,0 +1,3 @@
#!/bin/bash
set -o errexit
exec rspamc -h /run/rspamd/worker-controller.sock learn_ham

View file

@ -0,0 +1,3 @@
#!/bin/bash
set -o errexit
exec rspamc -h /run/rspamd/worker-controller.sock learn_spam

View file

@ -22,7 +22,7 @@ in
{ {
config = with cfg; lib.mkIf enable { config = with cfg; lib.mkIf enable {
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
dovecot opendkim openssh postfix rspamd rmilter dovecot opendkim openssh postfix rspamd
] ++ (if certificateScheme == 2 then [ openssl ] else []); ] ++ (if certificateScheme == 2 then [ openssl ] else []);
}; };
} }

90
mail-server/opendkim.nix Normal file
View file

@ -0,0 +1,90 @@
# nixos-mailserver: a simple mail server
# Copyright (C) 2017 Brian Olsen
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.mailserver;
dkimUser = config.services.opendkim.user;
dkimGroup = config.services.opendkim.group;
createDomainDkimCert = dom:
let
dkim_key = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.key";
dkim_txt = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.txt";
in
''
if [ ! -f "${dkim_key}" ] || [ ! -f "${dkim_txt}" ]
then
${pkgs.opendkim}/bin/opendkim-genkey -s "${cfg.dkimSelector}" \
-d "${dom}" \
--directory="${cfg.dkimKeyDirectory}"
mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.private" "${dkim_key}"
mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.txt" "${dkim_txt}"
echo "Generated key for domain ${dom} selector ${cfg.dkimSelector}"
fi
'';
createAllCerts = lib.concatStringsSep "\n" (map createDomainDkimCert cfg.domains);
create_dkim_cert =
''
# Create dkim dir
mkdir -p "${cfg.dkimKeyDirectory}"
chown ${dkimUser}:${dkimGroup} "${cfg.dkimKeyDirectory}"
${createAllCerts}
chown -R ${dkimUser}:${dkimGroup} "${cfg.dkimKeyDirectory}"
'';
keyTable = pkgs.writeText "opendkim-KeyTable"
(lib.concatStringsSep "\n" (lib.flip map cfg.domains
(dom: "${dom} ${dom}:${cfg.dkimSelector}:${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.key")));
signingTable = pkgs.writeText "opendkim-SigningTable"
(lib.concatStringsSep "\n" (lib.flip map cfg.domains (dom: "${dom} ${dom}")));
dkim = config.services.opendkim;
args = [ "-f" "-l" ] ++ lib.optionals (dkim.configFile != null) [ "-x" dkim.configFile ];
in
{
config = mkIf (cfg.dkimSigning && cfg.enable) {
services.opendkim = {
enable = true;
selector = cfg.dkimSelector;
domains = "csl:${builtins.concatStringsSep "," cfg.domains}";
configFile = pkgs.writeText "opendkim.conf" (''
Canonicalization relaxed/simple
UMask 0002
Socket ${dkim.socket}
KeyTable file:${keyTable}
SigningTable file:${signingTable}
'' + (lib.optionalString cfg.debug ''
Syslog yes
SyslogSuccess yes
LogWhy yes
''));
};
users.users = optionalAttrs (config.services.postfix.user == "postfix") {
postfix.extraGroups = [ "${config.services.opendkim.group}" ];
};
systemd.services.opendkim = {
preStart = create_dkim_cert;
serviceConfig.ExecStart = lib.mkForce "${pkgs.opendkim}/bin/opendkim ${escapeShellArgs args}";
};
};
}

View file

@ -90,6 +90,19 @@ let
/^Message-ID:\s+<(.*?)@.*?>/ REPLACE Message-ID: <$1@${cfg.fqdn}> /^Message-ID:\s+<(.*?)@.*?>/ REPLACE Message-ID: <$1@${cfg.fqdn}>
''); '');
inetSocket = addr: port: "inet:[${toString port}@${addr}]";
unixSocket = sock: "unix:${sock}";
smtpdMilters =
(lib.optional cfg.dkimSigning "unix:/run/opendkim/opendkim.sock")
++ [ "unix:/run/rspamd/rspamd-milter.sock" ];
policyd-spf = pkgs.writeText "policyd-spf.conf" (
cfg.policydSPFExtraConfig
+ (lib.optionalString cfg.debug ''
debugLevel = 4
''));
in in
{ {
config = with cfg; lib.mkIf enable { config = with cfg; lib.mkIf enable {
@ -121,16 +134,21 @@ in
virtual_mailbox_domains = ${vhosts_file} virtual_mailbox_domains = ${vhosts_file}
virtual_mailbox_maps = hash:/var/lib/postfix/conf/valias virtual_mailbox_maps = hash:/var/lib/postfix/conf/valias
virtual_alias_maps = hash:/var/lib/postfix/conf/valias virtual_alias_maps = hash:/var/lib/postfix/conf/valias
virtual_transport = lmtp:unix:private/dovecot-lmtp virtual_transport = lmtp:unix:/run/dovecot2/dovecot-lmtp
# sasl with dovecot # sasl with dovecot
smtpd_sasl_type = dovecot smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth smtpd_sasl_path = /run/dovecot2/auth
smtpd_sasl_auth_enable = yes smtpd_sasl_auth_enable = yes
smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination
# reject selected recipients, quota policy-spf_time_limit = 3600s
smtpd_recipient_restrictions = check_recipient_access hash:/var/lib/postfix/conf/reject_recipients, check_policy_service inet:localhost:12340
# quota and spf checking
smtpd_recipient_restrictions =
check_recipient_access hash:/var/lib/postfix/conf/reject_recipients,
check_policy_service inet:localhost:12340,
check_policy_service unix:private/policy-spf
# TLS settings, inspired by https://github.com/jeaye/nix-files # TLS settings, inspired by https://github.com/jeaye/nix-files
# Submission by mail clients is handled in submissionOptions # Submission by mail clients is handled in submissionOptions
@ -151,6 +169,11 @@ in
# Configure a non blocking source of randomness # Configure a non blocking source of randomness
tls_random_source = dev:/dev/urandom tls_random_source = dev:/dev/urandom
smtpd_milters = ${lib.concatStringsSep "," smtpdMilters}
${lib.optionalString cfg.dkimSigning "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 =
@ -158,7 +181,7 @@ in
smtpd_tls_security_level = "encrypt"; smtpd_tls_security_level = "encrypt";
smtpd_sasl_auth_enable = "yes"; smtpd_sasl_auth_enable = "yes";
smtpd_sasl_type = "dovecot"; smtpd_sasl_type = "dovecot";
smtpd_sasl_path = "private/auth"; smtpd_sasl_path = "/run/dovecot2/auth";
smtpd_sasl_security_options = "noanonymous"; smtpd_sasl_security_options = "noanonymous";
smtpd_sasl_local_domain = "$myhostname"; smtpd_sasl_local_domain = "$myhostname";
smtpd_client_restrictions = "permit_sasl_authenticated,reject"; smtpd_client_restrictions = "permit_sasl_authenticated,reject";
@ -167,11 +190,23 @@ in
smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject"; smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject";
cleanup_service_name = "submission-header-cleanup"; cleanup_service_name = "submission-header-cleanup";
}; };
masterConfig = {
extraMasterConf = '' "policy-spf" = {
submission-header-cleanup unix n - n - 0 cleanup type = "unix";
-o header_checks=pcre:${submissionHeaderCleanupRules} 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,74 +0,0 @@
# nixos-mailserver: a simple mail server
# Copyright (C) 2016-2018 Robin Raymond
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }:
let
cfg = config.mailserver;
clamav = if cfg.virusScanning
then
''
clamav {
servers = /var/run/clamav/clamd.ctl;
};
''
else "";
dkim = if cfg.dkimSigning
# Note: domain = "*"; causes Rmilter to try to search key in the key path
# as keypath/domain.selector.key for any domain.
then
''
dkim {
domain {
key = "${cfg.dkimKeyDirectory}";
domain = "*";
selector = "${cfg.dkimSelector}";
};
sign_alg = sha256;
auth_only = yes;
header_canon = relaxed;
}
''
else "";
in
{
config = with cfg; lib.mkIf enable {
services.rspamd = {
enable = true;
};
services.rmilter = {
inherit debug;
enable = true;
postfix.enable = true;
rspamd = {
enable = true;
extraConfig = "extended_spam_headers = yes;";
};
extraConfig =
''
use_redis = true;
max_size = 20M;
${clamav}
${dkim}
'';
};
};
}

88
mail-server/rspamd.nix Normal file
View file

@ -0,0 +1,88 @@
# nixos-mailserver: a simple mail server
# Copyright (C) 2016-2018 Robin Raymond
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }:
let
cfg = config.mailserver;
postfixCfg = config.services.postfix;
rspamdCfg = config.services.rspamd;
rspamdSocket = if rspamdCfg.socketActivation
then "rspamd-rspamd_proxy-1.socket"
else "rspamd.service";
in
{
config = with cfg; lib.mkIf enable {
services.rspamd = {
enable = true;
socketActivation = false;
extraConfig = ''
extended_spam_headers = yes;
'' + (lib.optionalString cfg.virusScanning ''
antivirus {
clamav {
action = "reject";
symbol = "CLAM_VIRUS";
type = "clamav";
log_clean = true;
servers = "/run/clamav/clamd.ctl";
}
}
'');
workers.rspamd_proxy = {
type = "proxy";
bindSockets = [{
socket = "/run/rspamd/rspamd-milter.sock";
mode = "0664";
}];
count = 1; # Do not spawn too many processes of this type
extraConfig = ''
milter = yes; # Enable milter mode
timeout = 120s; # Needed for Milter usually
upstream "local" {
default = yes; # Self-scan upstreams are always default
self_scan = yes; # Enable self-scan
}
'';
};
workers.controller = {
type = "controller";
count = 1;
bindSockets = [{
socket = "/run/rspamd/worker-controller.sock";
mode = "0666";
}];
includes = [];
};
};
systemd.services.rspamd = {
requires = (lib.optional cfg.virusScanning "clamav-daemon.service");
after = (lib.optional cfg.virusScanning "clamav-daemon.service");
};
systemd.services.postfix = {
after = [ rspamdSocket ];
requires = [ rspamdSocket ];
};
users.extraUsers.${postfixCfg.user}.extraGroups = [ rspamdCfg.group ];
};
}

View file

@ -19,99 +19,83 @@
let let
cfg = config.mailserver; cfg = config.mailserver;
create_certificate = if cfg.certificateScheme == 2 then
''
# Create certificates if they do not exist yet
dir="${cfg.certificateDirectory}"
fqdn="${cfg.fqdn}"
case $fqdn in /*) fqdn=$(cat "$fqdn");; esac
key="''${dir}/key-${cfg.fqdn}.pem";
cert="''${dir}/cert-${cfg.fqdn}.pem";
if [ ! -f "''${key}" ] || [ ! -f "''${cert}" ]
then
mkdir -p "${cfg.certificateDirectory}"
(umask 077; "${pkgs.openssl}/bin/openssl" genrsa -out "''${key}" 2048) &&
"${pkgs.openssl}/bin/openssl" req -new -key "''${key}" -x509 -subj "/CN=''${fqdn}" \
-days 3650 -out "''${cert}"
fi
''
else "";
createDhParameterFile = createDhParameterFile =
'' lib.optionalString (lib.versionAtLeast (lib.getVersion pkgs.dovecot) "2.3")
# Create a dh parameter file ''
if [ ! -s "${cfg.certificateDirectory}/dh.pem" ] # Create a dh parameter file
then if [ ! -s "${cfg.certificateDirectory}/dh.pem" ]
mkdir -p "${cfg.certificateDirectory}" then
${pkgs.openssl}/bin/openssl \ mkdir -p "${cfg.certificateDirectory}"
dhparam ${builtins.toString cfg.dhParamBitLength} \ ${pkgs.openssl}/bin/openssl \
> "${cfg.certificateDirectory}/dh.pem" dhparam ${builtins.toString cfg.dhParamBitLength} \
fi > "${cfg.certificateDirectory}/dh.pem"
''; fi
'';
createDomainDkimCert = dom: preliminarySelfsigned = config.security.acme.preliminarySelfsigned;
let acmeWantsTarget = [ "acme-certificates.target" ]
dkim_key = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.key"; ++ (lib.optional preliminarySelfsigned "acme-selfsigned-certificates.target");
dkim_txt = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.txt"; acmeAfterTarget = if preliminarySelfsigned
in then [ "acme-selfsigned-certificates.target" ]
'' else [ "acme-certificates.target" ];
if [ ! -f "${dkim_key}" ] || [ ! -f "${dkim_txt}" ]
then
${pkgs.opendkim}/bin/opendkim-genkey -s "${cfg.dkimSelector}" \
-d "${dom}" \
--directory="${cfg.dkimKeyDirectory}"
mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.private" "${dkim_key}"
mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.txt" "${dkim_txt}"
fi
'';
createAllCerts = lib.concatStringsSep "\n" (map createDomainDkimCert cfg.domains);
create_dkim_cert =
''
# Create dkim dir
mkdir -p "${cfg.dkimKeyDirectory}"
chown rmilter:rmilter "${cfg.dkimKeyDirectory}"
${createAllCerts}
chown -R rmilter:rmilter "${cfg.dkimKeyDirectory}"
'';
in in
{ {
config = with cfg; lib.mkIf enable { config = with cfg; lib.mkIf enable {
# Make sure postfix gets started first, so that the certificates are in place # Add target for when certificates are available
systemd.services.dovecot2.after = [ "postfix.service" ]; systemd.targets."mailserver-certificates" = {
wants = lib.mkIf (cfg.certificateScheme == 3) acmeWantsTarget;
after = lib.mkIf (cfg.certificateScheme == 3) acmeAfterTarget;
};
# Create certificates and maildir folder # Create self signed certificate
systemd.services.postfix = { systemd.services.mailserver-selfsigned-certificate = lib.mkIf (cfg.certificateScheme == 2) {
after = (if (certificateScheme == 3) then [ "nginx.service" ] else []); wantedBy = [ "mailserver-certificates.target" ];
preStart = after = [ "local-fs.target" ];
'' before = [ "mailserver-certificates.target" ];
# Create mail directory and set permissions. See script = ''
# <http://wiki2.dovecot.org/SharedMailboxes/Permissions>. # Create certificates if they do not exist yet
mkdir -p "${mailDirectory}" dir="${cfg.certificateDirectory}"
chgrp "${vmailGroupName}" "${mailDirectory}" fqdn="${cfg.fqdn}"
chmod 02770 "${mailDirectory}" case $fqdn in /*) fqdn=$(cat "$fqdn");; esac
key="''${dir}/key-${cfg.fqdn}.pem";
cert="''${dir}/cert-${cfg.fqdn}.pem";
${create_certificate} if [ ! -f "''${key}" ] || [ ! -f "''${cert}" ]
then
mkdir -p "${cfg.certificateDirectory}"
(umask 077; "${pkgs.openssl}/bin/openssl" genrsa -out "''${key}" 2048) &&
"${pkgs.openssl}/bin/openssl" req -new -key "''${key}" -x509 -subj "/CN=''${fqdn}" \
-days 3650 -out "''${cert}"
fi
'';
serviceConfig = {
Type = "oneshot";
PrivateTmp = true;
};
};
${let # Create maildir folder and dh parameters before dovecot startup
dovecotVersion = builtins.fromJSON systemd.services.dovecot2 = {
(builtins.readFile (pkgs.callPackage ./dovecot-version.nix {})); after = [ "mailserver-certificates.target" ];
in lib.optionalString wants = [ "mailserver-certificates.target" ];
(dovecotVersion.major == 2 && dovecotVersion.minor >= 3) preStart = ''
createDhParameterFile} # Create mail directory and set permissions. See
# <http://wiki2.dovecot.org/SharedMailboxes/Permissions>.
mkdir -p "${mailDirectory}"
chgrp "${vmailGroupName}" "${mailDirectory}"
chmod 02770 "${mailDirectory}"
${createDhParameterFile}
''; '';
}; };
# Create dkim certificates # Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work
systemd.services.rmilter = { systemd.services.postfix = {
requires = [ "rmilter.socket" ]; after = [ "dovecot2.service" "mailserver-certificates.target" ]
after = [ "rmilter.socket" ]; ++ (lib.optional cfg.dkimSigning "opendkim.service");
preStart = wants = [ "mailserver-certificates.target" ];
'' requires = [ "dovecot2.service" ]
${create_dkim_cert} ++ (lib.optional cfg.dkimSigning "opendkim.service");
'';
}; };
}; };
} }

228
tests/clamav.nix Normal file
View file

@ -0,0 +1,228 @@
# nixos-mailserver: a simple mail server
# Copyright (C) 2016-2018 Robin Raymond
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
import <nixpkgs/nixos/tests/make-test.nix> {
nodes = {
server = { config, pkgs, lib, ... }:
let
clamav-db = pkgs.srcOnly {
name = "ClamAV-db";
src = pkgs.fetchurl {
url = "https://files.griff.name/ClamAV-db.tar";
sha256 = "eecad99f4c071d216bd91565f84c0d90a1f93e5e3e22d8f3087686ba3bd219e7";
};
};
in
{
imports = [
../default.nix
];
virtualisation.memorySize = 1500;
services.rsyslogd = {
enable = true;
defaultConfig = ''
*.* /dev/console
'';
};
services.clamav.updater.enable = lib.mkForce false;
systemd.services.old-clam = {
before = [ "clamav-daemon.service" ];
requiredBy = [ "clamav-daemon.service" ];
description = "ClamAV virus database";
preStart = ''
mkdir -m 0755 -p /var/lib/clamav
chown clamav:clamav /var/lib/clamav
'';
script = ''
cp ${clamav-db}/bytecode.cvd /var/lib/clamav/
cp ${clamav-db}/main.cvd /var/lib/clamav/
cp ${clamav-db}/daily.cvd /var/lib/clamav/
chown clamav:clamav /var/lib/clamav/*
'';
serviceConfig = {
Type = "oneshot";
PrivateTmp = "yes";
PrivateDevices = "yes";
};
};
mailserver = {
enable = true;
debug = true;
fqdn = "mail.example.com";
domains = [ "example.com" "example2.com" ];
dhParamBitLength = 512;
virusScanning = true;
loginAccounts = {
"user1@example.com" = {
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
aliases = [ "postmaster@example.com" ];
catchAll = [ "example.com" ];
};
"user@example2.com" = {
hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
};
};
enableImap = true;
};
environment.etc = {
"root/eicar.com.txt".text = "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
};
};
client = { nodes, config, pkgs, ... }: let
serverIP = nodes.server.config.networking.primaryIPAddress;
clientIP = nodes.client.config.networking.primaryIPAddress;
grep-ip = pkgs.writeScriptBin "grep-ip" ''
#!${pkgs.stdenv.shell}
echo grep '${clientIP}' "$@" >&2
exec grep '${clientIP}' "$@"
'';
in {
environment.systemPackages = with pkgs; [
fetchmail msmtp procmail findutils grep-ip
];
environment.etc = {
"root/.fetchmailrc" = {
text = ''
poll ${serverIP} with proto IMAP
user 'user1@example.com' there with password 'user1' is 'root' here
mda procmail
'';
mode = "0700";
};
"root/.procmailrc" = {
text = "DEFAULT=$HOME/mail";
};
"root/.msmtprc" = {
text = ''
account test2
host ${serverIP}
port 587
from user@example2.com
user user@example2.com
password user2
'';
};
"root/virus-email".text = ''
From: User2 <user@example2.com>
Content-Type: multipart/mixed;
boundary="Apple-Mail=_2689C63E-FD18-4E4D-8822-54797BDA9607"
Mime-Version: 1.0 (Mac OS X Mail 11.3 \(3445.6.18\))
Subject: Testy McTest
Message-Id: <94550DD9-1FF1-4ED1-9F09-8812FF2E59AA@example.com>
Date: Sat, 12 May 2018 14:15:44 +0200
To: User1 <user1@example.com>
X-Mailer: Apple Mail (2.3445.6.18)
--Apple-Mail=_2689C63E-FD18-4E4D-8822-54797BDA9607
Content-Transfer-Encoding: 7bit
Content-Type: text/plain;
charset=us-ascii
Hello
I have attached a dangerous virus.
Mfg.
User2
--Apple-Mail=_2689C63E-FD18-4E4D-8822-54797BDA9607
Content-Disposition: attachment;
filename=eicar.com.txt
Content-Type: text/plain;
x-unix-mode=0644;
name="eicar.com.txt"
Content-Transfer-Encoding: 7bit
X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*
--Apple-Mail=_2689C63E-FD18-4E4D-8822-54797BDA9607--
'';
"root/email2".text = ''
From: User <user@example2.com>
To: User1 <user1@example.com>
Cc:
Bcc:
Subject: This is a test Email from user@example2.com to user1
Reply-To:
Hello User1,
how are you doing today?
XOXO User1
'';
};
};
};
testScript =
''
startAll;
$server->waitForUnit("multi-user.target");
$client->waitForUnit("multi-user.target");
$client->execute("cp -p /etc/root/.* ~/");
$client->succeed("mkdir -p ~/mail");
$client->succeed("ls -la ~/ >&2");
$client->succeed("cat ~/.fetchmailrc >&2");
$client->succeed("cat ~/.procmailrc >&2");
$client->succeed("cat ~/.msmtprc >&2");
# fetchmail returns EXIT_CODE 1 when no new mail
$client->succeed("fetchmail -v || [ \$? -eq 1 ] >&2");
# Verify that mail can be sent and received before testing virus scanner
$client->execute("rm ~/mail/*");
$client->succeed("msmtp -a test2 --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email2 >&2");
# give the mail server some time to process the mail
$server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]');
$client->execute("rm ~/mail/*");
# fetchmail returns EXIT_CODE 0 when it retrieves mail
$client->succeed("fetchmail -v >&2");
$client->execute("rm ~/mail/*");
subtest "virus scan file", sub {
$server->fail("clamscan --follow-file-symlinks=2 -r /etc/root/ >&2");
};
subtest "virus scanner", sub {
$client->fail("msmtp -a test2 --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/virus-email >&2");
# give the mail server some time to process the mail
$server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]');
};
subtest "no warnings or errors", sub {
$server->fail("journalctl -u postfix | grep -i error >&2");
$server->fail("journalctl -u postfix | grep -i warning >&2");
$server->fail("journalctl -u dovecot2 | grep -i error >&2");
$server->fail("journalctl -u dovecot2 | grep -i warning >&2");
};
'';
}

View file

@ -23,6 +23,14 @@ import <nixpkgs/nixos/tests/make-test.nix> {
../default.nix ../default.nix
]; ];
services.rsyslogd = {
enable = true;
defaultConfig = ''
*.* /dev/console
'';
};
mailserver = { mailserver = {
enable = true; enable = true;
debug = true; debug = true;
@ -56,6 +64,7 @@ import <nixpkgs/nixos/tests/make-test.nix> {
}; };
enableImap = true; enableImap = true;
enableImapSsl = true;
}; };
}; };
client = { nodes, config, pkgs, ... }: let client = { nodes, config, pkgs, ... }: let
@ -71,9 +80,63 @@ import <nixpkgs/nixos/tests/make-test.nix> {
echo grep '^Message-ID:.*@mail.example.com>$' "$@" >&2 echo grep '^Message-ID:.*@mail.example.com>$' "$@" >&2
exec grep '^Message-ID:.*@mail.example.com>$' "$@" exec grep '^Message-ID:.*@mail.example.com>$' "$@"
''; '';
test-imap-spam = pkgs.writeScriptBin "imap-mark-spam" ''
#!${pkgs.python3.interpreter}
import imaplib
with imaplib.IMAP4_SSL('${serverIP}') as imap:
imap.login('user1@example.com', 'user1')
imap.select()
status, [response] = imap.search(None, 'ALL')
msg_ids = response.decode("utf-8").split(' ')
print(msg_ids)
assert status == 'OK'
assert len(msg_ids) == 1
imap.copy(','.join(msg_ids), 'Junk')
for num in msg_ids:
imap.store(num, '+FLAGS', '\\Deleted')
imap.expunge()
imap.select('Junk')
status, [response] = imap.search(None, 'ALL')
msg_ids = response.decode("utf-8").split(' ')
print(msg_ids)
assert status == 'OK'
assert len(msg_ids) == 1
imap.close()
'';
test-imap-ham = pkgs.writeScriptBin "imap-mark-ham" ''
#!${pkgs.python3.interpreter}
import imaplib
with imaplib.IMAP4_SSL('${serverIP}') as imap:
imap.login('user1@example.com', 'user1')
imap.select('Junk')
status, [response] = imap.search(None, 'ALL')
msg_ids = response.decode("utf-8").split(' ')
print(msg_ids)
assert status == 'OK'
assert len(msg_ids) == 1
imap.copy(','.join(msg_ids), 'INBOX')
for num in msg_ids:
imap.store(num, '+FLAGS', '\\Deleted')
imap.expunge()
imap.select('INBOX')
status, [response] = imap.search(None, 'ALL')
msg_ids = response.decode("utf-8").split(' ')
print(msg_ids)
assert status == 'OK'
assert len(msg_ids) == 1
imap.close()
'';
in { in {
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
fetchmail msmtp procmail findutils grep-ip check-mail-id fetchmail msmtp procmail findutils grep-ip check-mail-id test-imap-spam test-imap-ham
]; ];
environment.etc = { environment.etc = {
"root/.fetchmailrc" = { "root/.fetchmailrc" = {
@ -87,7 +150,7 @@ import <nixpkgs/nixos/tests/make-test.nix> {
"root/.fetchmailRcLowQuota" = { "root/.fetchmailRcLowQuota" = {
text = '' text = ''
poll ${serverIP} with proto IMAP poll ${serverIP} with proto IMAP
user 'lowquota\@example.com' there with password 'user1' is 'root' here user 'lowquota@example.com' there with password 'user2' is 'root' here
mda procmail mda procmail
''; '';
mode = "0700"; mode = "0700";
@ -217,7 +280,7 @@ import <nixpkgs/nixos/tests/make-test.nix> {
$client->waitForUnit("multi-user.target"); $client->waitForUnit("multi-user.target");
$client->execute("cp -p /etc/root/.* ~/"); $client->execute("cp -p /etc/root/.* ~/");
$client->succeed("mkdir ~/mail"); $client->succeed("mkdir -p ~/mail");
$client->succeed("ls -la ~/ >&2"); $client->succeed("ls -la ~/ >&2");
$client->succeed("cat ~/.fetchmailrc >&2"); $client->succeed("cat ~/.fetchmailrc >&2");
$client->succeed("cat ~/.procmailrc >&2"); $client->succeed("cat ~/.procmailrc >&2");
@ -317,6 +380,18 @@ import <nixpkgs/nixos/tests/make-test.nix> {
}; };
subtest "imap sieve junk trainer", sub {
# send email from user2 to user1
$client->succeed("msmtp -a test --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email1 >&2");
# give the mail server some time to process the mail
$server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]');
$client->succeed("imap-mark-spam >&2");
$server->waitUntilSucceeds("journalctl -u dovecot2 | grep -i sa-learn-spam.sh >&2");
$client->succeed("imap-mark-ham >&2");
$server->waitUntilSucceeds("journalctl -u dovecot2 | grep -i sa-learn-ham.sh >&2");
};
subtest "no warnings or errors", sub { subtest "no warnings or errors", sub {
$server->fail("journalctl -u postfix | grep -i error >&2"); $server->fail("journalctl -u postfix | grep -i error >&2");
$server->fail("journalctl -u postfix | grep -i warning >&2"); $server->fail("journalctl -u postfix | grep -i warning >&2");