Begin attempting to configure stalwart
Some checks failed
/ dev-shell (push) Successful in 1m50s
/ rust-packages (push) Successful in 11m58s
/ check (push) Failing after 1m16s
/ systems (push) Successful in 38m10s
/ terraform-providers (push) Successful in 15m4s

This commit is contained in:
Kaare Hoff Skovgaard 2025-07-27 00:39:55 +02:00
parent 6e665a70bc
commit c97b19c495
Signed by: khs
GPG key ID: C7D890804F01E9F0
4 changed files with 265 additions and 80 deletions

View file

@ -1,12 +1,84 @@
{
config,
lib,
inputs,
pkgs,
...
}:
let
cfg = config.khscodes.infrastructure.mailserver;
fqdn = config.khscodes.networking.fqdn;
adminCredentialsFile = "/etc/stalwart/admin-pw";
stalwart-spam-filter = pkgs.callPackage ./stalwart-spam-filter.nix { };
ifthen = condition: expr: {
"if" = condition;
"then" = expr;
};
otherwise = expr: { "else" = expr; };
authDkimForDomain = domain: [
(ifthen "sender_domain = '${domain}'" "['${domain}_rsa', '${domain}_ed25519']")
];
authDkim = lib.lists.flatten (lib.lists.map authDkimForDomain cfg.domains);
signatureForDomain = domain: [
{
name = "${domain}_rsa";
value = {
inherit domain;
private-key = "%{file:/run/secret/dkim/${domain}.snm_rsa.key}";
selector = "snm_rsa";
headers = [
"From"
"To"
"Cc"
"Date"
"Subject"
"Message-ID"
"Organization"
"MIME-Version"
"Content-Type"
"In-Reply-To"
"References"
"List-Id"
"User-Agent"
"Thread-Topic"
"Thread-Index"
];
algorithm = "rsa-sha256";
canonicalization = "relaxed/relaxed";
report = true;
};
}
{
name = "${domain}_ed25519";
value = {
inherit domain;
private-key = "%{file:/run/secret/dkim/${domain}.snm_ed25519.key}";
selector = "snm_ed25519";
headers = [
"From"
"To"
"Cc"
"Date"
"Subject"
"Message-ID"
"Organization"
"MIME-Version"
"Content-Type"
"In-Reply-To"
"References"
"List-Id"
"User-Agent"
"Thread-Topic"
"Thread-Index"
];
algorithm = "ed25519-sha256";
canonicalization = "relaxed/relaxed";
report = true;
};
}
];
dkimSignatures = {
signature = lib.listToAttrs (lib.lists.flatten (lib.lists.map signatureForDomain cfg.domains));
};
in
{
options.khscodes.infrastructure.mailserver = {
@ -18,7 +90,6 @@ in
};
imports = [
./dkim.nix
inputs.simple-nixos-mailserver.nixosModules.mailserver
];
config = lib.mkIf cfg.enable {
# TODO: Include a similiar rule for openstack
@ -49,43 +120,87 @@ in
}
)
];
mailserver = {
enable = true;
certificateScheme = "acme";
fqdn = config.khscodes.networking.fqdn;
domains = cfg.domains;
environment.etc."stalwart/admin-pw".text = "test";
networking.firewall.allowedTCPPorts = [
25
465
993
];
security.acme.certs."${fqdn}" = {
# Not sure if this does an actual reload (which is not supported apparently),
# or if it does a full restart (which would be needed).
# If it doesn't work, then I should look into using post run to query the API.
reloadServices = [ "stalwart-mail.service" ];
};
services.prometheus.exporters.postfix = {
enable = true;
users.users.stalwart-mail.extraGroups = [ config.security.acme.certs.${fqdn}.group ];
systemd.services.stalwart-mail = {
serviceConfig = {
ReadOnlyPaths = [ config.security.acme.certs."${fqdn}".directory ];
};
wants = [ "acme-${fqdn}.service" ];
after = [ "acme-${fqdn}.service" ];
};
khscodes.infrastructure.vault-prometheus-sender.exporters.enabled = [ "postfix" ];
services.fail2ban.jails = {
postfix = {
settings = {
enabled = true;
mode = "aggressive";
port = "smtp,submission,imap,imaps,pop3,pop3s";
findtime = 600;
bantime = "1d";
maxretry = 3;
services.stalwart-mail = {
enable = true;
package = pkgs.stalwart-mail;
openFirewall = false;
settings = {
http = {
url = "https://${fqdn}";
use-x-forwarded = true;
};
};
dovecot = {
settings = {
enabled = true;
mode = "aggressive";
findtime = 600;
bantime = "1d";
maxretry = 3;
server = {
hostname = fqdn;
tls = {
enable = true;
certificate = "default";
implicit = true;
ignore-client-order = true;
};
listener = {
smtp = {
protocol = "smtp";
bind = "[::]:25";
};
submissions = {
bind = "[::]:465";
protocol = "smtp";
};
imaps = {
bind = "[::]:993";
protocol = "imap";
};
management = {
bind = [ "127.0.0.1:8080" ];
protocol = "http";
};
};
};
};
roundcube-auth = {
settings = {
enabled = true;
findtime = 600;
maxretry = 7;
# There's a bug in 25.05 that references the wrong file. Fixed in master/unstable.
spam-filter.resource = "file://${stalwart-spam-filter}/spam-filter.toml";
authentication.fallback-admin = {
user = "admin";
secret = "%{file:/etc/stalwart/admin-pw}%";
};
};
certificate.default = {
cert = "%{file://${config.security.acme.certs."${fqdn}".directory}/fullchain.pem}";
private-key = "%{file://${config.security.acme.certs."${fqdn}".directory}/key.pem}";
default = true;
};
auth.dkim = {
sign = authDkim ++ [
(otherwise false)
];
};
directory.memory = {
type = "memory";
domains = [
"agerlinskovgaard.dk"
"agerlin-skovgaard.dk"
];
};
}
// dkimSignatures;
};
};
}

View file

@ -52,25 +52,35 @@ in
};
};
config = lib.mkIf (cfg.enable) {
mailserver = {
dkimSigning = false;
};
services.rspamd.locals."dkim_signing.conf" = lib.mkForce {
text = ''
enabled = true;
domain {
${lib.strings.concatStringsSep "\n " (lib.lists.map dkimSigningForDomain cfg.domains)}
}
'';
};
systemd.services.rspamd = {
# mailserver = {
# dkimSigning = false;
# };
# services.rspamd.locals."dkim_signing.conf" = lib.mkForce {
# text = ''
# enabled = true;
# domain {
# ${lib.strings.concatStringsSep "\n " (lib.lists.map dkimSigningForDomain cfg.domains)}
# }
# '';
# };
# systemd.services.rspamd = {
# unitConfig = {
# ConditionPathExists = domainKeyPaths;
# };
# };
# systemd.services.postfix = {
# unitConfig = {
# ConditionPathExists = domainKeyPaths;
# };
# };
systemd.services.stalwart-mail = {
unitConfig = {
ConditionPathExists = domainKeyPaths;
};
};
systemd.services.postfix = {
unitConfig = {
ConditionPathExists = domainKeyPaths;
serviceConfig = {
ReadOnlyPaths = [
"/run/secret/dkim"
];
};
};
khscodes.infrastructure.vault-server-approle.policy = {
@ -121,8 +131,7 @@ in
}) cfg.domains)
++ (lib.lists.map (domain: {
fqdn = domain;
# TODO: Use something here that doesn't require knowing the IP, such that we don't tie this down to hcloud instances.
content = ''"v=spf1 ip4:${config.khscodes.hcloud.output.server.compute.ipv4_address} ip6:${config.khscodes.hcloud.output.server.compute.ipv6_address} -all"'';
content = ''"v=spf1 mx -all"'';
ttl = 600;
}) cfg.domains)
++ (lib.lists.map (domain: {
@ -159,7 +168,11 @@ in
}
)
];
systemd.services.stalwart-mail.serviceConfig = {
ReadWritePaths = [
"/run/secret/dkim"
];
};
khscodes.services.vault-agent.templates = lib.lists.flatten (
lib.lists.map (domain: [
{
@ -170,11 +183,10 @@ in
'';
destination = rsaKeyPath domain;
perms = "0600";
owner = "rspamd";
group = "rspamd";
owner = "stalwart-mail";
group = "stalwart-mail";
restartUnits = [
"rspamd.service"
"postfix.service"
"stalwart-mail.service"
];
}
{
@ -185,11 +197,10 @@ in
'';
destination = ed25519KeyPath domain;
perms = "0600";
owner = "rspamd";
group = "rspamd";
owner = "stalwart-mail";
group = "stalwart-mail";
restartUnits = [
"rspamd.service"
"postfix.service"
"stalwart-mail.service"
];
}
]) cfg.domains

View file

@ -0,0 +1,43 @@
{
lib,
fetchFromGitHub,
stdenv,
stalwart-mail,
nix-update-script,
}:
stdenv.mkDerivation (finalAttrs: {
pname = "spam-filter";
version = "2.0.3";
src = fetchFromGitHub {
owner = "stalwartlabs";
repo = "spam-filter";
tag = "v${finalAttrs.version}";
hash = "sha256-NhD/qUiGhgESwR2IOzAHfDATRlgWMcCktlktvVfDONk=";
};
buildPhase = ''
bash ./build.sh
'';
installPhase = ''
mkdir -p $out
cp spam-filter.toml $out/
'';
passthru = {
updateScript = nix-update-script { };
};
meta = {
description = "Secure & modern all-in-one mail server Stalwart (spam-filter module)";
homepage = "https://github.com/stalwartlabs/spam-filter";
changelog = "https://github.com/stalwartlabs/spam-filter/blob/${finalAttrs.src.tag}/CHANGELOG.md";
license = with lib.licenses; [
mit
asl20
];
inherit (stalwart-mail.meta) maintainers;
};
})

View file

@ -1,5 +1,6 @@
{
inputs,
config,
...
}:
{
@ -9,7 +10,7 @@
];
khscodes.infrastructure.provisioning.pre.modules = [
(
{ config, ... }:
{ ... }:
{
khscodes.vault = {
enable = true;
@ -22,11 +23,6 @@
description = "Secrets used for mx.kaareskovgaard.net";
};
};
resource.hcloud_volume.mail_disk = {
name = "mx.kaareskovgaard.net-mail1";
size = 20;
server_id = config.khscodes.output.server.compute.id;
};
}
)
];
@ -50,21 +46,41 @@
};
};
};
# services.roundcube = {
# enable = true;
# hostName = "mail.kaareskovgaard.net";
# configureNginx = true;
# extraConfig = ''
# # starttls needed for authentication, so the fqdn required to match
# # the certificate
# $config['smtp_host'] = "tls://${config.mailserver.fqdn}";
# $config['smtp_user'] = "%u";
# $config['smtp_pass'] = "%p";
# '';
# };
services.roundcube = {
enable = true;
hostName = "mail.kaareskovgaard.net";
configureNginx = true;
extraConfig = ''
# starttls needed for authentication, so the fqdn required to match
# the certificate
$config['smtp_host'] = "ssl://${config.khscodes.networking.fqdn}:465";
$config['imap_host'] = "ssl://${config.khscodes.networking.fqdn}:993";
'';
};
khscodes.services.nginx = {
enable = true;
# virtualHosts."mail.kaareskovgaard.net" = { };
virtualHosts."mx.kaareskovgaard.net" = {
locations."/" = {
proxyPass = "http://127.0.0.1:8080";
proxyWebsockets = true;
recommendedProxySettings = true;
};
};
virtualHosts."mail.kaareskovgaard.net" = { };
virtualHosts."autoconfig.kaareskovgaard.net" = {
locations."/" = {
proxyPass = "http://127.0.0.1:8080";
proxyWebsockets = true;
recommendedProxySettings = true;
};
};
virtualHosts."autodiscover.kaareskovgaard.net" = {
locations."/" = {
proxyPass = "http://127.0.0.1:8080";
proxyWebsockets = true;
recommendedProxySettings = true;
};
};
};
khscodes.networking.fqdn = "mx.kaareskovgaard.net";
system.stateVersion = "25.05";