nixos/postfix-tlspol: init (#415482)

* pkgs.formats.yaml_1_2: init

Same as YAML 1.1 but relies on the unpinned remarshal version which emits
YAML 1.2.

* nixos/postfix-tlspol: init

MTA-STS and DANE/TLSA resolver and TLS policy socketmap server for
Postfix.

* nixos/tests/postfix-tlspol: init

Simple test if the service comes up and the CLI can interact with it and
gives reasonable results.
This commit is contained in:
Martin Weinelt 2025-06-12 00:36:50 +02:00 committed by GitHub
commit 1b59fd6732
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 333 additions and 2 deletions

View File

@ -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).

View File

@ -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

View File

@ -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.
<https://github.com/Zuplu/postfix-tlspol/blob/main/configs/config.default.yaml>
'';
};
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";
};
};
};
}

View File

@ -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 { };

View File

@ -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))
'';
}

View File

@ -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";

View File

@ -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;

View File

@ -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 = {