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, config,
lib, lib,
inputs, pkgs,
... ...
}: }:
let let
cfg = config.khscodes.infrastructure.mailserver; cfg = config.khscodes.infrastructure.mailserver;
fqdn = config.khscodes.networking.fqdn; 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 in
{ {
options.khscodes.infrastructure.mailserver = { options.khscodes.infrastructure.mailserver = {
@ -18,7 +90,6 @@ in
}; };
imports = [ imports = [
./dkim.nix ./dkim.nix
inputs.simple-nixos-mailserver.nixosModules.mailserver
]; ];
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
# TODO: Include a similiar rule for openstack # TODO: Include a similiar rule for openstack
@ -49,43 +120,87 @@ in
} }
) )
]; ];
mailserver = { 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" ];
};
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" ];
};
services.stalwart-mail = {
enable = true; enable = true;
certificateScheme = "acme"; package = pkgs.stalwart-mail;
fqdn = config.khscodes.networking.fqdn; openFirewall = false;
domains = cfg.domains; settings = {
http = {
url = "https://${fqdn}";
use-x-forwarded = true;
}; };
services.prometheus.exporters.postfix = { server = {
hostname = fqdn;
tls = {
enable = true; enable = true;
certificate = "default";
implicit = true;
ignore-client-order = true;
}; };
khscodes.infrastructure.vault-prometheus-sender.exporters.enabled = [ "postfix" ]; listener = {
services.fail2ban.jails = { smtp = {
postfix = { protocol = "smtp";
settings = { bind = "[::]:25";
enabled = true; };
mode = "aggressive"; submissions = {
port = "smtp,submission,imap,imaps,pop3,pop3s"; bind = "[::]:465";
findtime = 600; protocol = "smtp";
bantime = "1d"; };
maxretry = 3; imaps = {
bind = "[::]:993";
protocol = "imap";
};
management = {
bind = [ "127.0.0.1:8080" ];
protocol = "http";
}; };
}; };
dovecot = {
settings = {
enabled = true;
mode = "aggressive";
findtime = 600;
bantime = "1d";
maxretry = 3;
}; };
# 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}%";
}; };
roundcube-auth = { certificate.default = {
settings = { cert = "%{file://${config.security.acme.certs."${fqdn}".directory}/fullchain.pem}";
enabled = true; private-key = "%{file://${config.security.acme.certs."${fqdn}".directory}/key.pem}";
findtime = 600; default = true;
maxretry = 7;
}; };
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) { config = lib.mkIf (cfg.enable) {
mailserver = { # mailserver = {
dkimSigning = false; # dkimSigning = false;
}; # };
services.rspamd.locals."dkim_signing.conf" = lib.mkForce { # services.rspamd.locals."dkim_signing.conf" = lib.mkForce {
text = '' # text = ''
enabled = true; # enabled = true;
domain { # domain {
${lib.strings.concatStringsSep "\n " (lib.lists.map dkimSigningForDomain cfg.domains)} # ${lib.strings.concatStringsSep "\n " (lib.lists.map dkimSigningForDomain cfg.domains)}
} # }
''; # '';
}; # };
systemd.services.rspamd = { # systemd.services.rspamd = {
# unitConfig = {
# ConditionPathExists = domainKeyPaths;
# };
# };
# systemd.services.postfix = {
# unitConfig = {
# ConditionPathExists = domainKeyPaths;
# };
# };
systemd.services.stalwart-mail = {
unitConfig = { unitConfig = {
ConditionPathExists = domainKeyPaths; ConditionPathExists = domainKeyPaths;
}; };
}; serviceConfig = {
systemd.services.postfix = { ReadOnlyPaths = [
unitConfig = { "/run/secret/dkim"
ConditionPathExists = domainKeyPaths; ];
}; };
}; };
khscodes.infrastructure.vault-server-approle.policy = { khscodes.infrastructure.vault-server-approle.policy = {
@ -121,8 +131,7 @@ in
}) cfg.domains) }) cfg.domains)
++ (lib.lists.map (domain: { ++ (lib.lists.map (domain: {
fqdn = 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 mx -all"'';
content = ''"v=spf1 ip4:${config.khscodes.hcloud.output.server.compute.ipv4_address} ip6:${config.khscodes.hcloud.output.server.compute.ipv6_address} -all"'';
ttl = 600; ttl = 600;
}) cfg.domains) }) cfg.domains)
++ (lib.lists.map (domain: { ++ (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 ( khscodes.services.vault-agent.templates = lib.lists.flatten (
lib.lists.map (domain: [ lib.lists.map (domain: [
{ {
@ -170,11 +183,10 @@ in
''; '';
destination = rsaKeyPath domain; destination = rsaKeyPath domain;
perms = "0600"; perms = "0600";
owner = "rspamd"; owner = "stalwart-mail";
group = "rspamd"; group = "stalwart-mail";
restartUnits = [ restartUnits = [
"rspamd.service" "stalwart-mail.service"
"postfix.service"
]; ];
} }
{ {
@ -185,11 +197,10 @@ in
''; '';
destination = ed25519KeyPath domain; destination = ed25519KeyPath domain;
perms = "0600"; perms = "0600";
owner = "rspamd"; owner = "stalwart-mail";
group = "rspamd"; group = "stalwart-mail";
restartUnits = [ restartUnits = [
"rspamd.service" "stalwart-mail.service"
"postfix.service"
]; ];
} }
]) cfg.domains ]) 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, inputs,
config,
... ...
}: }:
{ {
@ -9,7 +10,7 @@
]; ];
khscodes.infrastructure.provisioning.pre.modules = [ khscodes.infrastructure.provisioning.pre.modules = [
( (
{ config, ... }: { ... }:
{ {
khscodes.vault = { khscodes.vault = {
enable = true; enable = true;
@ -22,11 +23,6 @@
description = "Secrets used for mx.kaareskovgaard.net"; 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 = { services.roundcube = {
# enable = true; enable = true;
# hostName = "mail.kaareskovgaard.net"; hostName = "mail.kaareskovgaard.net";
# configureNginx = true; configureNginx = true;
# extraConfig = '' extraConfig = ''
# # starttls needed for authentication, so the fqdn required to match # starttls needed for authentication, so the fqdn required to match
# # the certificate # the certificate
# $config['smtp_host'] = "tls://${config.mailserver.fqdn}"; $config['smtp_host'] = "ssl://${config.khscodes.networking.fqdn}:465";
# $config['smtp_user'] = "%u"; $config['imap_host'] = "ssl://${config.khscodes.networking.fqdn}:993";
# $config['smtp_pass'] = "%p"; '';
# ''; };
# };
khscodes.services.nginx = { khscodes.services.nginx = {
enable = true; 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"; khscodes.networking.fqdn = "mx.kaareskovgaard.net";
system.stateVersion = "25.05"; system.stateVersion = "25.05";