nixos/postsrd: settings option, hardening, postfix integration (#424954)
This commit is contained in:
		
						commit
						35cd17de4b
					
				| @ -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). | ||||
|  | ||||
| @ -4,6 +4,10 @@ | ||||
|   pkgs, | ||||
|   ... | ||||
| }: | ||||
| 
 | ||||
| let | ||||
|   cfg = config.services.pfix-srsd; | ||||
| in | ||||
| { | ||||
| 
 | ||||
|   ###### interface | ||||
| @ -32,12 +36,30 @@ | ||||
|         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 { | ||||
|   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 ]; | ||||
|       }; | ||||
| @ -54,5 +76,6 @@ | ||||
|           ExecStart = "${pkgs.pfixtools}/bin/pfix-srsd -p /run/pfix-srsd.pid -I ${config.services.pfix-srsd.domain} ${config.services.pfix-srsd.secretsFile}"; | ||||
|         }; | ||||
|       }; | ||||
|   }; | ||||
|     }) | ||||
|   ]; | ||||
| } | ||||
|  | ||||
| @ -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" ]) | ||||
|   ]; | ||||
| } | ||||
|  | ||||
| @ -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,23 +83,58 @@ 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. | ||||
|           ::: | ||||
|         ''; | ||||
|       }; | ||||
| 
 | ||||
|       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 = lib.types.listOf lib.types.str; | ||||
|         description = "Domain names for rewrite"; | ||||
|         default = [ config.networking.hostName ]; | ||||
|         defaultText = lib.literalExpression "[ config.networking.hostName ]"; | ||||
|               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 { | ||||
| @ -79,7 +144,79 @@ in | ||||
|                 "+" | ||||
|               ]; | ||||
|               default = "="; | ||||
|         description = "First separator character in generated addresses"; | ||||
|               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. | ||||
|         ''; | ||||
|       }; | ||||
| 
 | ||||
|       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,19 +230,26 @@ 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 { | ||||
|   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.users.postfix.extraGroups = [ cfg.group ]; | ||||
|     }) | ||||
| 
 | ||||
|     (lib.mkIf cfg.enable { | ||||
|       users.users = lib.optionalAttrs (cfg.user == "postsrsd") { | ||||
|         postsrsd = { | ||||
|           group = cfg.group; | ||||
| @ -134,6 +278,8 @@ in | ||||
|         }; | ||||
|       }; | ||||
| 
 | ||||
|       environment.etc."postsrsd.conf".source = configFile; | ||||
| 
 | ||||
|       systemd.services.postsrsd = { | ||||
|         description = "PostSRSd SRS rewriting server"; | ||||
|         after = [ | ||||
| @ -143,16 +289,61 @@ in | ||||
|         before = [ "postfix.service" ]; | ||||
|         wantedBy = [ "multi-user.target" ]; | ||||
|         requires = [ "postsrsd-generate-secrets.service" ]; | ||||
|       confinement.enable = true; | ||||
|         restartTriggers = [ configFile ]; | ||||
| 
 | ||||
|         serviceConfig = { | ||||
|         ExecStart = "${lib.getExe pkgs.postsrsd} -C ${configFile}"; | ||||
|           ExecStart = utils.escapeSystemdExecArgs [ | ||||
|             (lib.getExe cfg.package) | ||||
|             "-C" | ||||
|             "/etc/postsrsd.conf" | ||||
|           ]; | ||||
|           User = cfg.user; | ||||
|           Group = cfg.group; | ||||
|         PermissionsStartOnly = true; | ||||
|         RuntimeDirectory = runtimeDirectoryName; | ||||
|           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; | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Martin Weinelt
						Martin Weinelt