1{
2 config,
3 lib,
4 pkgs,
5 utils,
6 ...
7}:
8let
9
10 cfg = config.services.postsrsd;
11
12 inherit (lib)
13 concatMapStringsSep
14 concatMapAttrsStringSep
15 isBool
16 isFloat
17 isInt
18 isPath
19 isString
20 isList
21 mkEnableOption
22 mkPackageOption
23 mkRemovedOptionModule
24 mkRenamedOptionModule
25 ;
26
27 # This is a implementation of a simple libconfuse config renderer sufficient
28 # for the postsrsd configuration file complexity.
29 # TODO: Replace with pkgs.formats.libconfuse, once implemented (https://github.com/NixOS/nixpkgs/issues/401565)
30 renderValue =
31 value:
32 if isBool value then
33 if value then "true" else "false"
34 else if isString value || isPath value then
35 builtins.toJSON value # for escaping
36 else if isInt value || isFloat value then
37 toString value
38 else if isList value then
39 "{${concatMapStringsSep "," renderValue value}}"
40 else
41 throw "postsrsd: unsupported value type in settings option";
42
43 renderAttr =
44 attrs: concatMapAttrsStringSep "\n" (name: value: "${name} = ${renderValue value}") attrs;
45
46 configFile = pkgs.writeText "postsrsd.conf" (
47 renderAttr (lib.filterAttrsRecursive (_: v: v != null) cfg.settings)
48 );
49in
50{
51 imports = [
52 (mkRemovedOptionModule [ "services" "postsrsd" "socketPath" ] ''
53 Configure/reference `services.postsrsd.settings.socketmap` instead. Note that its now required to start with the `inet:` or `unix:` prefix.
54 '')
55 (mkRenamedOptionModule
56 [ "services" "postsrsd" "domains" ]
57 [ "services" "postsrsd" "settings" "domains" ]
58 )
59 (mkRenamedOptionModule
60 [ "services" "postsrsd" "separator" ]
61 [ "services" "postsrsd" "settings" "separator" ]
62 )
63 ]
64 ++
65 map
66 (
67 name:
68 lib.mkRemovedOptionModule [ "services" "postsrsd" name ] ''
69 `postsrsd` was upgraded to `>= 2.0.0`, with some different behaviors and configuration settings:
70 - NixOS Release Notes: https://nixos.org/manual/nixos/unstable/release-notes#sec-nixpkgs-release-25.05-incompatibilities
71 - NixOS Options Reference: https://nixos.org/manual/nixos/unstable/options#opt-services.postsrsd.enable
72 - Migration instructions: https://github.com/roehling/postsrsd/blob/2.0.10/README.rst#migrating-from-version-1x
73 - Postfix Setup: https://github.com/roehling/postsrsd/blob/2.0.10/README.rst#postfix-setup
74 ''
75 )
76 [
77 "domain"
78 "forwardPort"
79 "reversePort"
80 "timeout"
81 "excludeDomains"
82 ];
83
84 options = {
85 services.postsrsd = {
86 enable = mkEnableOption "the postsrsd SRS server for Postfix.";
87
88 package = mkPackageOption pkgs "postsrsd" { };
89
90 secretsFile = lib.mkOption {
91 type = lib.types.path;
92 default = "/var/lib/postsrsd/postsrsd.secret";
93 description = ''
94 Secret keys used for signing and verification.
95
96 ::: {.note}
97 The secret will be generated, if it does not exist at the given path.
98 :::
99 '';
100 };
101
102 settings = lib.mkOption {
103 type = lib.types.submodule {
104 freeformType =
105 with lib.types;
106 attrsOf (oneOf [
107 bool
108 float
109 int
110 path
111 str
112 (listOf str)
113 ]);
114
115 options = {
116 domains = lib.mkOption {
117 type = with lib.types; listOf str;
118 default = [ ];
119 example = [ "example.com" ];
120 description = ''
121 List of local domains, that do not require rewriting.
122 '';
123 };
124
125 secrets-file = lib.mkOption {
126 type = lib.types.str;
127 default = "\${CREDENTIALS_DIRECTORY}/secrets-file";
128 readOnly = true;
129 description = ''
130 Path to the file containing the secret keys.
131
132 ::: {.note}
133 Secrets are passed using `LoadCredential=` on the systemd unit,
134 so this options is read-only.
135
136 Configure {option}`services.postsrsd.secretsFile` instead.
137 '';
138 };
139
140 separator = lib.mkOption {
141 type = lib.types.enum [
142 "-"
143 "="
144 "+"
145 ];
146 default = "=";
147 description = ''
148 SRS tag separator used in generated sender addresses.
149
150 Unless you have a very good reason, you should leave this
151 setting at its default.
152 '';
153 };
154
155 srs-domain = lib.mkOption {
156 type = with lib.types; nullOr str;
157 default = null;
158 example = "srs.example.com";
159 description = ''
160 Dedicated mail domain used for ephemeral SRS envelope addresses.
161
162 Recommended to configure, when hosting multiple unrelated mail
163 domains (e.g. for different customers), to prevent privacy
164 issues.
165
166 Set to `null` to not configure any `srs-domain`.
167 '';
168 };
169
170 socketmap = lib.mkOption {
171 type = lib.types.strMatching "^(unix|inet):.+";
172 default = "unix:/run/postsrsd/socket";
173 example = "inet:localhost:10003";
174 description = ''
175 Listener configuration in socket map format native to Postfix configuration.
176 '';
177 };
178
179 chroot-dir = lib.mkOption {
180 type = lib.types.str;
181 default = "";
182 readOnly = true;
183 description = ''
184 Path to chroot into at runtime as an additional layer of protection.
185
186 ::: {.note}
187 We confine the runtime environment through systemd hardening instead, so this option is read-only.
188 :::
189 '';
190 };
191
192 unprivileged-user = lib.mkOption {
193 type = lib.types.str;
194 default = "";
195 readOnly = true;
196 description = ''
197 Unprivileged user to drop privileges to.
198
199 ::: {.note}
200 Our systemd unit never runs postsrsd as a privileged process, so this option is read-only.
201 :::
202 '';
203 };
204 };
205 };
206 default = { };
207 description = ''
208 Configuration options for the postsrsd.conf file.
209
210 See the [example configuration](https://github.com/roehling/postsrsd/blob/${cfg.package.version}/doc/postsrsd.conf) for possible values.
211 '';
212 };
213
214 configurePostfix = lib.mkOption {
215 type = lib.types.bool;
216 default = true;
217 description = ''
218 Whether to configure the required settings to use postsrsd in the local Postfix instance.
219 '';
220 };
221
222 user = lib.mkOption {
223 type = lib.types.str;
224 default = "postsrsd";
225 description = "User for the daemon";
226 };
227
228 group = lib.mkOption {
229 type = lib.types.str;
230 default = "postsrsd";
231 description = "Group for the daemon";
232 };
233 };
234 };
235
236 config = lib.mkMerge [
237 (lib.mkIf (cfg.enable && cfg.configurePostfix && config.services.postfix.enable) {
238 services.postfix.settings.main = {
239 # https://github.com/roehling/postsrsd#configuration
240 sender_canonical_maps = "socketmap:${cfg.settings.socketmap}:forward";
241 sender_canonical_classes = "envelope_sender";
242 recipient_canonical_maps = "socketmap:${cfg.settings.socketmap}:reverse";
243 recipient_canonical_classes = [
244 "envelope_recipient"
245 "header_recipient"
246 ];
247 };
248
249 users.users.postfix.extraGroups = [ cfg.group ];
250 })
251
252 (lib.mkIf cfg.enable {
253 users.users = lib.optionalAttrs (cfg.user == "postsrsd") {
254 postsrsd = {
255 group = cfg.group;
256 uid = config.ids.uids.postsrsd;
257 };
258 };
259
260 users.groups = lib.optionalAttrs (cfg.group == "postsrsd") {
261 postsrsd.gid = config.ids.gids.postsrsd;
262 };
263
264 systemd.services.postsrsd-generate-secrets = {
265 path = [ pkgs.coreutils ];
266 script = ''
267 if [ -e "${cfg.secretsFile}" ]; then
268 echo "Secrets file exists. Nothing to do!"
269 else
270 echo "WARNING: secrets file not found, autogenerating!"
271 DIR="$(dirname "${cfg.secretsFile}")"
272 install -m 750 -o ${cfg.user} -g ${cfg.group} -d "$DIR"
273 install -m 600 -o ${cfg.user} -g ${cfg.group} <(dd if=/dev/random bs=18 count=1 | base64) "${cfg.secretsFile}"
274 fi
275 '';
276 serviceConfig = {
277 Type = "oneshot";
278 };
279 };
280
281 environment.etc."postsrsd.conf".source = configFile;
282
283 systemd.services.postsrsd = {
284 description = "PostSRSd SRS rewriting server";
285 after = [
286 "network.target"
287 "postsrsd-generate-secrets.service"
288 ];
289 before = [ "postfix.service" ];
290 wantedBy = [ "multi-user.target" ];
291 requires = [ "postsrsd-generate-secrets.service" ];
292 restartTriggers = [ configFile ];
293
294 serviceConfig = {
295 ExecStart = utils.escapeSystemdExecArgs [
296 (lib.getExe cfg.package)
297 "-C"
298 "/etc/postsrsd.conf"
299 ];
300 User = cfg.user;
301 Group = cfg.group;
302 RuntimeDirectory = "postsrsd";
303 RuntimeDirectoryMode = "0750";
304 LoadCredential = "secrets-file:${cfg.secretsFile}";
305
306 CapabilityBoundingSet = [ "" ];
307 LockPersonality = true;
308 MemoryDenyWriteExecute = true;
309 NoNewPrivileges = true;
310 PrivateDevices = true;
311 PrivateMounts = true;
312 PrivateNetwork = lib.hasPrefix "unix:" cfg.settings.socketmap;
313 PrivateTmp = true;
314 PrivateUsers = true;
315 ProtectControlGroups = true;
316 ProtectHome = true;
317 ProtectHostname = true;
318 ProtectKernelLogs = true;
319 ProtectKernelModules = true;
320 ProtectKernelTunables = true;
321 ProtectSystem = "strict";
322 ProtectProc = "invisible";
323 ProcSubset = "pid";
324 RemoveIPC = true;
325 RestrictAddressFamilies =
326 if lib.hasPrefix "unix:" cfg.settings.socketmap then
327 [ "AF_UNIX" ]
328 else
329 [
330 "AF_INET"
331 "AF_INET6"
332 ];
333 RestrictNamespaces = true;
334 RestrictRealtime = true;
335 RestrictSUIDSGID = true;
336 SystemCallArchitectures = "native";
337 SystemCallFilter = [
338 "@system-service"
339 "~@privileged @resources"
340 ];
341 UMask = "0027";
342 };
343 };
344 })
345 ];
346
347 # package version referenced in option documentation
348 meta.buildDocsInSandbox = false;
349}