nixos/postsrsd: migrate to rfc42 settings
Allow a freeform configuration approach to satisfy different configuration complexities. Remove confinement options and make its hardening options more explicit and removed the deprecated PermissionStartOnly= option.
This commit is contained in:
@@ -7,32 +7,59 @@
|
||||
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
|
||||
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 ] ''
|
||||
@@ -62,24 +89,125 @@ in
|
||||
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)
|
||||
]);
|
||||
|
||||
separator = lib.mkOption {
|
||||
type = lib.types.enum [
|
||||
"-"
|
||||
"="
|
||||
"+"
|
||||
];
|
||||
default = "=";
|
||||
description = "First separator character in generated addresses";
|
||||
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/main/doc/postsrsd.conf) for possible values.
|
||||
'';
|
||||
};
|
||||
|
||||
user = lib.mkOption {
|
||||
@@ -93,15 +221,6 @@ 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.'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -143,15 +262,21 @@ in
|
||||
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;
|
||||
RuntimeDirectory = "postsrsd";
|
||||
LoadCredential = "secrets-file:${cfg.secretsFile}";
|
||||
|
||||
PrivateDevices = true;
|
||||
PrivateMounts = true;
|
||||
PrivateTmp = true;
|
||||
PrivateUsers = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user