aboutsummaryrefslogtreecommitdiffstats
path: root/modules/by-name/st/stalwart-mail/module.nix
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-07-30 14:05:39 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-07-30 14:05:39 +0200
commitc153a351659e4596acfc31f00bf594343cbfcd68 (patch)
treea9ed83de07711d6424fbea09aad28891bae5bfe4 /modules/by-name/st/stalwart-mail/module.nix
parenthosts/server3: Setup prometheus server in agent mode (diff)
downloadnixos-server-c153a351659e4596acfc31f00bf594343cbfcd68.zip
modules: Use namespaces
That might make it easier in the future to merge different server configs together (and thusly facilitate code-reuse.).
Diffstat (limited to 'modules/by-name/st/stalwart-mail/module.nix')
-rw-r--r--modules/by-name/st/stalwart-mail/module.nix428
1 files changed, 0 insertions, 428 deletions
diff --git a/modules/by-name/st/stalwart-mail/module.nix b/modules/by-name/st/stalwart-mail/module.nix
deleted file mode 100644
index f7e44d8..0000000
--- a/modules/by-name/st/stalwart-mail/module.nix
+++ /dev/null
@@ -1,428 +0,0 @@
-{
- lib,
- config,
- pkgs,
- vhackPackages,
- ...
-}: let
- cfg = config.vhack.stalwart-mail;
- topCfg = config.services.stalwart-mail;
-
- configFormat = pkgs.formats.toml {};
- configFile = configFormat.generate "stalwart-mail.toml" topCfg.settings;
-in {
- imports = [
- ./settings.nix
- ];
-
- options.vhack.stalwart-mail = {
- enable = lib.mkEnableOption "starwart-mail";
-
- package = lib.mkPackageOption pkgs "stalwart-mail" {};
-
- admin = lib.mkOption {
- description = ''
- Email address to advertise as administrator. This is the address, where dkim, spv
- etc. refusal reports are sent to.
-
- The format should be: `mailto:<name>@<domain>`
- '';
- type = lib.types.str;
- example = "mailto:dmarc+rua@example.com";
- default = "";
- };
-
- fqdn = lib.mkOption {
- type = lib.types.str;
- example = "mail.foss-syndicate.org";
- description = ''
- The fully qualified domain name for this mail server.
- '';
- };
-
- principals = lib.mkOption {
- default = null;
- type = lib.types.nullOr (lib.types.listOf (lib.types.submodule {
- options = {
- name = lib.mkOption {
- type = lib.types.str;
- description = "Specifies the username of the account";
- };
-
- class = lib.mkOption {
- type = lib.types.enum ["individual" "admin"];
- description = "Specifies the account type";
- };
-
- description = lib.mkOption {
- type = lib.types.str;
- description = "Provides a description or full name for the user";
- default = "";
- };
-
- secret = lib.mkOption {
- type = let
- prefix = pre: lib.types.strMatching "^${lib.strings.escapeRegex pre}.*";
- in
- lib.types.oneOf [
- (prefix "$argon2")
- (prefix "$pbkdf2")
- (prefix "$scrypt")
- (prefix "$2") # bcrypt
- (prefix "$6$") # sha-512
- (prefix "$5$") # sha-256
- (prefix "$sha1")
- (prefix "$1") # md5
- (prefix "_") # BSDi crypt
- (prefix "{SHA}") # base64 sha
- (prefix "{SSHA}") # base64 salted sha
-
- # unix crypt
- (prefix "{CRYPT}")
- (prefix "{crypt}")
-
- # Plain text
- (prefix "{PLAIN}")
- (prefix "{plain}")
- (prefix "{CLEAR}")
- (prefix "{clear}")
- ];
- description = ''
- Sets the password for the user account.
- Passwords can be stored hashed or in plain text (not recommended).
- See <https://stalw.art/docs/auth/authentication/password/> for a description
- of password encoding.
- '';
- };
- email = lib.mkOption {
- type = lib.types.listOf lib.types.str;
- description = ''
- A list of email addresses associated with the user.
- The first address in the list is considered the primary address.
- '';
- };
- };
- }));
- };
-
- dataDirectory = lib.mkOption {
- description = ''
- The directory in which to store all storage things.
- '';
- default = "/var/lib/stalwart-mail";
- type = lib.types.path;
- readOnly = true;
- };
-
- openFirewall = lib.mkOption {
- type = lib.types.bool;
- default = false;
- description = ''
- Whether to open TCP firewall ports, which are specified in
- {option}`services.stalwart-mail.settings.listener` on all interfaces.
- '';
- };
-
- security = lib.mkOption {
- type = lib.types.nullOr (lib.types.submodule {
- options = {
- verificationMode = lib.mkOption {
- type = lib.types.enum ["relaxed" "strict"];
- description = ''
- Whether to allow invalid signatures/checks or not.
- '';
- default = "relaxed";
- };
-
- dkimKeys = lib.mkOption {
- type = lib.types.attrsOf (lib.types.submodule {
- options = {
- dkimPublicKey = lib.mkOption {
- type = lib.types.str;
- description = ''
- The base 64 encoded representation of the public dkim key.
- '';
- };
- dkimPrivateKeyPath = lib.mkOption {
- type = lib.types.path;
- description = ''
- The path to the dkim private key agenix file.
- Generate it via the `./gen_key` script:
- '';
- };
- keyAlgorithm = lib.mkOption {
- type = lib.types.enum ["ed25519-sha256" "rsa-sha-256" "rsa-sha-1"];
- description = "The algorithm of the used key";
- };
- };
- });
- description = ''
- Which key to use for which domain. The attr keys are the domains
- '';
- default = {};
- };
- };
- });
- description = ''
- Security options. This should only be set to `null` when testing.
- '';
- };
- };
-
- config = lib.mkIf cfg.enable {
- assertions = [
- {
- assertion = cfg.admin != "";
- message = "You need to specify an admin address.";
- }
- ];
-
- vhack.nginx.enable = true;
- services = {
- stalwart-mail = {
- # NOTE(@bpeetz): We do not use the NixOS service, as it comes with too much
- # bothersome default configuration and not really any useful configuration.
- # However, this decision could obviously be reversed in the future. <2025-02-08>
- enable = false;
- inherit (cfg) package;
- };
-
- nginx.virtualHosts."${cfg.fqdn}" = {
- locations."/" = {
- proxyPass = "http://${builtins.elemAt config.services.stalwart-mail.settings.server.listener.http.bind 0}";
- recommendedProxySettings = true;
- };
-
- useACMEHost = "${cfg.fqdn}";
- forceSSL = true;
- };
-
- redis = {
- servers = {
- "stalwart-mail" = {
- enable = true;
-
- user = "stalwart-mail";
-
- # Disable TCP listening. (We have a UNIX socket)
- port = 0;
- bind = null;
-
- settings = {
- protected-mode = true;
- enable-protected-configs = false;
- enable-debug-command = false;
- enable-module-command = false;
-
- supervised = "systemd";
- stop-writes-on-bgsave-error = true;
- sanitize-dump-payload = "clients";
- };
- };
- };
- };
- };
- security.acme.certs = {
- "${cfg.fqdn}" = {
- domain = cfg.fqdn;
- group = "stalwart-mail-certificates";
- };
- };
-
- age.secrets = let
- keys =
- lib.mapAttrs' (
- keyDomain: keyConfig:
- lib.nameValuePair "stalwartMail${keyDomain}"
- {
- file = keyConfig.dkimPrivateKeyPath;
- mode = "600";
- owner = "stalwart-mail";
- group = "stalwart-mail";
- }
- )
- cfg.security.dkimKeys;
- in
- lib.mkIf (cfg.security != null) keys;
-
- vhack.persist.directories = [
- {
- directory = "${cfg.dataDirectory}";
- user = "stalwart-mail";
- group = "stalwart-mail";
- mode = "0700";
- }
- {
- directory = "${config.services.redis.servers."stalwart-mail".settings.dir}";
- user = "stalwart-mail";
- group = "redis-stalwart-mail";
- mode = "0770";
- }
- ];
-
- # This service stores a potentially large amount of data.
- # Running it as a dynamic user would force chown to be run every time the
- # service is restarted on a potentially large number of files.
- # That would cause unnecessary and unwanted delays.
- users = {
- groups = {
- stalwart-mail = {
- gid = config.vhack.constants.ids.gids.stalwart-mail;
- };
- stalwart-mail-certificates = {
- gid = config.vhack.constants.ids.gids.stalwart-mail-certificates;
- };
- redis-stalwart-mail = {
- gid = config.vhack.constants.ids.gids.redis-stalwart-mail;
- };
- };
- users = {
- nginx = {
- extraGroups = ["stalwart-mail-certificates"];
- };
- stalwart-mail = {
- isSystemUser = true;
- group = "stalwart-mail";
- uid = config.vhack.constants.ids.uids.stalwart-mail;
- extraGroups = ["stalwart-mail-certificates"];
- };
- redis-stalwart-mail = {
- group = "redis-stalwart-mail";
- uid = config.vhack.constants.ids.uids.redis-stalwart-mail;
- };
- };
- };
-
- systemd.tmpfiles.rules = [
- "d '${cfg.dataDirectory}' - stalwart-mail stalwart-mail - -"
- ];
-
- systemd = {
- services.stalwart-mail = {
- wantedBy = ["multi-user.target"];
- requires =
- [
- "redis-stalwart-mail.service"
- "network-online.target"
- ]
- ++ (lib.optional (cfg.security != null) "acme-${cfg.fqdn}.service");
- after = [
- "local-fs.target"
- "network.target"
- "network-online.target"
- "redis-stalwart-mail.service"
- "acme-${cfg.fqdn}.service"
- ];
- conflicts = [
- "postfix.service"
- "sendmail.service"
- "exim4.service"
- ];
- description = "Stalwart Mail Server";
-
- environment = {
- SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt";
- NIX_SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt";
- };
-
- preStart = let
- esa = lib.strings.escapeShellArg;
- mkTmpFile = path: "[ -d ${esa path} ] || mkdir --parents ${esa path}";
-
- # Create the directories for stalwart
- storageDirectories = lib.lists.filter (v: v != null) (lib.attrsets.mapAttrsToList (_: {path ? null, ...}:
- if (path != null)
- then mkTmpFile path
- else null)
- topCfg.settings.store);
- in
- ''
- # Stalwart actually wants to store _data_ (e.g., blocked ips) in it's own config file.
- # Thus we need to make it writable.
- cat ${esa configFile} >$CACHE_DIRECTORY/mutable_config_file.toml
- ''
- + (builtins.concatStringsSep "\n" storageDirectories);
-
- serviceConfig = {
- ExecStart = pkgs.writers.writeDash "start-stalwart-mail" ''
- ${lib.getExe cfg.package} --config="$CACHE_DIRECTORY/mutable_config_file.toml"
- '';
-
- Restart = "no";
-
- KillMode = "process";
- KillSignal = "SIGINT";
-
- Type = "simple";
- LimitNOFILE = 65536;
-
- StandardOutput = "journal";
- StandardError = "journal";
-
- ReadWritePaths = [
- cfg.dataDirectory
- ];
- CacheDirectory = "stalwart-mail";
- StateDirectory = "stalwart-mail";
-
- User = "stalwart-mail";
- Group = "stalwart-mail";
-
- SyslogIdentifier = "stalwart-mail";
-
- # Bind standard privileged ports
- AmbientCapabilities = ["CAP_NET_BIND_SERVICE"];
- CapabilityBoundingSet = ["CAP_NET_BIND_SERVICE"];
-
- # Hardening
- DeviceAllow = [""];
- LockPersonality = true;
- MemoryDenyWriteExecute = true;
- PrivateDevices = true;
- PrivateUsers = false; # incompatible with CAP_NET_BIND_SERVICE
- ProcSubset = "pid";
- PrivateTmp = true;
- ProtectClock = true;
- ProtectControlGroups = true;
- ProtectHome = true;
- ProtectHostname = true;
- ProtectKernelLogs = true;
- ProtectKernelModules = true;
- ProtectKernelTunables = true;
- ProtectProc = "invisible";
- ProtectSystem = "strict";
- RestrictAddressFamilies = [
- "AF_INET"
- "AF_INET6"
- "AF_UNIX"
- ];
- RestrictNamespaces = true;
- RestrictRealtime = true;
- RestrictSUIDSGID = true;
- SystemCallArchitectures = "native";
- SystemCallFilter = [
- "@system-service"
- "~@privileged"
- ];
- UMask = "0077";
- };
- };
- };
-
- # Make admin commands available in the shell
- environment.systemPackages = [cfg.package];
-
- networking.firewall = let
- parsePorts = listeners: let
- parseAddresses = listeners: lib.flatten (lib.mapAttrsToList (name: value: value.bind) listeners);
- splitAddress = addr: lib.splitString ":" addr;
- extractPort = addr: lib.toInt (builtins.foldl' (a: b: b) "" (splitAddress addr));
- in
- builtins.map extractPort (parseAddresses listeners);
- in
- lib.mkIf (cfg.openFirewall && (builtins.hasAttr "listener" topCfg.settings.server))
- {
- allowedTCPPorts = parsePorts topCfg.settings.server.listener;
- };
- };
-}