diff --git a/nix/modules/nixos/infrastructure/mailserver/default.nix b/nix/modules/nixos/infrastructure/mailserver/default.nix index 19642ba..41afd19 100644 --- a/nix/modules/nixos/infrastructure/mailserver/default.nix +++ b/nix/modules/nixos/infrastructure/mailserver/default.nix @@ -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; }; }; } diff --git a/nix/modules/nixos/infrastructure/mailserver/dkim.nix b/nix/modules/nixos/infrastructure/mailserver/dkim.nix index 5f27112..d32fd0f 100644 --- a/nix/modules/nixos/infrastructure/mailserver/dkim.nix +++ b/nix/modules/nixos/infrastructure/mailserver/dkim.nix @@ -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 diff --git a/nix/modules/nixos/infrastructure/mailserver/stalwart-spam-filter.nix b/nix/modules/nixos/infrastructure/mailserver/stalwart-spam-filter.nix new file mode 100644 index 0000000..d0ca6c6 --- /dev/null +++ b/nix/modules/nixos/infrastructure/mailserver/stalwart-spam-filter.nix @@ -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; + }; +}) diff --git a/nix/systems/aarch64-linux/mx.kaareskovgaard.net/default.nix b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/default.nix index 5904b8b..42ad3a2 100644 --- a/nix/systems/aarch64-linux/mx.kaareskovgaard.net/default.nix +++ b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/default.nix @@ -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";