diff --git a/nixos/doc/manual/release-notes/rl-2511.section.md b/nixos/doc/manual/release-notes/rl-2511.section.md index 8b2cfb815780..d0da48a610d6 100644 --- a/nixos/doc/manual/release-notes/rl-2511.section.md +++ b/nixos/doc/manual/release-notes/rl-2511.section.md @@ -28,6 +28,8 @@ - [Draupnir](https://github.com/the-draupnir-project/draupnir), a Matrix moderation bot. Available as [services.draupnir](#opt-services.draupnir.enable). +- [postfix-tlspol](https://github.com/Zuplu/postfix-tlspol), MTA-STS and DANE resolver and TLS policy server for Postfix. Available as [services.postfix-tlspol](#opt-services.postfix-tlspol.enable). + - [SuiteNumérique Docs](https://github.com/suitenumerique/docs), a collaborative note taking, wiki and documentation web platform and alternative to Notion or Outline. Available as [services.lasuite-docs](#opt-services.lasuite-docs.enable). [dwl](https://codeberg.org/dwl/dwl), a compact, hackable compositor for Wayland based on wlroots. Available as [programs.dwl](#opt-programs.dwl.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 77055c886318..7c2e1205169f 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -739,6 +739,7 @@ ./services/mail/opendkim.nix ./services/mail/opensmtpd.nix ./services/mail/pfix-srsd.nix + ./services/mail/postfix-tlspol.nix ./services/mail/postfix.nix ./services/mail/postfixadmin.nix ./services/mail/postgrey.nix diff --git a/nixos/modules/services/mail/postfix-tlspol.nix b/nixos/modules/services/mail/postfix-tlspol.nix new file mode 100644 index 000000000000..01373659da4a --- /dev/null +++ b/nixos/modules/services/mail/postfix-tlspol.nix @@ -0,0 +1,220 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) + hasPrefix + mkEnableOption + mkIf + mkOption + mkPackageOption + types + ; + + cfg = config.services.postfix-tlspol; + + format = pkgs.formats.yaml_1_2 { }; +in + +{ + options.services.postfix-tlspol = { + enable = mkEnableOption "postfix-tlspol"; + + package = mkPackageOption pkgs "postfix-tlspol" { }; + + settings = mkOption { + type = types.submodule { + freeformType = format.type; + options = { + server = { + address = mkOption { + type = types.str; + default = "unix:/run/postfix-tlspol/tlspol.sock"; + example = "127.0.0.1:8642"; + description = '' + Path or address/port where postfix-tlspol binds its socket to. + ''; + }; + + socket-permissions = mkOption { + type = types.str; + default = "0660"; + readOnly = true; + description = '' + Permissions to the UNIX socket, if configured. + + ::: {.note} + Due to hardening on the systemd unit the socket can never be created world readable/writable. + ::: + ''; + apply = value: (builtins.fromTOML "v=0o${value}").v; + }; + + log-level = mkOption { + type = types.enum [ + "debug" + "info" + "warn" + "error" + ]; + default = "info"; + example = "warn"; + description = '' + Log level + ''; + }; + + prefetch = mkOption { + type = types.bool; + default = true; + example = false; + description = '' + Whether to prefetch DNS records when the TTL of a cached record is about to expire. + ''; + }; + + cache-file = mkOption { + type = types.path; + default = "/var/cache/postfix-tlspol/cache.db"; + readOnly = true; + description = '' + Path to the cache file. + ''; + }; + }; + + dns = { + server = mkOption { + type = types.str; + default = "127.0.0.1:53"; + description = '' + IP and port to your DNS resolver + + ::: {.note} + The configured DNS resolver must validate DNSSEC signatures. + ::: + ''; + }; + }; + }; + }; + + default = { }; + description = '' + The postfix-tlspol configuration file as a Nix attribute set. + + See the reference documentation for possible options. + + ''; + }; + + configurePostfix = mkOption { + type = types.bool; + default = true; + description = '' + Whether to configure the required settings to use postfix-tlspol in the local Postfix instance. + ''; + }; + }; + + config = mkIf cfg.enable { + environment.etc."postfix-tlspol/config.yaml".source = + format.generate "postfix-tlspol.yaml" cfg.settings; + + environment.systemPackages = [ cfg.package ]; + + # https://github.com/Zuplu/postfix-tlspol#postfix-configuration + services.postfix.config = mkIf (config.services.postfix.enable && cfg.configurePostfix) { + smtp_dns_support_level = "dnssec"; + smtp_tls_security_level = "dane"; + smtp_tls_policy_maps = + let + address = + if (hasPrefix "unix:" cfg.settings.server.address) then + cfg.settings.server.address + else + "inet:${cfg.settings.server.address}"; + in + [ "socketmap:${address}:QUERYwithTLSRPT" ]; + }; + + systemd.services.postfix-tlspol = { + after = [ + "nss-lookup.target" + "network-online.target" + ]; + wants = [ + "nss-lookup.target" + "network-online.target" + ]; + wantedBy = [ "multi-user.target" ]; + + description = "Postfix DANE/MTA-STS TLS policy socketmap service"; + documentation = [ "https://github.com/Zuplu/postfix-tlspol" ]; + + # https://github.com/Zuplu/postfix-tlspol/blob/main/init/postfix-tlspol.service + serviceConfig = { + ExecStart = toString [ + (lib.getExe cfg.package) + "-config" + "/etc/postfix-tlspol/config.yaml" + ]; + ExecReload = "${lib.getExe' pkgs.util-linux "kill"} -HUP $MAINPID"; + Restart = "always"; + RestartSec = 5; + + DynamicUser = true; + + CacheDirectory = "postfix-tlspol"; + CapabilityBoundingSet = [ "" ]; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + PrivateUsers = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + ReadOnlyPaths = [ "/etc/postfix-tlspol/config.yaml" ]; + RemoveIPC = true; + RestrictAddressFamilies = + [ + "AF_INET" + "AF_INET6" + ] + ++ lib.optionals (lib.hasPrefix "unix:" cfg.settings.server.address) [ + "AF_UNIX" + ]; + RestrictNamespace = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged @resources" + ]; + SystemCallErrorNumber = "EPERM"; + SecureBits = [ + "noroot" + "noroot-locked" + ]; + RuntimeDirectory = "postfix-tlspol"; + RuntimeDirectoryMode = "1750"; + WorkingDirectory = "/var/cache/postfix-tlspol"; + UMask = "0117"; + }; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 134dc973183d..4ae9f4aeced5 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1103,6 +1103,7 @@ in postfix-raise-smtpd-tls-security-level = handleTest ./postfix-raise-smtpd-tls-security-level.nix { }; + postfix-tlspol = runTest ./postfix-tlspol.nix; postfixadmin = runTest ./postfixadmin.nix; postgres-websockets = runTest ./postgres-websockets.nix; postgresql = handleTest ./postgresql { }; diff --git a/nixos/tests/postfix-tlspol.nix b/nixos/tests/postfix-tlspol.nix new file mode 100644 index 000000000000..0c76257f6a5a --- /dev/null +++ b/nixos/tests/postfix-tlspol.nix @@ -0,0 +1,29 @@ +{ + lib, + ... +}: +{ + name = "postfix-tlspol"; + + meta.maintainers = with lib.maintainers; [ hexa ]; + + nodes.machine = { + services.postfix-tlspol.enable = true; + }; + + enableOCR = true; + + testScript = '' + import json + + machine.wait_for_unit("postfix-tlspol.service") + + with subtest("Interact with the service"): + machine.succeed("postfix-tlspol -purge") + + response = json.loads((machine.succeed("postfix-tlspol -query localhost"))) + machine.log(json.dumps(response, indent=2)) + + ''; + +} diff --git a/pkgs/by-name/po/postfix-tlspol/package.nix b/pkgs/by-name/po/postfix-tlspol/package.nix index f3d2c5a2f0f2..69a89a8b8319 100644 --- a/pkgs/by-name/po/postfix-tlspol/package.nix +++ b/pkgs/by-name/po/postfix-tlspol/package.nix @@ -2,6 +2,7 @@ lib, buildGoModule, fetchFromGitHub, + nixosTests, }: buildGoModule rec { @@ -22,6 +23,10 @@ buildGoModule rec { ldflags = [ "-X main.Version=${version}" ]; + passthru.tests = { + inherit (nixosTests) postfix-tlspol; + }; + meta = { description = "Lightweight MTA-STS + DANE/TLSA resolver and TLS policy server for Postfix, prioritizing DANE."; homepage = "https://github.com/Zuplu/postfix-tlspol"; diff --git a/pkgs/pkgs-lib/formats.nix b/pkgs/pkgs-lib/formats.nix index 9113d8ec3307..0836519c203d 100644 --- a/pkgs/pkgs-lib/formats.nix +++ b/pkgs/pkgs-lib/formats.nix @@ -192,7 +192,46 @@ optionalAttrs allowAliases aliases (listOf valueType) ]) // { - description = "YAML value"; + description = "YAML 1.1 value"; + }; + in + valueType; + + }; + + yaml_1_2 = + { }: + { + generate = + name: value: + pkgs.callPackage ( + { runCommand, remarshal }: + runCommand name + { + nativeBuildInputs = [ remarshal ]; + value = builtins.toJSON value; + passAsFile = [ "value" ]; + preferLocalBuild = true; + } + '' + json2yaml "$valuePath" "$out" + '' + ) { }; + + type = + let + valueType = + nullOr (oneOf [ + bool + int + float + str + path + (attrsOf valueType) + (listOf valueType) + ]) + // { + description = "YAML 1.2 value"; }; in valueType; diff --git a/pkgs/pkgs-lib/tests/formats.nix b/pkgs/pkgs-lib/tests/formats.nix index d69fd9e15fb9..4f635a4070fe 100644 --- a/pkgs/pkgs-lib/tests/formats.nix +++ b/pkgs/pkgs-lib/tests/formats.nix @@ -143,7 +143,7 @@ runBuildTests { }; yaml_1_1Atoms = shouldPass { - format = formats.yaml { }; + format = formats.yaml_1_1 { }; input = { null = null; false = false; @@ -176,6 +176,40 @@ runBuildTests { ''; }; + yaml_1_2Atoms = shouldPass { + format = formats.yaml_1_2 { }; + input = { + null = null; + false = false; + true = true; + float = 3.141; + str = "foo"; + attrs.foo = null; + list = [ + null + null + ]; + path = ./testfile; + no = "no"; + time = "22:30:00"; + }; + expected = '' + attrs: + foo: null + 'false': false + float: 3.141 + list: + - null + - null + no: no + 'null': null + path: ${./testfile} + str: foo + time: 22:30:00 + 'true': true + ''; + }; + iniAtoms = shouldPass { format = formats.ini { }; input = {