diff --git a/nixos/doc/manual/release-notes/rl-2511.section.md b/nixos/doc/manual/release-notes/rl-2511.section.md index 4b0a2c4395d1..683724e11fc7 100644 --- a/nixos/doc/manual/release-notes/rl-2511.section.md +++ b/nixos/doc/manual/release-notes/rl-2511.section.md @@ -120,6 +120,10 @@ - `services.ntpd-rs` now performs configuration validation. +- `services.postsrsd` now automatically integrates with the local Postfix instance, when enabled. This behavior can disabled using the [services.postsrsd.configurePostfix](#opt-services.postsrsd.configurePostfix) option. + +- `services.pfix-srsd` now automatically integrates with the local Postfix instance, when enabled. This behavior can disabled using the [services.pfix-srsd.configurePostfix](#opt-services.pfix-srsd.configurePostfix) option. + - `services.monero` now includes the `environmentFile` option for adding secrets to the Monero daemon config. - `amdgpu` kernel driver overdrive mode can now be enabled by setting [hardware.amdgpu.overdrive.enable](#opt-hardware.amdgpu.overdrive.enable) and customized through [hardware.amdgpu.overdrive.ppfeaturemask](#opt-hardware.amdgpu.overdrive.ppfeaturemask). diff --git a/nixos/modules/services/mail/pfix-srsd.nix b/nixos/modules/services/mail/pfix-srsd.nix index fb6a395e3ab8..035f331dcf6d 100644 --- a/nixos/modules/services/mail/pfix-srsd.nix +++ b/nixos/modules/services/mail/pfix-srsd.nix @@ -4,6 +4,10 @@ pkgs, ... }: + +let + cfg = config.services.pfix-srsd; +in { ###### interface @@ -32,27 +36,46 @@ type = lib.types.path; default = "/var/lib/pfix-srsd/secrets"; }; + + configurePostfix = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Whether to configure the required settings to use pfix-srsd in the local Postfix instance. + ''; + }; }; }; ###### implementation - config = lib.mkIf config.services.pfix-srsd.enable { - environment = { - systemPackages = [ pkgs.pfixtools ]; - }; - - systemd.services.pfix-srsd = { - description = "Postfix sender rewriting scheme daemon"; - before = [ "postfix.service" ]; - #note that we use requires rather than wants because postfix - #is unable to process (almost) all mail without srsd - requiredBy = [ "postfix.service" ]; - serviceConfig = { - Type = "forking"; - PIDFile = "/run/pfix-srsd.pid"; - ExecStart = "${pkgs.pfixtools}/bin/pfix-srsd -p /run/pfix-srsd.pid -I ${config.services.pfix-srsd.domain} ${config.services.pfix-srsd.secretsFile}"; + config = lib.mkMerge [ + (lib.mkIf (cfg.enable && cfg.configurePostfix && config.services.postfix.enable) { + services.postfix.config = { + sender_canonical_maps = [ "tcp:127.0.0.1:10001" ]; + sender_canonical_classes = [ "envelope_sender" ]; + recipient_canonical_maps = [ "tcp:127.0.0.1:10002" ]; + recipient_canonical_classes = [ "envelope_recipient" ]; }; - }; - }; + }) + + (lib.mkIf cfg.enable { + environment = { + systemPackages = [ pkgs.pfixtools ]; + }; + + systemd.services.pfix-srsd = { + description = "Postfix sender rewriting scheme daemon"; + before = [ "postfix.service" ]; + #note that we use requires rather than wants because postfix + #is unable to process (almost) all mail without srsd + requiredBy = [ "postfix.service" ]; + serviceConfig = { + Type = "forking"; + PIDFile = "/run/pfix-srsd.pid"; + ExecStart = "${pkgs.pfixtools}/bin/pfix-srsd -p /run/pfix-srsd.pid -I ${config.services.pfix-srsd.domain} ${config.services.pfix-srsd.secretsFile}"; + }; + }; + }) + ]; } diff --git a/nixos/modules/services/mail/postfix.nix b/nixos/modules/services/mail/postfix.nix index 7b2d62e1fc97..710f2d381e6a 100644 --- a/nixos/modules/services/mail/postfix.nix +++ b/nixos/modules/services/mail/postfix.nix @@ -785,12 +785,6 @@ in description = "Maps to be compiled and placed into /var/lib/postfix/conf."; }; - useSrs = lib.mkOption { - type = lib.types.bool; - default = false; - description = "Whether to enable sender rewriting scheme"; - }; - }; }; @@ -808,8 +802,6 @@ in systemPackages = [ pkgs.postfix ]; }; - services.pfix-srsd.enable = config.services.postfix.useSrs; - services.mail.sendmailSetuidWrapper = lib.mkIf config.services.postfix.setSendmail { program = "sendmail"; source = "${pkgs.postfix}/bin/sendmail"; @@ -1002,12 +994,6 @@ in ] ++ lib.optional haveAliases "$alias_maps"; } // lib.optionalAttrs (cfg.dnsBlacklists != [ ]) { smtpd_client_restrictions = clientRestrictions; } - // lib.optionalAttrs cfg.useSrs { - sender_canonical_maps = [ "tcp:127.0.0.1:10001" ]; - sender_canonical_classes = [ "envelope_sender" ]; - recipient_canonical_maps = [ "tcp:127.0.0.1:10002" ]; - recipient_canonical_classes = [ "envelope_recipient" ]; - } // lib.optionalAttrs cfg.enableHeaderChecks { header_checks = [ "regexp:/etc/postfix/header_checks" ]; } @@ -1190,5 +1176,6 @@ in [ "services" "postfix" "config" "smtp_tls_security_level" ] (config: lib.mkIf config.services.postfix.useDane "dane") ) + (lib.mkRenamedOptionModule [ "services" "postfix" "useSrs" ] [ "services" "pfix-srsd" "enable" ]) ]; } diff --git a/nixos/modules/services/mail/postsrsd.nix b/nixos/modules/services/mail/postsrsd.nix index bc91edbbe000..b264d17a1911 100644 --- a/nixos/modules/services/mail/postsrsd.nix +++ b/nixos/modules/services/mail/postsrsd.nix @@ -2,37 +2,67 @@ config, lib, pkgs, + utils, ... }: let cfg = config.services.postsrsd; - runtimeDirectoryName = "postsrsd"; - runtimeDirectory = "/run/${runtimeDirectoryName}"; - # TODO: follow RFC 42, but we need a libconfuse format first: - # https://github.com/NixOS/nixpkgs/issues/401565 - # Arrays in `libconfuse` look like this: {"Life", "Universe", "Everything"} - # See https://www.nongnu.org/confuse/tutorial-html/ar01s03.html. - # - # Note: We're using `builtins.toJSON` to escape strings, but JSON strings - # don't have exactly the same semantics as libconfuse strings. For example, - # "${F}" gets treated as an env var reference, see above issue for details. - libconfuseDomains = "{ " + lib.concatMapStringsSep ", " builtins.toJSON cfg.domains + " }"; - configFile = pkgs.writeText "postsrsd.conf" '' - secrets-file = "''${CREDENTIALS_DIRECTORY}/secrets-file" - domains = ${libconfuseDomains} - separator = "${cfg.separator}" - socketmap = "unix:${cfg.socketPath}" - # Disable postsrsd's jailing in favor of confinement with systemd. - unprivileged-user = "" - chroot-dir = "" - ''; + inherit (lib) + concatMapStringsSep + concatMapAttrsStringSep + isBool + isFloat + isInt + isPath + isString + isList + mkEnableOption + mkPackageOption + mkRemovedOptionModule + mkRenamedOptionModule + ; + # This is a implementation of a simple libconfuse config renderer sufficient + # for the postsrsd configuration file complexity. + # TODO: Replace with pkgs.formats.libconfuse, once implemented (https://github.com/NixOS/nixpkgs/issues/401565) + renderValue = + value: + if isBool value then + if value then "true" else "false" + else if isString value || isPath value then + builtins.toJSON value # for escaping + else if isInt value || isFloat value then + toString value + else if isList value then + "{${concatMapStringsSep "," renderValue value}}" + else + throw "postsrsd: unsupported value type in settings option"; + + renderAttr = + attrs: concatMapAttrsStringSep "\n" (name: value: "${name} = ${renderValue value}") attrs; + + configFile = pkgs.writeText "postsrsd.conf" ( + renderAttr (lib.filterAttrsRecursive (_: v: v != null) cfg.settings) + ); in { imports = - map + [ + (mkRemovedOptionModule [ "services" "postsrsd" "socketPath" ] '' + Configure/reference `services.postsrsd.settings.socketmap` instead. Note that its now required to start with the `inet:` or `unix:` prefix. + '') + (mkRenamedOptionModule + [ "services" "postsrsd" "domains" ] + [ "services" "postsrsd" "settings" "domains" ] + ) + (mkRenamedOptionModule + [ "services" "postsrsd" "separator" ] + [ "services" "postsrsd" "settings" "separator" ] + ) + ] + ++ map ( name: lib.mkRemovedOptionModule [ "services" "postsrsd" name ] '' @@ -53,33 +83,140 @@ in options = { services.postsrsd = { - enable = lib.mkOption { - type = lib.types.bool; - default = false; - description = "Whether to enable the postsrsd SRS server for Postfix."; - }; + enable = mkEnableOption "the postsrsd SRS server for Postfix."; + + package = mkPackageOption pkgs "postsrsd" { }; secretsFile = lib.mkOption { type = lib.types.path; default = "/var/lib/postsrsd/postsrsd.secret"; - description = "Secret keys used for signing and verification"; + description = '' + Secret keys used for signing and verification. + + ::: {.note} + The secret will be generated, if it does not exist at the given path. + ::: + ''; }; - domains = lib.mkOption { - type = lib.types.listOf lib.types.str; - description = "Domain names for rewrite"; - default = [ config.networking.hostName ]; - defaultText = lib.literalExpression "[ config.networking.hostName ]"; + settings = lib.mkOption { + type = lib.types.submodule { + freeformType = + with lib.types; + attrsOf (oneOf [ + bool + float + int + path + str + (listOf str) + ]); + + options = { + domains = lib.mkOption { + type = with lib.types; listOf str; + default = [ ]; + example = [ "example.com" ]; + description = '' + List of local domains, that do not require rewriting. + ''; + }; + + secrets-file = lib.mkOption { + type = lib.types.str; + default = "\${CREDENTIALS_DIRECTORY}/secrets-file"; + readOnly = true; + description = '' + Path to the file containing the secret keys. + + ::: {.note} + Secrets are passed using `LoadCredential=` on the systemd unit, + so this options is read-only. + + Configure {option}`services.postsrsd.secretsFile` instead. + ''; + }; + + separator = lib.mkOption { + type = lib.types.enum [ + "-" + "=" + "+" + ]; + default = "="; + description = '' + SRS tag separator used in generated sender addresses. + + Unless you have a very good reason, you should leave this + setting at its default. + ''; + }; + + srs-domain = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + example = "srs.example.com"; + description = '' + Dedicated mail domain used for ephemeral SRS envelope addresses. + + Recommended to configure, when hosting multiple unrelated mail + domains (e.g. for different customers), to prevent privacy + issues. + + Set to `null` to not configure any `srs-domain`. + ''; + }; + + socketmap = lib.mkOption { + type = lib.types.strMatching "^(unix|inet):.+"; + default = "unix:/run/postsrsd/socket"; + example = "inet:localhost:10003"; + description = '' + Listener configuration in socket map format native to Postfix configuration. + ''; + }; + + chroot-dir = lib.mkOption { + type = lib.types.str; + default = ""; + readOnly = true; + description = '' + Path to chroot into at runtime as an additional layer of protection. + + ::: {.note} + We confine the runtime environment through systemd hardening instead, so this option is read-only. + ::: + ''; + }; + + unprivileged-user = lib.mkOption { + type = lib.types.str; + default = ""; + readOnly = true; + description = '' + Unprivileged user to drop privileges to. + + ::: {.note} + Our systemd unit never runs postsrsd as a privileged process, so this option is read-only. + ::: + ''; + }; + }; + }; + default = { }; + description = '' + Configuration options for the postsrsd.conf file. + + See the [example configuration](https://github.com/roehling/postsrsd/blob/${cfg.package.version}/doc/postsrsd.conf) for possible values. + ''; }; - separator = lib.mkOption { - type = lib.types.enum [ - "-" - "=" - "+" - ]; - default = "="; - description = "First separator character in generated addresses"; + configurePostfix = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Whether to configure the required settings to use postsrsd in the local Postfix instance. + ''; }; user = lib.mkOption { @@ -93,66 +230,120 @@ in default = "postsrsd"; description = "Group for the daemon"; }; - - socketPath = lib.mkOption { - type = lib.types.path; - default = "${runtimeDirectory}/socket"; - readOnly = true; - description = '' - Path to the Unix socket for connecting to postsrsd. - Read-only, intended for usage when integrating postsrsd into other NixOS config.''; - }; }; }; - config = lib.mkIf cfg.enable { - users.users = lib.optionalAttrs (cfg.user == "postsrsd") { - postsrsd = { - group = cfg.group; - uid = config.ids.uids.postsrsd; + config = lib.mkMerge [ + (lib.mkIf (cfg.enable && cfg.configurePostfix && config.services.postfix.enable) { + services.postfix.config = { + # https://github.com/roehling/postsrsd#configuration + sender_canonical_maps = "socketmap:${cfg.settings.socketmap}:forward"; + sender_canonical_classes = "envelope_sender"; + recipient_canonical_maps = "socketmap:${cfg.settings.socketmap}:reverse"; + recipient_canonical_classes = [ + "envelope_recipient" + "header_recipient" + ]; }; - }; - users.groups = lib.optionalAttrs (cfg.group == "postsrsd") { - postsrsd.gid = config.ids.gids.postsrsd; - }; + users.users.postfix.extraGroups = [ cfg.group ]; + }) - systemd.services.postsrsd-generate-secrets = { - path = [ pkgs.coreutils ]; - script = '' - if [ -e "${cfg.secretsFile}" ]; then - echo "Secrets file exists. Nothing to do!" - else - echo "WARNING: secrets file not found, autogenerating!" - DIR="$(dirname "${cfg.secretsFile}")" - install -m 750 -o ${cfg.user} -g ${cfg.group} -d "$DIR" - install -m 600 -o ${cfg.user} -g ${cfg.group} <(dd if=/dev/random bs=18 count=1 | base64) "${cfg.secretsFile}" - fi - ''; - serviceConfig = { - Type = "oneshot"; + (lib.mkIf cfg.enable { + users.users = lib.optionalAttrs (cfg.user == "postsrsd") { + postsrsd = { + group = cfg.group; + uid = config.ids.uids.postsrsd; + }; }; - }; - systemd.services.postsrsd = { - description = "PostSRSd SRS rewriting server"; - after = [ - "network.target" - "postsrsd-generate-secrets.service" - ]; - before = [ "postfix.service" ]; - wantedBy = [ "multi-user.target" ]; - requires = [ "postsrsd-generate-secrets.service" ]; - confinement.enable = true; - - serviceConfig = { - ExecStart = "${lib.getExe pkgs.postsrsd} -C ${configFile}"; - User = cfg.user; - Group = cfg.group; - PermissionsStartOnly = true; - RuntimeDirectory = runtimeDirectoryName; - LoadCredential = "secrets-file:${cfg.secretsFile}"; + users.groups = lib.optionalAttrs (cfg.group == "postsrsd") { + postsrsd.gid = config.ids.gids.postsrsd; }; - }; - }; + + systemd.services.postsrsd-generate-secrets = { + path = [ pkgs.coreutils ]; + script = '' + if [ -e "${cfg.secretsFile}" ]; then + echo "Secrets file exists. Nothing to do!" + else + echo "WARNING: secrets file not found, autogenerating!" + DIR="$(dirname "${cfg.secretsFile}")" + install -m 750 -o ${cfg.user} -g ${cfg.group} -d "$DIR" + install -m 600 -o ${cfg.user} -g ${cfg.group} <(dd if=/dev/random bs=18 count=1 | base64) "${cfg.secretsFile}" + fi + ''; + serviceConfig = { + Type = "oneshot"; + }; + }; + + environment.etc."postsrsd.conf".source = configFile; + + systemd.services.postsrsd = { + description = "PostSRSd SRS rewriting server"; + after = [ + "network.target" + "postsrsd-generate-secrets.service" + ]; + before = [ "postfix.service" ]; + wantedBy = [ "multi-user.target" ]; + requires = [ "postsrsd-generate-secrets.service" ]; + restartTriggers = [ configFile ]; + + serviceConfig = { + ExecStart = utils.escapeSystemdExecArgs [ + (lib.getExe cfg.package) + "-C" + "/etc/postsrsd.conf" + ]; + User = cfg.user; + Group = cfg.group; + RuntimeDirectory = "postsrsd"; + RuntimeDirectoryMode = "0750"; + LoadCredential = "secrets-file:${cfg.secretsFile}"; + + CapabilityBoundingSet = [ "" ]; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateMounts = true; + PrivateNetwork = lib.hasPrefix "unix:" cfg.settings.socketmap; + PrivateTmp = true; + PrivateUsers = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectSystem = "strict"; + ProtectProc = "invisible"; + ProcSubset = "pid"; + RemoveIPC = true; + RestrictAddressFamilies = + if lib.hasPrefix "unix:" cfg.settings.socketmap then + [ "AF_UNIX" ] + else + [ + "AF_INET" + "AF_INET6" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged @resources" + ]; + UMask = "0027"; + }; + }; + }) + ]; + + # package version referenced in option documentation + meta.buildDocsInSandbox = false; }