Merge pull request #79759 from lopsided98/syncoid-no-root

nixos/syncoid: automatically setup privilege delegation

Changed files
+53 -8
nixos
modules
services
backup
tests
+48 -4
nixos/modules/services/backup/syncoid.nix
···
let
cfg = config.services.syncoid;
+
+
# Extract pool names of local datasets (ones that don't contain "@") that
+
# have the specified type (either "source" or "target")
+
getPools = type: unique (map (d: head (builtins.match "([^/]+).*" d)) (
+
# Filter local datasets
+
filter (d: !hasInfix "@" d)
+
# Get datasets of the specified type
+
(catAttrs type (attrValues cfg.commands))
+
));
in {
# Interface
···
user = mkOption {
type = types.str;
-
default = "root";
+
default = "syncoid";
example = "backup";
description = ''
-
The user for the service. Sudo or ZFS privilege delegation must be
-
configured to use a user other than root.
+
The user for the service. ZFS privilege delegation will be
+
automatically configured for any local pools used by syncoid if this
+
option is set to a user other than root. The user will be given the
+
"hold" and "send" privileges on any pool that has datasets being sent
+
and the "create", "mount", "receive", and "rollback" privileges on
+
any pool that has datasets being received.
'';
+
};
+
+
group = mkOption {
+
type = types.str;
+
default = "syncoid";
+
example = "backup";
+
description = "The group for the service.";
};
sshKey = mkOption {
···
# Implementation
config = mkIf cfg.enable {
+
users = {
+
users = mkIf (cfg.user == "syncoid") {
+
syncoid = {
+
group = cfg.group;
+
isSystemUser = true;
+
};
+
};
+
groups = mkIf (cfg.group == "syncoid") {
+
syncoid = {};
+
};
+
};
+
systemd.services.syncoid = {
description = "Syncoid ZFS synchronization service";
script = concatMapStringsSep "\n" (c: lib.escapeShellArgs
···
++ c.extraArgs
++ [ "--sendoptions" c.sendOptions
"--recvoptions" c.recvOptions
+
"--no-privilege-elevation"
c.source c.target
])) (attrValues cfg.commands);
after = [ "zfs.target" ];
-
serviceConfig.User = cfg.user;
+
serviceConfig = {
+
ExecStartPre = (map (pool: lib.escapeShellArgs [
+
"+/run/booted-system/sw/bin/zfs" "allow"
+
cfg.user "hold,send" pool
+
]) (getPools "source")) ++
+
(map (pool: lib.escapeShellArgs [
+
"+/run/booted-system/sw/bin/zfs" "allow"
+
cfg.user "create,mount,receive,rollback" pool
+
]) (getPools "target"));
+
User = cfg.user;
+
Group = cfg.group;
+
};
startAt = cfg.interval;
};
};
+5 -4
nixos/tests/sanoid.nix
···
services.syncoid = {
enable = true;
-
sshKey = "/root/.ssh/id_ecdsa";
+
sshKey = "/var/lib/syncoid/id_ecdsa";
commonArgs = [ "--no-sync-snap" ];
commands."pool/test".target = "root@target:pool/test";
};
···
"udevadm settle",
)
-
source.succeed("mkdir -m 700 /root/.ssh")
source.succeed(
-
"cat '${snakeOilPrivateKey}' > /root/.ssh/id_ecdsa"
+
"mkdir -m 700 -p /var/lib/syncoid",
+
"cat '${snakeOilPrivateKey}' > /var/lib/syncoid/id_ecdsa",
+
"chmod 600 /var/lib/syncoid/id_ecdsa",
+
"chown -R syncoid:syncoid /var/lib/syncoid/",
)
-
source.succeed("chmod 600 /root/.ssh/id_ecdsa")
source.succeed("touch /tmp/mnt/test.txt")
source.systemctl("start --wait sanoid.service")