Self-host your own digital island

initial

Ryan Gibb 41b2b90e

+27
flake.lock
···
+
{
+
"nodes": {
+
"nixpkgs": {
+
"locked": {
+
"lastModified": 1667231093,
+
"narHash": "sha256-RERXruzBEBuf0c7OfZeX1hxEKB+PTCUNxWeB6C1jd8Y=",
+
"owner": "nixos",
+
"repo": "nixpkgs",
+
"rev": "d40fea9aeb8840fea0d377baa4b38e39b9582458",
+
"type": "github"
+
},
+
"original": {
+
"owner": "nixos",
+
"ref": "nixos-unstable",
+
"repo": "nixpkgs",
+
"type": "github"
+
}
+
},
+
"root": {
+
"inputs": {
+
"nixpkgs": "nixpkgs"
+
}
+
}
+
},
+
"root": "root",
+
"version": 7
+
}
+11
flake.nix
···
+
{
+
inputs = {
+
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
+
};
+
+
outputs = { self, nixpkgs, ... }@inputs: {
+
nixosModules.default = {
+
imports = [ ./modules/default.nix ];
+
};
+
};
+
}
+14
modules/default.nix
···
+
{
+
imports = [
+
./dns/default.nix
+
./mailserver/default.nix
+
./hosting/default.nix
+
./hosting/mastodon.nix
+
./hosting/mailserver.nix
+
./hosting/gitea.nix
+
./hosting/dns.nix
+
./hosting/matrix.nix
+
./wireguard/server.nix
+
./wireguard/default.nix
+
];
+
}
+17
modules/dns/bind.nix
···
+
{ pkgs, config, lib, ... }:
+
+
let cfg = config.dns; in {
+
services.bind = lib.mkIf (cfg.enable && cfg.server == "bind") {
+
enable = true;
+
# recursive resolver
+
# cacheNetworks = [ "0.0.0.0/0" ];
+
zones."${config.networking.domain}" = {
+
master = true;
+
file = import ./zonefile.nix { inherit pkgs config lib; };
+
# axfr zone transfer
+
slaves = [
+
"127.0.0.1"
+
];
+
};
+
};
+
}
+75
modules/dns/default.nix
···
+
{ pkgs, config, lib, ... }:
+
+
with lib;
+
+
{
+
imports = [ ./bind.nix ];
+
+
options.dns = {
+
enable = lib.mkEnableOption "DNS server";
+
server = mkOption {
+
type = types.enum [ "bind" ];
+
default = "bind";
+
};
+
domain = mkOption {
+
type = types.str;
+
default = config.networking.domain;
+
};
+
ttl = mkOption {
+
type = types.int;
+
default = 3600; # 1hr
+
};
+
soa = {
+
ns = mkOption {
+
type = types.str;
+
default = "ns1";
+
};
+
email = mkOption {
+
type = types.str;
+
default = "dns";
+
};
+
# TODO auto increment
+
serial = mkOption {
+
type = types.int;
+
};
+
refresh = mkOption {
+
type = types.int;
+
default = 3600; # 1hr
+
};
+
retry = mkOption {
+
type = types.int;
+
default = 15; # 15m
+
};
+
expire = mkOption {
+
type = types.int;
+
default = 1814400; # 21d
+
};
+
negativeCacheTtl = mkOption {
+
type = types.int;
+
default = 3600; # 1hr
+
};
+
};
+
records =
+
let recordOpts = {
+
options = {
+
name = mkOption {
+
type = types.str;
+
};
+
ttl = mkOption {
+
type = with types; nullOr int;
+
default = null;
+
};
+
type = mkOption {
+
type = types.str;
+
};
+
data = mkOption {
+
type = types.str;
+
};
+
};
+
};
+
in mkOption {
+
type = with types; listOf (submodule recordOpts);
+
default = [ ];
+
};
+
};
+
}
+20
modules/dns/zonefile.nix
···
+
{ pkgs, config, lib, ... }:
+
+
let cfg = config.dns; in pkgs.writeTextFile {
+
name = "zonefile";
+
text = ''
+
$ORIGIN ${cfg.domain}.
+
$TTL ${builtins.toString cfg.ttl}
+
@ IN SOA ${cfg.soa.ns} ${cfg.soa.email} (
+
${builtins.toString cfg.soa.serial}
+
${builtins.toString cfg.soa.refresh}
+
${builtins.toString cfg.soa.retry}
+
${builtins.toString cfg.soa.expire}
+
${builtins.toString cfg.soa.negativeCacheTtl}
+
)
+
${
+
lib.strings.concatStringsSep "\n"
+
(builtins.map (rr: "${rr.name} IN ${builtins.toString rr.ttl} ${rr.type} ${rr.data}") cfg.records)
+
}
+
'';
+
}
+15
modules/hosting/default.nix
···
+
{ lib, ... }:
+
+
{
+
options = {
+
hosting.username = lib.mkOption {
+
type = lib.types.str;
+
};
+
hosting.serverIpv4 = lib.mkOption {
+
type = lib.types.str;
+
};
+
hosting.serverIpv6 = lib.mkOption {
+
type = lib.types.str;
+
};
+
};
+
}
+58
modules/hosting/dns.nix
···
+
{ config, lib, ... }:
+
+
let cfg = config.hosting; in
+
{
+
options.hosting.dns = lib.mkEnableOption "dns";
+
+
config.dns = lib.mkIf cfg.dns {
+
enable = true;
+
soa.serial = 2018011623;
+
records = builtins.concatMap (ns: [
+
{
+
name = "@";
+
type = "NS";
+
data = ns;
+
}
+
{
+
name = ns;
+
type = "A";
+
data = config.hosting.serverIpv4;
+
}
+
]) [ "ns1" "ns2" ] ++
+
[
+
{
+
name = "www";
+
type = "CNAME";
+
data = "@";
+
}
+
+
{
+
name = "@";
+
type = "A";
+
data = config.hosting.serverIpv4;
+
}
+
{
+
name = "@";
+
type = "AAAA";
+
data = config.hosting.serverIpv6;
+
}
+
+
{
+
name = "vps";
+
type = "A";
+
data = config.hosting.serverIpv4;
+
}
+
{
+
name = "vps";
+
type = "AAAA";
+
data = config.hosting.serverIpv6;
+
}
+
+
{
+
name = "@";
+
type = "LOC";
+
data = "52 12 40.4 N 0 5 31.9 E 22m 10m 10m 10m";
+
}
+
];
+
};
+
}
+88
modules/hosting/gitea.nix
···
+
{ pkgs, config, lib, ... }:
+
+
let
+
cfg = config.hosting;
+
domain = config.networking.domain;
+
in {
+
options.hosting.gitea = lib.mkEnableOption "gitea";
+
+
config = lib.mkIf cfg.gitea {
+
services.nginx = {
+
recommendedProxySettings = true;
+
virtualHosts."git.${domain}" = {
+
enableACME = true;
+
forceSSL = true;
+
locations."/" = {
+
proxyPass = "http://localhost:${builtins.toString config.services.gitea.httpPort}/";
+
};
+
};
+
};
+
+
users.users.git = {
+
description = "Git Service";
+
home = config.services.gitea.stateDir;
+
useDefaultShell = true;
+
group = "gitea";
+
isSystemUser = true;
+
};
+
+
services.gitea = {
+
enable = true;
+
user = "git";
+
appName = "git | ${domain}";
+
domain = "git.${domain}";
+
rootUrl = "https://git.${domain}/";
+
mailerPasswordFile = "${config.custom.secretsDir}/email-pswd-unhashed";
+
settings = {
+
mailer = {
+
ENABLED = true;
+
FROM = "git@${domain}";
+
MAILER_TYPE = "smtp";
+
HOST = "mail.${domain}:465";
+
USER = "misc@${domain}";
+
IS_TLS_ENABLED = true;
+
};
+
repository.DEFAULT_BRANCH = "main";
+
service.DISABLE_REGISTRATION = true;
+
};
+
database = {
+
type = "postgres";
+
passwordFile = "${config.custom.secretsDir}/gitea-db";
+
user = "git";
+
name = "git";
+
#createDatabase = true;
+
#socket = "/run/postgresql";
+
};
+
#httpPort = 3000;
+
#stateDir = "/var/lib/gitea";
+
};
+
+
# https://github.com/NixOS/nixpkgs/issues/103446
+
systemd.services.gitea.serviceConfig = {
+
ReadWritePaths = [ "/var/lib/postfix/queue/maildrop" ];
+
NoNewPrivileges = lib.mkForce false;
+
PrivateDevices = lib.mkForce false;
+
PrivateUsers = lib.mkForce false;
+
ProtectHostname = lib.mkForce false;
+
ProtectClock = lib.mkForce false;
+
ProtectKernelTunables = lib.mkForce false;
+
ProtectKernelModules = lib.mkForce false;
+
ProtectKernelLogs = lib.mkForce false;
+
RestrictAddressFamilies = lib.mkForce [ ];
+
LockPersonality = lib.mkForce false;
+
MemoryDenyWriteExecute = lib.mkForce false;
+
RestrictRealtime = lib.mkForce false;
+
RestrictSUIDSGID = lib.mkForce false;
+
SystemCallArchitectures = lib.mkForce "";
+
SystemCallFilter = lib.mkForce [];
+
};
+
+
dns.records = [
+
{
+
name = "git";
+
type = "CNAME";
+
data = "vps";
+
}
+
];
+
};
+
}
+81
modules/hosting/mailserver.nix
···
+
{ config, lib, ... }:
+
+
let
+
cfg = config.hosting;
+
domain = config.networking.domain;
+
in {
+
options.hosting.mailserver = lib.mkEnableOption "mailserver";
+
+
config = lib.mkIf cfg.mailserver {
+
mailserver = {
+
enable = true;
+
fqdn = "mail.${domain}";
+
domains = [ "${domain}" ];
+
+
# A list of all login accounts. To create the password hashes, use
+
# nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2
+
loginAccounts = {
+
"${config.hosting.username}@${domain}" = {
+
hashedPasswordFile = "${config.custom.secretsDir}/email-pswd";
+
aliases = [
+
"dns@${domain}"
+
"postmaster@${domain}"
+
];
+
};
+
"misc@${domain}" = {
+
hashedPasswordFile = "${config.custom.secretsDir}/email-pswd";
+
aliases = [
+
"git@${domain}"
+
"mastodon@${domain}"
+
];
+
catchAll = [ "${domain}" ];
+
};
+
};
+
+
# Use Let's Encrypt certificates. Note that this needs to set up a stripped
+
# down nginx and opens port 80.
+
certificateScheme = 3;
+
+
localDnsResolver = false;
+
};
+
+
services.nginx.virtualHosts."${config.mailserver.fqdn}".extraConfig = ''
+
return 301 $scheme://${domain}$request_uri;
+
'';
+
+
dns.records = [
+
{
+
name = "mail";
+
type = "A";
+
data = config.hosting.serverIpv4;
+
}
+
{
+
name = "mail";
+
type = "AAAA";
+
data = config.hosting.serverIpv6;
+
}
+
{
+
name = "@";
+
type = "MX";
+
data = "10 mail";
+
}
+
{
+
name = "@";
+
type = "TXT";
+
data = "\"v=spf1 a:mail.${config.networking.domain} -all\"";
+
}
+
{
+
name = "mail._domainkey";
+
ttl = 10800;
+
type = "TXT";
+
data = "\"v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6YmYYvoFF7VqtGcozpVQa78aaGgZdvc5ZIHqzmkKdCBEyDF2FRbCEK4s2AlC8hhc8O4mSSe3S4AzEhlRgHXbU22GBaUZ3s2WHS8JJwZvWeTjsbXQwjN/U7xpkqXPHLH9IVfOJbHlp4HQmCAXw4NaypgkkxIGK0jaZHm2j6/1izQIDAQAB\"";
+
}
+
{
+
name = "_dmarc";
+
ttl = 10800;
+
type = "TXT";
+
data = "\"v=DMARC1; p=none\"";
+
}
+
];
+
};
+
}
+79
modules/hosting/mastodon.nix
···
+
{ pkgs, config, lib, ... }:
+
+
let
+
cfg = config.hosting;
+
domain = config.networking.domain;
+
in {
+
options.hosting.mastodon = lib.mkEnableOption "mastodon";
+
+
config = lib.mkIf cfg.mastodon {
+
services.mastodon = {
+
enable = true;
+
enableUnixSocket = false;
+
webProcesses = 1;
+
webThreads = 3;
+
sidekiqThreads = 5;
+
smtp = {
+
#createLocally = false;
+
user = "misc@${domain}";
+
port = 465;
+
host = "mail.${domain}";
+
authenticate = true;
+
passwordFile = "${config.custom.secretsDir}/email-pswd-unhashed";
+
fromAddress = "mastodon@${domain}";
+
};
+
extraConfig = {
+
# override localDomain
+
LOCAL_DOMAIN = "${domain}";
+
WEB_DOMAIN = "mastodon.${domain}";
+
+
# https://peterbabic.dev/blog/setting-up-smtp-in-mastodon/
+
SMTP_SSL="true";
+
SMTP_ENABLE_STARTTLS="false";
+
SMTP_OPENSSL_VERIFY_MODE="none";
+
};
+
};
+
+
users.groups.${config.services.mastodon.group}.members = [ config.services.nginx.user ];
+
+
services.nginx = {
+
enable = true;
+
recommendedProxySettings = true;
+
virtualHosts = {
+
# relies on root domain being set up
+
"${domain}".locations."/.well-known/host-meta".extraConfig = ''
+
return 301 https://mastodon.${domain}$request_uri;
+
'';
+
"mastodon.${domain}" = {
+
root = "${config.services.mastodon.package}/public/";
+
forceSSL = true;
+
enableACME = true;
+
+
locations."/system/".alias = "/var/lib/mastodon/public-system/";
+
+
locations."/" = {
+
tryFiles = "$uri @proxy";
+
};
+
+
locations."@proxy" = {
+
proxyPass = "http://127.0.0.1:${builtins.toString config.services.mastodon.webPort}";
+
proxyWebsockets = true;
+
};
+
+
locations."/api/v1/streaming/" = {
+
proxyPass = "http://127.0.0.1:${builtins.toString config.services.mastodon.streamingPort}/";
+
proxyWebsockets = true;
+
};
+
};
+
};
+
};
+
+
dns.records = [
+
{
+
name = "mastodon";
+
type = "CNAME";
+
data = "vps";
+
}
+
];
+
};
+
}
+112
modules/hosting/matrix.nix
···
+
{ config, pkgs, lib, ... }:
+
+
let cfg = config.hosting; in
+
{
+
options.hosting.matrix = lib.mkEnableOption "matrix";
+
+
config = lib.mkIf cfg.matrix {
+
services.postgresql.enable = true;
+
services.postgresql.package = pkgs.postgresql_13;
+
services.postgresql.initialScript = pkgs.writeText "synapse-init.sql" ''
+
CREATE ROLE "matrix-synapse" WITH LOGIN PASSWORD 'synapse';
+
CREATE DATABASE "matrix-synapse" WITH OWNER "matrix-synapse"
+
TEMPLATE template0
+
LC_COLLATE = "C"
+
LC_CTYPE = "C";
+
'';
+
+
services.nginx = {
+
enable = true;
+
# only recommendedProxySettings and recommendedGzipSettings are strictly required,
+
# but the rest make sense as well
+
recommendedTlsSettings = true;
+
recommendedOptimisation = true;
+
recommendedGzipSettings = true;
+
recommendedProxySettings = true;
+
+
virtualHosts = {
+
# This host section can be placed on a different host than the rest,
+
# i.e. to delegate from the host being accessible as ${config.networking.domain}
+
# to another host actually running the Matrix homeserver.
+
"${config.networking.domain}" = {
+
enableACME = true;
+
forceSSL = true;
+
+
locations."= /.well-known/matrix/server".extraConfig =
+
let
+
# use 443 instead of the default 8448 port to unite
+
# the client-server and server-server port for simplicity
+
server = { "m.server" = "matrix.${config.networking.domain}:443"; };
+
in ''
+
add_header Content-Type application/json;
+
return 200 '${builtins.toJSON server}';
+
'';
+
locations."= /.well-known/matrix/client".extraConfig =
+
let
+
client = {
+
"m.homeserver" = { "base_url" = "https://matrix.${config.networking.domain}"; };
+
"m.identity_server" = { "base_url" = "https://vector.im"; };
+
};
+
# ACAO required to allow element-web on any URL to request this json file
+
in ''
+
add_header Content-Type application/json;
+
add_header Access-Control-Allow-Origin *;
+
return 200 '${builtins.toJSON client}';
+
'';
+
};
+
+
# Reverse proxy for Matrix client-server and server-server communication
+
"matrix.${config.networking.domain}" = {
+
enableACME = true;
+
forceSSL = true;
+
+
# Or do a redirect instead of the 404, or whatever is appropriate for you.
+
# But do not put a Matrix Web client here! See the Element web section below.
+
locations."/".extraConfig = ''
+
return 404;
+
'';
+
+
# forward all Matrix API calls to the synapse Matrix homeserver
+
locations."/_matrix" = {
+
proxyPass = "http://127.0.0.1:8008"; # without a trailing /
+
#proxyPassReverse = "http://127.0.0.1:8008"; # without a trailing /
+
};
+
};
+
};
+
};
+
+
services.matrix-synapse = {
+
enable = true;
+
settings = {
+
server_name = config.networking.domain;
+
enable_registration = true;
+
registration_requires_token = true;
+
auto_join_rooms = [ "#freumh:freumh.org" ];
+
registration_shared_secret_path = "${config.custom.secretsDir}/matrix-shared-secret";
+
listeners = [
+
{
+
port = 8008;
+
bind_addresses = [ "::1" "127.0.0.1" ];
+
type = "http";
+
tls = false;
+
x_forwarded = true;
+
resources = [
+
{
+
names = [ "client" "federation" ];
+
compress = false;
+
}
+
];
+
}
+
];
+
};
+
};
+
+
dns.records = [
+
{
+
name = "matrix";
+
type = "CNAME";
+
data = "vps";
+
}
+
];
+
};
+
}
+78
modules/mailserver/borgbackup.nix
···
+
# 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.borgbackup;
+
+
methodFragment = lib.optional (cfg.compression.method != null) cfg.compression.method;
+
autoFragment =
+
if cfg.compression.auto && cfg.compression.method == null
+
then throw "compression.method must be set when using auto."
+
else lib.optional cfg.compression.auto "auto";
+
levelFragment =
+
if cfg.compression.level != null && cfg.compression.method == null
+
then throw "compression.method must be set when using compression.level."
+
else lib.optional (cfg.compression.level != null) (toString cfg.compression.level);
+
compressionFragment = lib.concatStringsSep "," (lib.flatten [autoFragment methodFragment levelFragment]);
+
compression = lib.optionalString (compressionFragment != "") "--compression ${compressionFragment}";
+
+
encryptionFragment = cfg.encryption.method;
+
passphraseFile = lib.escapeShellArg cfg.encryption.passphraseFile;
+
passphraseFragment = lib.optionalString (cfg.encryption.method != "none")
+
(if cfg.encryption.passphraseFile != null then ''env BORG_PASSPHRASE="$(cat ${passphraseFile})"''
+
else throw "passphraseFile must be set when using encryption.");
+
+
locations = lib.escapeShellArgs cfg.locations;
+
name = lib.escapeShellArg cfg.name;
+
+
repoLocation = lib.escapeShellArg cfg.repoLocation;
+
+
extraInitArgs = lib.escapeShellArgs cfg.extraArgumentsForInit;
+
extraCreateArgs = lib.escapeShellArgs cfg.extraArgumentsForCreate;
+
+
cmdPreexec = lib.optionalString (cfg.cmdPreexec != null) cfg.cmdPreexec;
+
cmdPostexec = lib.optionalString (cfg.cmdPostexec != null) cfg.cmdPostexec;
+
+
borgScript = ''
+
export BORG_REPO=${repoLocation}
+
${cmdPreexec}
+
${passphraseFragment} ${pkgs.borgbackup}/bin/borg init ${extraInitArgs} --encryption ${encryptionFragment} || true
+
${passphraseFragment} ${pkgs.borgbackup}/bin/borg create ${extraCreateArgs} ${compression} ::${name} ${locations}
+
${cmdPostexec}
+
'';
+
in {
+
config = lib.mkIf (config.mailserver.enable && cfg.enable) {
+
environment.systemPackages = with pkgs; [
+
borgbackup
+
];
+
+
systemd.services.borgbackup = {
+
description = "borgbackup";
+
unitConfig.Documentation = "man:borgbackup";
+
script = borgScript;
+
serviceConfig = {
+
User = cfg.user;
+
Group = cfg.group;
+
CPUSchedulingPolicy = "idle";
+
IOSchedulingClass = "idle";
+
ProtectSystem = "full";
+
};
+
startAt = cfg.startAt;
+
};
+
};
+
}
+30
modules/mailserver/clamav.nix
···
+
# 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, options, ... }:
+
+
let
+
cfg = config.mailserver;
+
in
+
{
+
config = lib.mkIf (cfg.enable && cfg.virusScanning) {
+
services.clamav.daemon = {
+
enable = true;
+
settings.PhishingScanURLs = "no";
+
};
+
services.clamav.updater.enable = true;
+
};
+
}
+48
modules/mailserver/common.nix
···
+
# 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;
+
in
+
{
+
# cert :: PATH
+
certificatePath = if cfg.certificateScheme == 1
+
then cfg.certificateFile
+
else if cfg.certificateScheme == 2
+
then "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem"
+
else if cfg.certificateScheme == 3
+
then "${config.security.acme.certs.${cfg.fqdn}.directory}/fullchain.pem"
+
else throw "Error: Certificate Scheme must be in { 1, 2, 3 }";
+
+
# key :: PATH
+
keyPath = if cfg.certificateScheme == 1
+
then cfg.keyFile
+
else if cfg.certificateScheme == 2
+
then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem"
+
else if cfg.certificateScheme == 3
+
then "${config.security.acme.certs.${cfg.fqdn}.directory}/key.pem"
+
else throw "Error: Certificate Scheme must be in { 1, 2, 3 }";
+
+
passwordFiles = let
+
mkHashFile = name: hash: pkgs.writeText "${builtins.hashString "sha256" name}-password-hash" hash;
+
in
+
lib.mapAttrs (name: value:
+
if value.hashedPasswordFile == null then
+
builtins.toString (mkHashFile name value.hashedPassword)
+
else value.hashedPasswordFile) cfg.loginAccounts;
+
}
+4
modules/mailserver/debug.nix
···
+
{ config, lib, ... }:
+
{
+
mailserver.policydSPFExtraConfig = lib.mkIf config.mailserver.debug "debugLevel = 4";
+
}
+1043
modules/mailserver/default.nix
···
+
# 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, lib, pkgs, ... }:
+
+
with lib;
+
+
let
+
cfg = config.mailserver;
+
in
+
{
+
options.mailserver = {
+
enable = lib.mkEnableOption "nixos-mailserver";
+
+
openFirewall = mkOption {
+
type = types.bool;
+
default = true;
+
description = "Automatically open ports in the firewall.";
+
};
+
+
fqdn = mkOption {
+
type = types.str;
+
example = "mx.example.com";
+
description = "The fully qualified domain name of the mail server.";
+
};
+
+
domains = mkOption {
+
type = types.listOf types.str;
+
example = [ "example.com" ];
+
default = [];
+
description = "The domains that this mail server serves.";
+
};
+
+
certificateDomains = mkOption {
+
type = types.listOf types.str;
+
example = [ "imap.example.com" "pop3.example.com" ];
+
default = [];
+
description = "Secondary domains and subdomains for which it is necessary to generate a certificate.";
+
};
+
+
messageSizeLimit = mkOption {
+
type = types.int;
+
example = 52428800;
+
default = 20971520;
+
description = "Message size limit enforced by Postfix.";
+
};
+
+
loginAccounts = mkOption {
+
type = types.attrsOf (types.submodule ({ name, ... }: {
+
options = {
+
name = mkOption {
+
type = types.str;
+
example = "user1@example.com";
+
description = "Username";
+
};
+
+
hashedPassword = mkOption {
+
type = with types; nullOr str;
+
default = null;
+
example = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/";
+
description = ''
+
The user's hashed password. Use `htpasswd` as follows
+
+
```
+
nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2
+
```
+
+
Warning: this is stored in plaintext in the Nix store!
+
Use `hashedPasswordFile` instead.
+
'';
+
};
+
+
hashedPasswordFile = mkOption {
+
type = with types; nullOr path;
+
default = null;
+
example = "/run/keys/user1-passwordhash";
+
description = ''
+
A file containing the user's hashed password. Use `htpasswd` as follows
+
+
```
+
nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2
+
```
+
'';
+
};
+
+
aliases = mkOption {
+
type = with types; listOf types.str;
+
example = ["abuse@example.com" "postmaster@example.com"];
+
default = [];
+
description = ''
+
A list of aliases of this login account.
+
Note: Use list entries like "@example.com" to create a catchAll
+
that allows sending from all email addresses in these domain.
+
'';
+
};
+
+
catchAll = mkOption {
+
type = with types; listOf (enum cfg.domains);
+
example = ["example.com" "example2.com"];
+
default = [];
+
description = ''
+
For which domains should this account act as a catch all?
+
Note: Does not allow sending from all addresses of these domains.
+
'';
+
};
+
+
quota = mkOption {
+
type = with types; nullOr types.str;
+
default = null;
+
example = "2G";
+
description = ''
+
Per user quota rules. Accepted sizes are `xx k/M/G/T` with the
+
obvious meaning. Leave blank for the standard quota `100G`.
+
'';
+
};
+
+
sieveScript = mkOption {
+
type = with types; nullOr lines;
+
default = null;
+
example = ''
+
require ["fileinto", "mailbox"];
+
+
if address :is "from" "gitlab@mg.gitlab.com" {
+
fileinto :create "GitLab";
+
stop;
+
}
+
+
# This must be the last rule, it will check if list-id is set, and
+
# file the message into the Lists folder for further investigation
+
elsif header :matches "list-id" "<?*>" {
+
fileinto :create "Lists";
+
stop;
+
}
+
'';
+
description = ''
+
Per-user sieve script.
+
'';
+
};
+
+
sendOnly = mkOption {
+
type = types.bool;
+
default = false;
+
description = ''
+
Specifies if the account should be a send-only account.
+
Emails sent to send-only accounts will be rejected from
+
unauthorized senders with the sendOnlyRejectMessage
+
stating the reason.
+
'';
+
};
+
+
sendOnlyRejectMessage = mkOption {
+
type = types.str;
+
default = "This account cannot receive emails.";
+
description = ''
+
The message that will be returned to the sender when an email is
+
sent to a send-only account. Only used if the account is marked
+
as send-only.
+
'';
+
};
+
};
+
+
config.name = mkDefault name;
+
}));
+
example = {
+
user1 = {
+
hashedPassword = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/";
+
};
+
user2 = {
+
hashedPassword = "$6$oE0ZNv2n7Vk9gOf$9xcZWCCLGdMflIfuA0vR1Q1Xblw6RZqPrP94mEit2/81/7AKj2bqUai5yPyWE.QYPyv6wLMHZvjw3Rlg7yTCD/";
+
};
+
};
+
description = ''
+
The login account of the domain. Every account is mapped to a unix user,
+
e.g. `user1@example.com`. To generate the passwords use `htpasswd` as
+
follows
+
+
```
+
nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2
+
```
+
'';
+
default = {};
+
};
+
+
indexDir = mkOption {
+
type = types.nullOr types.str;
+
default = null;
+
description = ''
+
Folder to store search indices. If null, indices are stored
+
along with email, which could not necessarily be desirable,
+
especially when the fullTextSearch option is enable since
+
indices it creates are voluminous and do not need to be backed
+
up.
+
+
Be careful when changing this option value since all indices
+
would be recreated at the new location (and clients would need
+
to resynchronize).
+
+
Note the some variables can be used in the file path. See
+
https://doc.dovecot.org/configuration_manual/mail_location/#variables
+
for details.
+
'';
+
example = "/var/lib/dovecot/indices";
+
};
+
+
fullTextSearch = {
+
enable = lib.mkEnableOption "Full text search indexing with xapian. This has significant performance and disk space cost.";
+
autoIndex = mkOption {
+
type = types.bool;
+
default = true;
+
description = "Enable automatic indexing of messages as they are received or modified.";
+
};
+
autoIndexExclude = mkOption {
+
type = types.listOf types.str;
+
default = [ ];
+
example = [ "\\Trash" "SomeFolder" "Other/*" ];
+
description = ''
+
Mailboxes to exclude from automatic indexing.
+
'';
+
};
+
+
indexAttachments = mkOption {
+
type = types.bool;
+
default = false;
+
description = "Also index text-only attachements. Binary attachements are never indexed.";
+
};
+
+
enforced = mkOption {
+
type = types.enum [ "yes" "no" "body" ];
+
default = "no";
+
description = ''
+
Fail searches when no index is available. If set to
+
<literal>body</literal>, then only body searches (as opposed to
+
header) are affected. If set to <literal>no</literal>, searches may
+
fall back to a very slow brute force search.
+
'';
+
};
+
+
minSize = mkOption {
+
type = types.int;
+
default = 2;
+
description = "Size of the smallest n-gram to index.";
+
};
+
maxSize = mkOption {
+
type = types.int;
+
default = 20;
+
description = "Size of the largest n-gram to index.";
+
};
+
memoryLimit = mkOption {
+
type = types.nullOr types.int;
+
default = null;
+
example = 2000;
+
description = "Memory limit for the indexer process, in MiB. If null, leaves the default (which is rather low), and if 0, no limit.";
+
};
+
+
maintenance = {
+
enable = mkOption {
+
type = types.bool;
+
default = true;
+
description = "Regularly optmize indices, as recommended by upstream.";
+
};
+
+
onCalendar = mkOption {
+
type = types.str;
+
default = "daily";
+
description = "When to run the maintenance job. See systemd.time(7) for more information about the format.";
+
};
+
+
randomizedDelaySec = mkOption {
+
type = types.int;
+
default = 1000;
+
description = "Run the maintenance job not exactly at the time specified with <literal>onCalendar</literal>, but plus or minus this many seconds.";
+
};
+
};
+
};
+
+
lmtpSaveToDetailMailbox = mkOption {
+
type = types.enum ["yes" "no"];
+
default = "yes";
+
description = ''
+
If an email address is delimited by a "+", should it be filed into a
+
mailbox matching the string after the "+"? For example,
+
user1+test@example.com would be filed into the mailbox "test".
+
'';
+
};
+
+
extraVirtualAliases = mkOption {
+
type = let
+
loginAccount = mkOptionType {
+
name = "Login Account";
+
check = (account: builtins.elem account (builtins.attrNames cfg.loginAccounts));
+
};
+
in with types; attrsOf (either loginAccount (nonEmptyListOf loginAccount));
+
example = {
+
"info@example.com" = "user1@example.com";
+
"postmaster@example.com" = "user1@example.com";
+
"abuse@example.com" = "user1@example.com";
+
"multi@example.com" = [ "user1@example.com" "user2@example.com" ];
+
};
+
description = ''
+
Virtual Aliases. A virtual alias `"info@example.com" = "user1@example.com"` means that
+
all mail to `info@example.com` is forwarded to `user1@example.com`. Note
+
that it is expected that `postmaster@example.com` and `abuse@example.com` is
+
forwarded to some valid email address. (Alternatively you can create login
+
accounts for `postmaster` and (or) `abuse`). Furthermore, it also allows
+
the user `user1@example.com` to send emails as `info@example.com`.
+
It's also possible to create an alias for multiple accounts. In this
+
example all mails for `multi@example.com` will be forwarded to both
+
`user1@example.com` and `user2@example.com`.
+
'';
+
default = {};
+
};
+
+
forwards = mkOption {
+
type = with types; attrsOf (either (listOf str) str);
+
example = {
+
"user@example.com" = "user@elsewhere.com";
+
};
+
description = ''
+
To forward mails to an external address. For instance,
+
the value {`"user@example.com" = "user@elsewhere.com";}`
+
means that mails to `user@example.com` are forwarded to
+
`user@elsewhere.com`. The difference with the
+
`extraVirtualAliases` option is that `user@elsewhere.com`
+
can't send mail as `user@example.com`. Also, this option
+
allows to forward mails to external addresses.
+
'';
+
default = {};
+
};
+
+
rejectSender = mkOption {
+
type = types.listOf types.str;
+
example = [ "@example.com" "spammer@example.net" ];
+
description = ''
+
Reject emails from these addresses from unauthorized senders.
+
Use if a spammer is using the same domain or the same sender over and over.
+
'';
+
default = [];
+
};
+
+
rejectRecipients = mkOption {
+
type = types.listOf types.str;
+
example = [ "sales@example.com" "info@example.com" ];
+
description = ''
+
Reject emails addressed to these local addresses from unauthorized senders.
+
Use if a spammer has found email addresses in a catchall domain but you do
+
not want to disable the catchall.
+
'';
+
default = [];
+
};
+
+
vmailUID = mkOption {
+
type = types.int;
+
default = 5000;
+
description = ''
+
The unix UID of the virtual mail user. Be mindful that if this is
+
changed, you will need to manually adjust the permissions of
+
mailDirectory.
+
'';
+
};
+
+
vmailUserName = mkOption {
+
type = types.str;
+
default = "virtualMail";
+
description = ''
+
The user name and group name of the user that owns the directory where all
+
the mail is stored.
+
'';
+
};
+
+
vmailGroupName = mkOption {
+
type = types.str;
+
default = "virtualMail";
+
description = ''
+
The user name and group name of the user that owns the directory where all
+
the mail is stored.
+
'';
+
};
+
+
mailDirectory = mkOption {
+
type = types.path;
+
default = "/var/vmail";
+
description = ''
+
Where to store the mail.
+
'';
+
};
+
+
useFsLayout = mkOption {
+
type = types.bool;
+
default = false;
+
description = ''
+
Sets whether dovecot should organize mail in subdirectories:
+
+
- /var/vmail/example.com/user/.folder.subfolder/ (default layout)
+
- /var/vmail/example.com/user/folder/subfolder/ (FS layout)
+
+
See https://wiki2.dovecot.org/MailboxFormat/Maildir for details.
+
'';
+
};
+
+
hierarchySeparator = mkOption {
+
type = types.str;
+
default = ".";
+
description = ''
+
The hierarchy separator for mailboxes used by dovecot for the namespace 'inbox'.
+
Dovecot defaults to "." but recommends "/".
+
This affects how mailboxes appear to mail clients and sieve scripts.
+
For instance when using "." then in a sieve script "example.com" would refer to the mailbox "com" in the parent mailbox "example".
+
This does not determine the way your mails are stored on disk.
+
See https://wiki.dovecot.org/Namespaces for details.
+
'';
+
};
+
+
mailboxes = mkOption {
+
description = ''
+
The mailboxes for dovecot.
+
Depending on the mail client used it might be necessary to change some mailbox's name.
+
'';
+
default = {
+
Trash = {
+
auto = "no";
+
specialUse = "Trash";
+
};
+
Junk = {
+
auto = "subscribe";
+
specialUse = "Junk";
+
};
+
Drafts = {
+
auto = "subscribe";
+
specialUse = "Drafts";
+
};
+
Sent = {
+
auto = "subscribe";
+
specialUse = "Sent";
+
};
+
};
+
};
+
+
certificateScheme = mkOption {
+
type = types.enum [ 1 2 3 ];
+
default = 2;
+
description = ''
+
Certificate Files. There are three options for these.
+
+
1) You specify locations and manually copy certificates there.
+
2) You let the server create new (self signed) certificates on the fly.
+
3) You let the server create a certificate via `Let's Encrypt`. Note that
+
this implies that a stripped down webserver has to be started. This also
+
implies that the FQDN must be set as an `A` record to point to the IP of
+
the server. In particular port 80 on the server will be opened. For details
+
on how to set up the domain records, see the guide in the readme.
+
'';
+
};
+
+
certificateFile = mkOption {
+
type = types.path;
+
example = "/root/mail-server.crt";
+
description = ''
+
Scheme 1)
+
Location of the certificate
+
'';
+
};
+
+
keyFile = mkOption {
+
type = types.path;
+
example = "/root/mail-server.key";
+
description = ''
+
Scheme 1)
+
Location of the key file
+
'';
+
};
+
+
certificateDirectory = mkOption {
+
type = types.path;
+
default = "/var/certs";
+
description = ''
+
Scheme 2)
+
This is the folder where the certificate will be created. The name is
+
hardcoded to "cert-DOMAIN.pem" and "key-DOMAIN.pem" and the
+
certificate is valid for 10 years.
+
'';
+
};
+
+
enableImap = mkOption {
+
type = types.bool;
+
default = true;
+
description = ''
+
Whether to enable IMAP with STARTTLS on port 143.
+
'';
+
};
+
+
enableImapSsl = mkOption {
+
type = types.bool;
+
default = true;
+
description = ''
+
Whether to enable IMAP with TLS in wrapper-mode on port 993.
+
'';
+
};
+
+
enableSubmission = mkOption {
+
type = types.bool;
+
default = true;
+
description = ''
+
Whether to enable SMTP with STARTTLS on port 587.
+
'';
+
};
+
+
enableSubmissionSsl = mkOption {
+
type = types.bool;
+
default = true;
+
description = ''
+
Whether to enable SMTP with TLS in wrapper-mode on port 465.
+
'';
+
};
+
+
enablePop3 = mkOption {
+
type = types.bool;
+
default = false;
+
description = ''
+
Whether to enable POP3 with STARTTLS on port on port 110.
+
'';
+
};
+
+
enablePop3Ssl = mkOption {
+
type = types.bool;
+
default = false;
+
description = ''
+
Whether to enable POP3 with TLS in wrapper-mode on port 995.
+
'';
+
};
+
+
enableManageSieve = mkOption {
+
type = types.bool;
+
default = false;
+
description = ''
+
Whether to enable ManageSieve, setting this option to true will open
+
port 4190 in the firewall.
+
+
The ManageSieve protocol allows users to manage their Sieve scripts on
+
a remote server with a supported client, including Thunderbird.
+
'';
+
};
+
+
sieveDirectory = mkOption {
+
type = types.path;
+
default = "/var/sieve";
+
description = ''
+
Where to store the sieve scripts.
+
'';
+
};
+
+
virusScanning = mkOption {
+
type = types.bool;
+
default = false;
+
description = ''
+
Whether to activate virus scanning. Note that virus scanning is _very_
+
expensive memory wise.
+
'';
+
};
+
+
dkimSigning = mkOption {
+
type = types.bool;
+
default = true;
+
description = ''
+
Whether to activate dkim signing.
+
'';
+
};
+
+
dkimSelector = mkOption {
+
type = types.str;
+
default = "mail";
+
description = ''
+
+
'';
+
};
+
+
dkimKeyDirectory = mkOption {
+
type = types.path;
+
default = "/var/dkim";
+
description = ''
+
+
'';
+
};
+
+
dkimKeyBits = mkOption {
+
type = types.int;
+
default = 1024;
+
description = ''
+
How many bits in generated DKIM keys. RFC6376 advises minimum 1024-bit keys.
+
+
If you have already deployed a key with a different number of bits than specified
+
here, then you should use a different selector (dkimSelector). In order to get
+
this package to generate a key with the new number of bits, you will either have to
+
change the selector or delete the old key file.
+
'';
+
};
+
+
dkimHeaderCanonicalization = mkOption {
+
type = types.enum ["relaxed" "simple"];
+
default = "relaxed";
+
description = ''
+
DKIM canonicalization algorithm for message headers.
+
+
See https://datatracker.ietf.org/doc/html/rfc6376/#section-3.4 for details.
+
'';
+
};
+
+
dkimBodyCanonicalization = mkOption {
+
type = types.enum ["relaxed" "simple"];
+
default = "relaxed";
+
description = ''
+
DKIM canonicalization algorithm for message bodies.
+
+
See https://datatracker.ietf.org/doc/html/rfc6376/#section-3.4 for details.
+
'';
+
};
+
+
debug = mkOption {
+
type = types.bool;
+
default = false;
+
description = ''
+
Whether to enable verbose logging for mailserver related services. This
+
intended be used for development purposes only, you probably don't want
+
to enable this unless you're hacking on nixos-mailserver.
+
'';
+
};
+
+
maxConnectionsPerUser = mkOption {
+
type = types.int;
+
default = 100;
+
description = ''
+
Maximum number of IMAP/POP3 connections allowed for a user from each IP address.
+
E.g. a value of 50 allows for 50 IMAP and 50 POP3 connections at the same
+
time for a single user.
+
'';
+
};
+
+
localDnsResolver = mkOption {
+
type = types.bool;
+
default = true;
+
description = ''
+
Runs a local DNS resolver (kresd) as recommended when running rspamd. This prevents your log file from filling up with rspamd_monitored_dns_mon entries.
+
'';
+
};
+
+
recipientDelimiter = mkOption {
+
type = types.str;
+
default = "+";
+
description = ''
+
Configure the recipient delimiter.
+
'';
+
};
+
+
redis = {
+
address = mkOption {
+
type = types.str;
+
# read the default from nixos' redis module
+
default = let
+
cf = config.services.redis.servers.rspamd.bind;
+
cfdefault = if cf == null then "127.0.0.1" else cf;
+
ips = lib.strings.splitString " " cfdefault;
+
ip = lib.lists.head (ips ++ [ "127.0.0.1" ]);
+
isIpv6 = ip: lib.lists.elem ":" (lib.stringToCharacters ip);
+
in
+
if (ip == "0.0.0.0" || ip == "::")
+
then "127.0.0.1"
+
else if isIpv6 ip then "[${ip}]" else ip;
+
defaultText = lib.literalDocBook "computed from <option>config.services.redis.servers.rspamd.bind</option>";
+
description = ''
+
Address that rspamd should use to contact redis.
+
'';
+
};
+
+
port = mkOption {
+
type = types.port;
+
default = config.services.redis.servers.rspamd.port;
+
defaultText = lib.literalExpression "config.services.redis.servers.rspamd.port";
+
description = ''
+
Port that rspamd should use to contact redis.
+
'';
+
};
+
+
password = mkOption {
+
type = types.nullOr types.str;
+
default = config.services.redis.servers.rspamd.requirePass;
+
defaultText = lib.literalExpression "config.services.redis.servers.rspamd.requirePass";
+
description = ''
+
Password that rspamd should use to contact redis, or null if not required.
+
'';
+
};
+
};
+
+
rewriteMessageId = mkOption {
+
type = types.bool;
+
default = false;
+
description = ''
+
Rewrites the Message-ID's hostname-part of outgoing emails to the FQDN.
+
Please be aware that this may cause problems with some mail clients
+
relying on the original Message-ID.
+
'';
+
};
+
+
sendingFqdn = mkOption {
+
type = types.str;
+
default = cfg.fqdn;
+
defaultText = "config.mailserver.fqdn";
+
example = "myserver.example.com";
+
description = ''
+
The fully qualified domain name of the mail server used to
+
identify with remote servers.
+
+
If this server's IP serves purposes other than a mail server,
+
it may be desirable for the server to have a name other than
+
that to which the user will connect. For example, the user
+
might connect to mx.example.com, but the server's IP has
+
reverse DNS that resolves to myserver.example.com; in this
+
scenario, some mail servers may reject or penalize the
+
message.
+
+
This setting allows the server to identify as
+
myserver.example.com when forwarding mail, independently of
+
`fqdn` (which, for SSL reasons, should generally be the name
+
to which the user connects).
+
+
Set this to the name to which the sending IP's reverse DNS
+
resolves.
+
'';
+
};
+
+
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 = {
+
enable = lib.mkEnableOption "monitoring via monit";
+
+
alertAddress = mkOption {
+
type = types.str;
+
description = ''
+
The email address to send alerts to.
+
'';
+
};
+
+
config = mkOption {
+
type = types.str;
+
default = ''
+
set daemon 120 with start delay 60
+
set mailserver
+
localhost
+
+
set httpd port 2812 and use address localhost
+
allow localhost
+
allow admin:obwjoawijerfoijsiwfj29jf2f2jd
+
+
check filesystem root with path /
+
if space usage > 80% then alert
+
if inode usage > 80% then alert
+
+
check system $HOST
+
if cpu usage > 95% for 10 cycles then alert
+
if memory usage > 75% for 5 cycles then alert
+
if swap usage > 20% for 10 cycles then alert
+
if loadavg (1min) > 90 for 15 cycles then alert
+
if loadavg (5min) > 80 for 10 cycles then alert
+
if loadavg (15min) > 70 for 8 cycles then alert
+
+
check process sshd with pidfile /var/run/sshd.pid
+
start program "${pkgs.systemd}/bin/systemctl start sshd"
+
stop program "${pkgs.systemd}/bin/systemctl stop sshd"
+
if failed port 22 protocol ssh for 2 cycles then restart
+
+
check process postfix with pidfile /var/lib/postfix/queue/pid/master.pid
+
start program = "${pkgs.systemd}/bin/systemctl start postfix"
+
stop program = "${pkgs.systemd}/bin/systemctl stop postfix"
+
if failed port 25 protocol smtp for 5 cycles then restart
+
+
check process dovecot with pidfile /var/run/dovecot2/master.pid
+
start program = "${pkgs.systemd}/bin/systemctl start dovecot2"
+
stop program = "${pkgs.systemd}/bin/systemctl stop dovecot2"
+
if failed host ${cfg.fqdn} port 993 type tcpssl sslauto protocol imap for 5 cycles then restart
+
+
check process rspamd with matching "rspamd: main process"
+
start program = "${pkgs.systemd}/bin/systemctl start rspamd"
+
stop program = "${pkgs.systemd}/bin/systemctl stop rspamd"
+
'';
+
defaultText = lib.literalDocBook "see source";
+
description = ''
+
The configuration used for monitoring via monit.
+
Use a mail address that you actively check and set it via 'set alert ...'.
+
'';
+
};
+
};
+
+
borgbackup = {
+
enable = lib.mkEnableOption "backup via borgbackup";
+
+
repoLocation = mkOption {
+
type = types.str;
+
default = "/var/borgbackup";
+
description = ''
+
The location where borg saves the backups.
+
This can be a local path or a remote location such as user@host:/path/to/repo.
+
It is exported and thus available as an environment variable to cmdPreexec and cmdPostexec.
+
'';
+
};
+
+
startAt = mkOption {
+
type = types.str;
+
default = "hourly";
+
description = "When or how often the backup should run. Must be in the format described in systemd.time 7.";
+
};
+
+
user = mkOption {
+
type = types.str;
+
default = "virtualMail";
+
description = "The user borg and its launch script is run as.";
+
};
+
+
group = mkOption {
+
type = types.str;
+
default = "virtualMail";
+
description = "The group borg and its launch script is run as.";
+
};
+
+
compression = {
+
method = mkOption {
+
type = types.nullOr (types.enum ["none" "lz4" "zstd" "zlib" "lzma"]);
+
default = null;
+
description = "Leaving this unset allows borg to choose. The default for borg 1.1.4 is lz4.";
+
};
+
+
level = mkOption {
+
type = types.nullOr types.int;
+
default = null;
+
description = ''
+
Denotes the level of compression used by borg.
+
Most methods accept levels from 0 to 9 but zstd which accepts values from 1 to 22.
+
If null the decision is left up to borg.
+
'';
+
};
+
+
auto = mkOption {
+
type = types.bool;
+
default = false;
+
description = "Leaves it to borg to determine whether an individual file should be compressed.";
+
};
+
};
+
+
encryption = {
+
method = mkOption {
+
type = types.enum [
+
"none"
+
"authenticated"
+
"authenticated-blake2"
+
"repokey"
+
"keyfile"
+
"repokey-blake2"
+
"keyfile-blake2"
+
];
+
default = "none";
+
description = ''
+
The backup can be encrypted by choosing any other value than 'none'.
+
When using encryption the password / passphrase must be provided in passphraseFile.
+
'';
+
};
+
+
passphraseFile = mkOption {
+
type = types.nullOr types.path;
+
default = null;
+
description = "Path to a file containing the encryption password or passphrase.";
+
};
+
};
+
+
name = mkOption {
+
type = types.str;
+
default = "{hostname}-{user}-{now}";
+
description = ''
+
The name of the individual backups as used by borg.
+
Certain placeholders will be replaced by borg.
+
'';
+
};
+
+
locations = mkOption {
+
type = types.listOf types.path;
+
default = [cfg.mailDirectory];
+
description = "The locations that are to be backed up by borg.";
+
};
+
+
extraArgumentsForInit = mkOption {
+
type = types.listOf types.str;
+
default = ["--critical"];
+
description = "Additional arguments to add to the borg init command line.";
+
};
+
+
extraArgumentsForCreate = mkOption {
+
type = types.listOf types.str;
+
default = [ ];
+
description = "Additional arguments to add to the borg create command line e.g. '--stats'.";
+
};
+
+
cmdPreexec = mkOption {
+
type = types.nullOr types.str;
+
default = null;
+
description = ''
+
The command to be executed before each backup operation.
+
This is called prior to borg init in the same script that runs borg init and create and cmdPostexec.
+
Example:
+
export BORG_RSH="ssh -i /path/to/private/key"
+
'';
+
};
+
+
cmdPostexec = mkOption {
+
type = types.nullOr types.str;
+
default = null;
+
description = ''
+
The command to be executed after each backup operation.
+
This is called after borg create completed successfully and in the same script that runs
+
cmdPreexec, borg init and create.
+
'';
+
};
+
+
};
+
+
rebootAfterKernelUpgrade = {
+
enable = mkOption {
+
type = types.bool;
+
default = false;
+
example = true;
+
description = ''
+
Whether to enable automatic reboot after kernel upgrades.
+
This is to be used in conjunction with system.autoUpgrade.enable = true"
+
'';
+
};
+
method = mkOption {
+
type = types.enum [ "reboot" "systemctl kexec" ];
+
default = "reboot";
+
description = ''
+
Whether to issue a full "reboot" or just a "systemctl kexec"-only reboot.
+
It is recommended to use the default value because the quicker kexec reboot has a number of problems.
+
Also if your server is running in a virtual machine the regular reboot will already be very quick.
+
'';
+
};
+
};
+
+
backup = {
+
enable = lib.mkEnableOption "backup via rsnapshot";
+
+
snapshotRoot = mkOption {
+
type = types.path;
+
default = "/var/rsnapshot";
+
description = ''
+
The directory where rsnapshot stores the backup.
+
'';
+
};
+
+
cmdPreexec = mkOption {
+
type = types.nullOr types.str;
+
default = null;
+
description = ''
+
The command to be executed before each backup operation. This is wrapped in a shell script to be called by rsnapshot.
+
'';
+
};
+
+
cmdPostexec = mkOption {
+
type = types.nullOr types.str;
+
default = null;
+
description = "The command to be executed after each backup operation. This is wrapped in a shell script to be called by rsnapshot.";
+
};
+
+
retain = {
+
hourly = mkOption {
+
type = types.int;
+
default = 24;
+
description = "How many hourly snapshots are retained.";
+
};
+
daily = mkOption {
+
type = types.int;
+
default = 7;
+
description = "How many daily snapshots are retained.";
+
};
+
weekly = mkOption {
+
type = types.int;
+
default = 54;
+
description = "How many weekly snapshots are retained.";
+
};
+
};
+
+
cronIntervals = mkOption {
+
type = types.attrsOf types.str;
+
default = {
+
# minute, hour, day-in-month, month, weekday (0 = sunday)
+
hourly = " 0 * * * *"; # Every full hour
+
daily = "30 3 * * *"; # Every day at 3:30
+
weekly = " 0 5 * * 0"; # Every sunday at 5:00 AM
+
};
+
description = ''
+
Periodicity at which intervals should be run by cron.
+
Note that the intervals also have to exist in configuration
+
as retain options.
+
'';
+
};
+
};
+
};
+
+
imports = [
+
./borgbackup.nix
+
./debug.nix
+
./rsnapshot.nix
+
./clamav.nix
+
./monit.nix
+
./users.nix
+
./environment.nix
+
./networking.nix
+
./systemd.nix
+
./dovecot.nix
+
./opendkim.nix
+
./postfix.nix
+
./rspamd.nix
+
./nginx.nix
+
./kresd.nix
+
./post-upgrade-check.nix
+
];
+
}
+324
modules/mailserver/dovecot.nix
···
+
# 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, ... }:
+
+
with (import ./common.nix { inherit config pkgs lib; });
+
+
let
+
cfg = config.mailserver;
+
+
passwdDir = "/run/dovecot2";
+
passwdFile = "${passwdDir}/passwd";
+
+
bool2int = x: if x then "1" else "0";
+
+
maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs";
+
+
# maildir in format "/${domain}/${user}"
+
dovecotMaildir =
+
"maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}"
+
+ (lib.optionalString (cfg.indexDir != null)
+
":INDEX=${cfg.indexDir}/%d/%n"
+
);
+
+
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
+
'';
+
};
+
+
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}") cfg.loginAccounts)}; 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 cfg.vmailUID}:${builtins.toString cfg.vmailUID}::${cfg.mailDirectory}:/run/current-system/sw/bin/nologin:"
+
+ (if lib.isString value.quota
+
then "userdb_quota_rule=*:storage=${value.quota}"
+
else "")
+
) cfg.loginAccounts)}
+
EOF
+
+
chmod 600 ${passwdFile}
+
'';
+
+
junkMailboxes = builtins.attrNames (lib.filterAttrs (n: v: v ? "specialUse" && v.specialUse == "Junk") cfg.mailboxes);
+
junkMailboxNumber = builtins.length junkMailboxes;
+
# The assertion garantees there is exactly one Junk mailbox.
+
junkMailboxName = if junkMailboxNumber == 1 then builtins.elemAt junkMailboxes 0 else "";
+
+
in
+
{
+
config = with cfg; lib.mkIf enable {
+
assertions = [
+
{
+
assertion = junkMailboxNumber == 1;
+
message = "nixos-mailserver requires exactly one dovecot mailbox with the 'special use' flag set to 'Junk' (${builtins.toString junkMailboxNumber} have been found)";
+
}
+
];
+
+
services.dovecot2 = {
+
enable = true;
+
enableImap = enableImap || enableImapSsl;
+
enablePop3 = enablePop3 || enablePop3Ssl;
+
enablePAM = false;
+
enableQuota = true;
+
mailGroup = vmailGroupName;
+
mailUser = vmailUserName;
+
mailLocation = dovecotMaildir;
+
sslServerCert = certificatePath;
+
sslServerKey = keyPath;
+
enableLmtp = true;
+
modules = [ pkgs.dovecot_pigeonhole ] ++ (lib.optional cfg.fullTextSearch.enable pkgs.dovecot_fts_xapian );
+
mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [ "fts" "fts_xapian" ];
+
protocols = lib.optional cfg.enableManageSieve "sieve";
+
+
sieveScripts = {
+
after = builtins.toFile "spam.sieve" ''
+
require "fileinto";
+
+
if header :is "X-Spam" "Yes" {
+
fileinto "${junkMailboxName}";
+
stop;
+
}
+
'';
+
};
+
+
mailboxes = cfg.mailboxes;
+
+
extraConfig = ''
+
#Extra Config
+
${lib.optionalString debug ''
+
mail_debug = yes
+
auth_debug = yes
+
verbose_ssl = yes
+
''}
+
+
${lib.optionalString (cfg.enableImap || cfg.enableImapSsl) ''
+
service imap-login {
+
inet_listener imap {
+
${if cfg.enableImap then ''
+
port = 143
+
'' else ''
+
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
+
port = 0
+
''}
+
}
+
inet_listener imaps {
+
${if cfg.enableImapSsl then ''
+
port = 993
+
ssl = yes
+
'' else ''
+
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
+
port = 0
+
''}
+
}
+
}
+
''}
+
${lib.optionalString (cfg.enablePop3 || cfg.enablePop3Ssl) ''
+
service pop3-login {
+
inet_listener pop3 {
+
${if cfg.enablePop3 then ''
+
port = 110
+
'' else ''
+
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
+
port = 0
+
''}
+
}
+
inet_listener pop3s {
+
${if cfg.enablePop3Ssl then ''
+
port = 995
+
ssl = yes
+
'' else ''
+
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
+
port = 0
+
''}
+
}
+
}
+
''}
+
+
protocol imap {
+
mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser}
+
mail_plugins = $mail_plugins imap_sieve
+
}
+
+
protocol pop3 {
+
mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser}
+
}
+
+
mail_access_groups = ${vmailGroupName}
+
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 = ${cfg.recipientDelimiter}
+
lmtp_save_to_detail_mailbox = ${cfg.lmtpSaveToDetailMailbox}
+
+
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 = ${cfg.hierarchySeparator}
+
inbox = yes
+
}
+
+
plugin {
+
sieve_plugins = sieve_imapsieve sieve_extprograms
+
sieve = file:${cfg.sieveDirectory}/%u/scripts;active=${cfg.sieveDirectory}/%u/active.sieve
+
sieve_default = file:${cfg.sieveDirectory}/%u/default.sieve
+
sieve_default_name = default
+
+
# From elsewhere to Spam folder
+
imapsieve_mailbox1_name = ${junkMailboxName}
+
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 = ${junkMailboxName}
+
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
+
}
+
+
${lib.optionalString cfg.fullTextSearch.enable ''
+
plugin {
+
plugin = fts fts_xapian
+
fts = xapian
+
fts_xapian = partial=${toString cfg.fullTextSearch.minSize} full=${toString cfg.fullTextSearch.maxSize} attachments=${bool2int cfg.fullTextSearch.indexAttachments} verbose=${bool2int cfg.debug}
+
+
fts_autoindex = ${if cfg.fullTextSearch.autoIndex then "yes" else "no"}
+
+
${lib.strings.concatImapStringsSep "\n" (n: x: "fts_autoindex_exclude${if n==1 then "" else toString n} = ${x}") cfg.fullTextSearch.autoIndexExclude}
+
+
fts_enforced = ${cfg.fullTextSearch.enforced}
+
}
+
+
${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) ''
+
service indexer-worker {
+
vsz_limit = ${toString (cfg.fullTextSearch.memoryLimit*1024*1024)}
+
}
+
''}
+
''}
+
+
lda_mailbox_autosubscribe = yes
+
lda_mailbox_autocreate = yes
+
'';
+
};
+
+
systemd.services.dovecot2 = {
+
preStart = ''
+
${genPasswdScript}
+
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'
+
'';
+
};
+
+
systemd.services.postfix.restartTriggers = [ genPasswdScript ];
+
+
systemd.services.dovecot-fts-xapian-optimize = lib.mkIf (cfg.fullTextSearch.enable && cfg.fullTextSearch.maintenance.enable) {
+
description = "Optimize dovecot indices for fts_xapian";
+
requisite = [ "dovecot2.service" ];
+
after = [ "dovecot2.service" ];
+
startAt = cfg.fullTextSearch.maintenance.onCalendar;
+
serviceConfig = {
+
Type = "oneshot";
+
ExecStart = "${pkgs.dovecot}/bin/doveadm fts optimize -A";
+
PrivateDevices = true;
+
PrivateNetwork = true;
+
ProtectKernelTunables = true;
+
ProtectKernelModules = true;
+
ProtectControlGroups = true;
+
ProtectHome = true;
+
ProtectSystem = true;
+
PrivateTmp = true;
+
};
+
};
+
systemd.timers.dovecot-fts-xapian-optimize = lib.mkIf (cfg.fullTextSearch.enable && cfg.fullTextSearch.maintenance.enable && cfg.fullTextSearch.maintenance.randomizedDelaySec != 0) {
+
timerConfig = {
+
RandomizedDelaySec = cfg.fullTextSearch.maintenance.randomizedDelaySec;
+
};
+
};
+
};
+
}
+15
modules/mailserver/dovecot/imap_sieve/report-ham.sieve
···
+
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}" ];
+7
modules/mailserver/dovecot/imap_sieve/report-spam.sieve
···
+
require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
+
+
if environment :matches "imap.user" "*" {
+
set "username" "${1}";
+
}
+
+
pipe :copy "sa-learn-spam.sh" [ "${username}" ];
+3
modules/mailserver/dovecot/pipe_bin/sa-learn-ham.sh
···
+
#!/bin/bash
+
set -o errexit
+
exec rspamc -h /run/rspamd/worker-controller.sock learn_ham
+3
modules/mailserver/dovecot/pipe_bin/sa-learn-spam.sh
···
+
#!/bin/bash
+
set -o errexit
+
exec rspamc -h /run/rspamd/worker-controller.sock learn_spam
+28
modules/mailserver/environment.nix
···
+
# 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;
+
in
+
{
+
config = with cfg; lib.mkIf enable {
+
environment.systemPackages = with pkgs; [
+
dovecot opendkim openssh postfix rspamd
+
] ++ (if certificateScheme == 2 then [ openssl ] else []);
+
};
+
}
+27
modules/mailserver/kresd.nix
···
+
# nixos-mailserver: a simple mail server
+
# Copyright (C) 2016-2017 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;
+
in
+
{
+
config = lib.mkIf (cfg.enable && cfg.localDnsResolver) {
+
services.kresd.enable = true;
+
};
+
}
+
+32
modules/mailserver/monit.nix
···
+
# 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;
+
in
+
{
+
config = lib.mkIf (cfg.enable && cfg.monitoring.enable) {
+
services.monit = {
+
enable = true;
+
config = ''
+
set alert ${cfg.monitoring.alertAddress}
+
${cfg.monitoring.config}
+
'';
+
};
+
};
+
}
+37
modules/mailserver/networking.nix
···
+
# 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;
+
in
+
{
+
config = with cfg; lib.mkIf (enable && openFirewall) {
+
+
networking.firewall = {
+
allowedTCPPorts = [ 25 ]
+
++ lib.optional enableSubmission 587
+
++ lib.optional enableSubmissionSsl 465
+
++ lib.optional enableImap 143
+
++ lib.optional enableImapSsl 993
+
++ lib.optional enablePop3 110
+
++ lib.optional enablePop3Ssl 995
+
++ lib.optional enableManageSieve 4190
+
++ lib.optional (certificateScheme == 3) 80;
+
};
+
};
+
}
+44
modules/mailserver/nginx.nix
···
+
# 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, ... }:
+
+
with (import ./common.nix { inherit config; });
+
+
let
+
cfg = config.mailserver;
+
acmeRoot = "/var/lib/acme/acme-challenge";
+
in
+
{
+
config = lib.mkIf (cfg.enable && cfg.certificateScheme == 3) {
+
services.nginx = {
+
enable = true;
+
virtualHosts."${cfg.fqdn}" = {
+
serverName = cfg.fqdn;
+
serverAliases = cfg.certificateDomains;
+
forceSSL = true;
+
enableACME = true;
+
acmeRoot = acmeRoot;
+
};
+
};
+
+
security.acme.certs."${cfg.fqdn}".reloadServices = [
+
"postfix.service"
+
"dovecot2.service"
+
];
+
};
+
}
+88
modules/mailserver/opendkim.nix
···
+
# 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}" ]
+
then
+
${pkgs.opendkim}/bin/opendkim-genkey -s "${cfg.dkimSelector}" \
+
-d "${dom}" \
+
--bits="${toString cfg.dkimKeyBits}" \
+
--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);
+
+
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;
+
keyPath = cfg.dkimKeyDirectory;
+
domains = "csl:${builtins.concatStringsSep "," cfg.domains}";
+
configFile = pkgs.writeText "opendkim.conf" (''
+
Canonicalization ${cfg.dkimHeaderCanonicalization}/${cfg.dkimBodyCanonicalization}
+
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 = [ "${dkimGroup}" ];
+
};
+
systemd.services.opendkim = {
+
preStart = lib.mkForce createAllCerts;
+
serviceConfig = {
+
ExecStart = lib.mkForce "${pkgs.opendkim}/bin/opendkim ${escapeShellArgs args}";
+
PermissionsStartOnly = lib.mkForce false;
+
};
+
};
+
systemd.tmpfiles.rules = [
+
"d '${cfg.dkimKeyDirectory}' - ${dkimUser} ${dkimGroup} - -"
+
];
+
};
+
}
+46
modules/mailserver/post-upgrade-check.nix
···
+
# 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, ... }:
+
+
with lib;
+
+
let
+
cfg = config.mailserver;
+
in
+
{
+
config = mkIf (cfg.enable && cfg.rebootAfterKernelUpgrade.enable) {
+
systemd.services.nixos-upgrade.serviceConfig.ExecStartPost = pkgs.writeScript "post-upgrade-check" ''
+
#!${pkgs.stdenv.shell}
+
+
# Checks whether the "current" kernel is different from the booted kernel
+
# and then triggers a reboot so that the "current" kernel will be the booted one.
+
# This is just an educated guess. If the links do not differ the kernels might still be different, according to spacefrogg in #nixos.
+
+
current=$(readlink -f /run/current-system/kernel)
+
booted=$(readlink -f /run/booted-system/kernel)
+
+
if [ "$current" == "$booted" ]; then
+
echo "kernel version seems unchanged, skipping reboot" | systemd-cat --priority 4 --identifier "post-upgrade-check";
+
else
+
echo "kernel path changed, possibly a new version" | systemd-cat --priority 2 --identifier "post-upgrade-check"
+
echo "$booted" | systemd-cat --priority 2 --identifier "post-upgrade-kernel-check"
+
echo "$current" | systemd-cat --priority 2 --identifier "post-upgrade-kernel-check"
+
${cfg.rebootAfterKernelUpgrade.method}
+
fi
+
'';
+
};
+
}
+269
modules/mailserver/postfix.nix
···
+
# 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, ... }:
+
+
with (import ./common.nix { inherit config pkgs lib; });
+
+
let
+
inherit (lib.strings) concatStringsSep;
+
cfg = config.mailserver;
+
+
# 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))
+
cfg.loginAccounts));
+
+
# catchAllPostfix :: Map String [String]
+
catchAllPostfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
+
(name: value:
+
let to = name;
+
in map (from: {"@${from}" = to;}) value.catchAll)
+
cfg.loginAccounts));
+
+
# all_valiases_postfix :: Map String [String]
+
all_valiases_postfix = mergeLookupTables [valiases_postfix extra_valiases_postfix];
+
+
# attrsToLookupTable :: Map String (Either String [ String ]) -> Map String [String]
+
attrsToLookupTable = aliases: let
+
lookupTables = lib.mapAttrsToList (from: to: {"${from}" = to;}) aliases;
+
in mergeLookupTables lookupTables;
+
+
# extra_valiases_postfix :: Map String [String]
+
extra_valiases_postfix = attrsToLookupTable cfg.extraVirtualAliases;
+
+
# forwards :: Map String [String]
+
forwards = attrsToLookupTable cfg.forwards;
+
+
# 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 catchAllPostfix]);
+
in builtins.toFile "valias" content;
+
+
# denied_recipients_postfix :: [ String ]
+
denied_recipients_postfix = (map
+
(acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}")
+
(lib.filter (acct: acct.sendOnly) (lib.attrValues cfg.loginAccounts)));
+
denied_recipients_file = builtins.toFile "denied_recipients" (lib.concatStringsSep "\n" denied_recipients_postfix);
+
+
reject_senders_postfix = (map
+
(sender:
+
"${sender} REJECT")
+
(cfg.rejectSender));
+
reject_senders_file = builtins.toFile "reject_senders" (lib.concatStringsSep "\n" (reject_senders_postfix)) ;
+
+
reject_recipients_postfix = (map
+
(recipient:
+
"${recipient} REJECT")
+
(cfg.rejectRecipients));
+
# rejectRecipients :: [ Path ]
+
reject_recipients_file = builtins.toFile "reject_recipients" (lib.concatStringsSep "\n" (reject_recipients_postfix)) ;
+
+
# vhosts_file :: Path
+
vhosts_file = builtins.toFile "vhosts" (concatStringsSep "\n" cfg.domains);
+
+
# vaccounts_file :: Path
+
# see
+
# https://blog.grimneko.de/2011/12/24/a-bunch-of-tips-for-improving-your-postfix-setup/
+
# for details on how this file looks. By using the same file as valiases,
+
# every alias is owned (uniquely) by its user.
+
# The user's own address is already in all_valiases_postfix.
+
vaccounts_file = builtins.toFile "vaccounts" (lookupTableToString all_valiases_postfix);
+
+
submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" (''
+
# Removes sensitive headers from mails handed in via the submission port.
+
# See https://thomas-leister.de/mailserver-debian-stretch/
+
# Uses "pcre" style regex.
+
+
/^Received:/ IGNORE
+
/^X-Originating-IP:/ IGNORE
+
/^X-Mailer:/ IGNORE
+
/^User-Agent:/ IGNORE
+
/^X-Enigmail:/ IGNORE
+
'' + lib.optionalString cfg.rewriteMessageId ''
+
+
# Replaces the user submitted hostname with the server's FQDN to hide the
+
# user's host or network.
+
+
/^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;
+
+
mappedFile = name: "hash:/var/lib/postfix/conf/${name}";
+
+
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";
+
};
+
in
+
{
+
config = with cfg; lib.mkIf enable {
+
+
services.postfix = {
+
enable = true;
+
hostname = "${sendingFqdn}";
+
networksStyle = "host";
+
mapFiles."valias" = valiases_file;
+
mapFiles."vaccounts" = vaccounts_file;
+
mapFiles."denied_recipients" = denied_recipients_file;
+
mapFiles."reject_senders" = reject_senders_file;
+
mapFiles."reject_recipients" = reject_recipients_file;
+
sslCert = certificatePath;
+
sslKey = keyPath;
+
enableSubmission = cfg.enableSubmission;
+
enableSubmissions = cfg.enableSubmissionSsl;
+
virtual = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix forwards]);
+
+
config = {
+
# Extra Config
+
mydestination = "";
+
recipient_delimiter = cfg.recipientDelimiter;
+
smtpd_banner = "${fqdn} ESMTP NO UCE";
+
disable_vrfy_command = true;
+
message_size_limit = toString cfg.messageSizeLimit;
+
+
# virtual mail system
+
virtual_uid_maps = "static:5000";
+
virtual_gid_maps = "static:5000";
+
virtual_mailbox_base = mailDirectory;
+
virtual_mailbox_domains = vhosts_file;
+
virtual_mailbox_maps = mappedFile "valias";
+
virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
+
# Avoid leakage of X-Original-To, X-Delivered-To headers between recipients
+
lmtp_destination_recipient_limit = "1";
+
+
# sasl with dovecot
+
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";
+
+
# reject selected senders
+
smtpd_sender_restrictions = [
+
"check_sender_access ${mappedFile "reject_senders"}"
+
];
+
+
# quota and spf checking
+
smtpd_recipient_restrictions = [
+
"check_recipient_access ${mappedFile "denied_recipients"}"
+
"check_recipient_access ${mappedFile "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
+
# 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";
+
+
# Disable obselete protocols
+
smtpd_tls_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
+
smtp_tls_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
+
smtpd_tls_mandatory_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
+
smtp_tls_mandatory_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
+
+
smtp_tls_ciphers = "high";
+
smtpd_tls_ciphers = "high";
+
smtp_tls_mandatory_ciphers = "high";
+
smtpd_tls_mandatory_ciphers = "high";
+
+
# Disable deprecated ciphers
+
smtpd_tls_mandatory_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
+
smtpd_tls_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
+
smtp_tls_mandatory_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
+
smtp_tls_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
+
+
tls_preempt_cipherlist = true;
+
+
# Allowing AUTH on a non encrypted connection poses a security risk
+
smtpd_tls_auth_only = true;
+
# Log only a summary message on TLS handshake completion
+
smtpd_tls_loglevel = "1";
+
+
# Configure a non blocking source of randomness
+
tls_random_source = "dev:/dev/urandom";
+
+
smtpd_milters = smtpdMilters;
+
non_smtpd_milters = lib.mkIf cfg.dkimSigning ["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}"];
+
};
+
};
+
};
+
};
+
}
+59
modules/mailserver/rsnapshot.nix
···
+
# 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, ... }:
+
+
with lib;
+
+
let
+
cfg = config.mailserver;
+
+
preexecDefined = cfg.backup.cmdPreexec != null;
+
preexecWrapped = pkgs.writeScript "rsnapshot-preexec.sh" ''
+
#!${pkgs.stdenv.shell}
+
set -e
+
+
${cfg.backup.cmdPreexec}
+
'';
+
preexecString = optionalString preexecDefined "cmd_preexec ${preexecWrapped}";
+
+
postexecDefined = cfg.backup.cmdPostexec != null;
+
postexecWrapped = pkgs.writeScript "rsnapshot-postexec.sh" ''
+
#!${pkgs.stdenv.shell}
+
set -e
+
+
${cfg.backup.cmdPostexec}
+
'';
+
postexecString = optionalString postexecDefined "cmd_postexec ${postexecWrapped}";
+
in {
+
config = mkIf (cfg.enable && cfg.backup.enable) {
+
services.rsnapshot = {
+
enable = true;
+
cronIntervals = cfg.backup.cronIntervals;
+
# rsnapshot expects intervals shortest first, e.g. hourly first, then daily.
+
# tabs must separate all elements
+
extraConfig = ''
+
${preexecString}
+
${postexecString}
+
snapshot_root ${cfg.backup.snapshotRoot}/
+
retain hourly ${toString cfg.backup.retain.hourly}
+
retain daily ${toString cfg.backup.retain.daily}
+
retain weekly ${toString cfg.backup.retain.weekly}
+
backup ${cfg.mailDirectory}/ localhost/
+
'';
+
};
+
};
+
}
+119
modules/mailserver/rspamd.nix
···
+
# 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 = "rspamd.service";
+
in
+
{
+
config = with cfg; lib.mkIf enable {
+
services.rspamd = {
+
enable = true;
+
inherit debug;
+
locals = {
+
"milter_headers.conf" = { text = ''
+
extended_spam_headers = yes;
+
''; };
+
"redis.conf" = { text = ''
+
servers = "${cfg.redis.address}:${toString cfg.redis.port}";
+
'' + (lib.optionalString (cfg.redis.password != null) ''
+
password = "${cfg.redis.password}";
+
''); };
+
"classifier-bayes.conf" = { text = ''
+
cache {
+
backend = "redis";
+
}
+
''; };
+
"antivirus.conf" = lib.mkIf cfg.virusScanning { text = ''
+
clamav {
+
action = "reject";
+
symbol = "CLAM_VIRUS";
+
type = "clamav";
+
log_clean = true;
+
servers = "/run/clamav/clamd.ctl";
+
scan_mime_parts = false; # scan mail as a whole unit, not parts. seems to be needed to work at all
+
}
+
''; };
+
"dkim_signing.conf" = { text = ''
+
# Disable outbound email signing, we use opendkim for 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; # 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 = [];
+
extraConfig = ''
+
static_dir = "''${WWWDIR}"; # Serve the web UI static assets
+
'';
+
};
+
+
};
+
+
services.redis.servers.rspamd = {
+
enable = lib.mkDefault true;
+
port = lib.mkDefault 6380;
+
};
+
+
systemd.services.rspamd = {
+
requires = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
+
after = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
+
};
+
+
systemd.services.postfix = {
+
after = [ rspamdSocket ];
+
requires = [ rspamdSocket ];
+
};
+
+
users.extraUsers.${postfixCfg.user}.extraGroups = [ rspamdCfg.group ];
+
};
+
}
+
+83
modules/mailserver/systemd.nix
···
+
# 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;
+
certificatesDeps =
+
if cfg.certificateScheme == 1 then
+
[]
+
else if cfg.certificateScheme == 2 then
+
[ "mailserver-selfsigned-certificate.service" ]
+
else
+
[ "acme-finished-${cfg.fqdn}.target" ];
+
in
+
{
+
config = with cfg; lib.mkIf enable {
+
# Create self signed certificate
+
systemd.services.mailserver-selfsigned-certificate = lib.mkIf (cfg.certificateScheme == 2) {
+
after = [ "local-fs.target" ];
+
script = ''
+
# Create certificates if they do not exist yet
+
dir="${cfg.certificateDirectory}"
+
fqdn="${cfg.fqdn}"
+
[[ $fqdn == /* ]] && fqdn=$(< "$fqdn")
+
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
+
'';
+
serviceConfig = {
+
Type = "oneshot";
+
PrivateTmp = true;
+
};
+
};
+
+
# Create maildir folder before dovecot startup
+
systemd.services.dovecot2 = {
+
wants = certificatesDeps;
+
after = certificatesDeps;
+
preStart = let
+
directories = lib.strings.escapeShellArgs (
+
[ mailDirectory ]
+
++ lib.optional (cfg.indexDir != null) cfg.indexDir
+
);
+
in ''
+
# Create mail directory and set permissions. See
+
# <http://wiki2.dovecot.org/SharedMailboxes/Permissions>.
+
mkdir -p ${directories}
+
chgrp "${vmailGroupName}" ${directories}
+
chmod 02770 ${directories}
+
'';
+
};
+
+
# Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work
+
systemd.services.postfix = {
+
wants = certificatesDeps;
+
after = [ "dovecot2.service" ]
+
++ lib.optional cfg.dkimSigning "opendkim.service"
+
++ certificatesDeps;
+
requires = [ "dovecot2.service" ]
+
++ lib.optional cfg.dkimSigning "opendkim.service";
+
};
+
};
+
}
+101
modules/mailserver/users.nix
···
+
# 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, ... }:
+
+
with config.mailserver;
+
+
let
+
vmail_user = {
+
name = vmailUserName;
+
isSystemUser = true;
+
uid = vmailUID;
+
home = mailDirectory;
+
createHome = true;
+
group = vmailGroupName;
+
};
+
+
+
virtualMailUsersActivationScript = pkgs.writeScript "activate-virtual-mail-users" ''
+
#!${pkgs.stdenv.shell}
+
+
set -euo pipefail
+
+
# Create directory to store user sieve scripts if it doesn't exist
+
if (! test -d "${sieveDirectory}"); then
+
mkdir "${sieveDirectory}"
+
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}"
+
chmod 770 "${sieveDirectory}"
+
fi
+
+
# Copy user's sieve script to the correct location (if it exists). If it
+
# is null, remove the file.
+
${lib.concatMapStringsSep "\n" ({ name, sieveScript }:
+
if lib.isString sieveScript then ''
+
if (! test -d "${sieveDirectory}/${name}"); then
+
mkdir -p "${sieveDirectory}/${name}"
+
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}"
+
chmod 770 "${sieveDirectory}/${name}"
+
fi
+
cat << 'EOF' > "${sieveDirectory}/${name}/default.sieve"
+
${sieveScript}
+
EOF
+
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}/default.sieve"
+
'' else ''
+
if (test -f "${sieveDirectory}/${name}/default.sieve"); then
+
rm "${sieveDirectory}/${name}/default.sieve"
+
fi
+
if (test -f "${sieveDirectory}/${name}.svbin"); then
+
rm "${sieveDirectory}/${name}/default.svbin"
+
fi
+
'') (map (user: { inherit (user) name sieveScript; })
+
(lib.attrValues loginAccounts))}
+
'';
+
in {
+
config = lib.mkIf enable {
+
# assert that all accounts provide a password
+
assertions = (map (acct: {
+
assertion = (acct.hashedPassword != null || acct.hashedPasswordFile != null);
+
message = "${acct.name} must provide either a hashed password or a password hash file";
+
}) (lib.attrValues loginAccounts));
+
+
# warn for accounts that specify both password and file
+
warnings = (map
+
(acct: "${acct.name} specifies both a password hash and hash file; hash file will be used")
+
(lib.filter
+
(acct: (acct.hashedPassword != null && acct.hashedPasswordFile != null))
+
(lib.attrValues loginAccounts)));
+
+
# set the vmail gid to a specific value
+
users.groups = {
+
"${vmailGroupName}" = { gid = vmailUID; };
+
};
+
+
# define all users
+
users.users = {
+
"${vmail_user.name}" = lib.mkForce vmail_user;
+
};
+
+
systemd.services.activate-virtual-mail-users = {
+
wantedBy = [ "multi-user.target" ];
+
before = [ "dovecot2.service" ];
+
serviceConfig = {
+
ExecStart = virtualMailUsersActivationScript;
+
};
+
enable = true;
+
};
+
};
+
}
+66
modules/wireguard/default.nix
···
+
{ pkgs, config, lib, ... }:
+
+
with lib;
+
+
let cfg = config.wireguard; in
+
{
+
options.wireguard = {
+
enable = lib.mkEnableOption "wireguard";
+
server = mkOption {
+
type = with types; bool;
+
default = cfg.hosts.${config.networking.hostName}.server;
+
};
+
hosts =
+
let hostOps = { ... }: {
+
options = {
+
ip = mkOption {
+
type = types.str;
+
};
+
publicKey = mkOption {
+
type = types.str;
+
};
+
server = mkOption {
+
type = types.bool;
+
default = false;
+
};
+
};
+
};
+
in mkOption {
+
type = with types; attrsOf (submodule hostOps);
+
};
+
};
+
+
config = mkIf cfg.enable {
+
environment.systemPackages = with pkgs; [ wireguard-tools ];
+
networking = {
+
# populate /etc/hosts with hostnames and IPs
+
extraHosts = builtins.concatStringsSep "\n" (
+
attrsets.mapAttrsToList (
+
hostName: values: "${values.ip} ${hostName}"
+
) cfg.hosts
+
);
+
+
firewall = {
+
allowedUDPPorts = [ 51820 ];
+
checkReversePath = false;
+
};
+
+
wireguard = {
+
enable = true;
+
interfaces.wg0 = {
+
ips = [ "${cfg.hosts.${config.networking.hostName}.ip}/24" ];
+
listenPort = 51820;
+
privateKeyFile = "${config.custom.secretsDir}/wireguard-key-${config.networking.hostName}";
+
peers = mkIf (!cfg.server) [
+
{
+
allowedIPs = [ "10.0.0.0/24" ];
+
publicKey = "${cfg.hosts.vps.publicKey}";
+
endpoint = "${config.hosting.serverIpv4}:51820";
+
persistentKeepalive = mkIf (config.networking.hostName == "rasp-pi") 25;
+
}
+
];
+
};
+
};
+
};
+
};
+
}
+10
modules/wireguard/generate-key.sh
···
+
#!/usr/bin/env bash
+
+
dir=$1
+
file=wireguard_key_"$(hostname)"
+
A
+
umask 077
+
chmod 700 "$dir"
+
+
wg genkey > "$dir/$file"
+
wg pubkey < "$dir/$file"
+39
modules/wireguard/server.nix
···
+
{ pkgs, config, lib, ... }:
+
+
let cfg = config.wireguard; in
+
{
+
networking = lib.mkIf (cfg.enable && cfg.server) {
+
nat = {
+
enable = true;
+
externalInterface = "enp1s0";
+
internalInterfaces = [ "wg0" ];
+
};
+
firewall = {
+
extraCommands = ''
+
iptables -I FORWARD -i wg0 -o wg0 -j ACCEPT
+
'';
+
trustedInterfaces = [ "wg0" ];
+
};
+
+
wireguard.interfaces.wg0 = {
+
# Route from wireguard to public internet, allowing server to act as VPN
+
postSetup = ''
+
${pkgs.iptables}/bin/iptables -t nat -A POSTROUTING -o enp1s0 -j MASQUERADE
+
'';
+
+
postShutdown = ''
+
${pkgs.iptables}/bin/iptables -t nat -D POSTROUTING -o enp1s0 -j MASQUERADE
+
'';
+
+
# add clients
+
peers = with lib.attrsets;
+
mapAttrsToList (
+
hostName: values: {
+
allowedIPs = [ "${values.ip}/32" ];
+
publicKey = values.publicKey;
+
persistentKeepalive = lib.mkIf (hostName == "rasp-pi") 25;
+
}
+
) cfg.hosts;
+
};
+
};
+
}