diff --git a/nixos/modules/services/backup/syncoid.nix b/nixos/modules/services/backup/syncoid.nix index 096094125f8b5..f4d09670c8fdf 100644 --- a/nixos/modules/services/backup/syncoid.nix +++ b/nixos/modules/services/backup/syncoid.nix @@ -249,6 +249,30 @@ in environment.LD_LIBRARY_PATH = config.system.nssModules.path; serviceConfig = { ExecStartPre = + # Recursively remove any residual permissions + # given on local+descendant datasets (source, target or target's parent) + # to any currently unknown (hence unused) systemd dynamic users (UID/GID range 61184…65519), + # which happens when a crash has occurred + # during any previous run of a syncoid-*.service (not only this one). + map (dataset: + "+" + pkgs.writeShellScript "zfs-unallow-unused-dynamic-users" '' + set -eu + zfs allow "$1" | + sed -ne 's/^\t\(user\|group\) (unknown: \([0-9]\+\)).*/\1 \2/p' | + { + declare -a uids + while read -r role id; do + if [ "$id" -ge 61184 ] && [ "$id" -le 65519 ]; then + case "$role" in + (user) uids+=("$id");; + esac + fi + done + zfs unallow -r -u "$(printf %s, "''${uids[@]}")" "$1" + } + '' + " " + lib.escapeShellArg dataset + ) (localDatasetName c.source ++ localDatasetName c.target ++ map builtins.dirOf (localDatasetName c.target)) ++ + # For a local source, allow the localSourceAllow ZFS permissions. map (dataset: "+/run/booted-system/sw/bin/zfs allow $USER " + lib.escapeShellArgs [ (lib.concatStringsSep "," c.localSourceAllow) dataset ] @@ -265,6 +289,8 @@ in zfs allow "$USER" ${lib.escapeShellArg (lib.concatStringsSep "," c.localTargetAllow)} "$dataset" '' + " " + lib.escapeShellArg dataset ) (localDatasetName c.target) ++ + # Adding a user to an nftables set will not persist across a reboot, + # hence there is no need to cleanup residual dynamic users remaining in it after a crash. lib.optional cfg.nftables.enable "+${pkgs.nftables}/bin/nft add element inet filter nixos-syncoid-uids { $USER }"; ExecStopPost = let diff --git a/nixos/tests/sanoid.nix b/nixos/tests/sanoid.nix index ca2473da41e1a..9f5d52c2c527e 100644 --- a/nixos/tests/sanoid.nix +++ b/nixos/tests/sanoid.nix @@ -51,17 +51,21 @@ import ./make-test-python.nix ( enable = true; sshKey = "/var/lib/syncoid/id_ecdsa"; commands = { - # Sync snapshot taken by sanoid - "pool/sanoid" = { - target = "root@target:pool/sanoid"; - extraArgs = [ - "--no-sync-snap" - "--create-bookmark" - ]; - }; # Take snapshot and sync "pool/syncoid".target = "root@target:pool/syncoid"; + # Sync the same dataset to different targets + "pool/sanoid1" = { + source = "pool/sanoid"; + target = "root@target:pool/sanoid1"; + extraArgs = [ "--no-sync-snap" "--create-bookmark" ]; + }; + "pool/sanoid2" = { + source = "pool/sanoid"; + target = "root@target:pool/sanoid2"; + extraArgs = [ "--no-sync-snap" "--create-bookmark" ]; + }; + # Test pool without parent (regression test for https://github.com/NixOS/nixpkgs/pull/180111) "pool".target = "root@target:pool/full-pool"; @@ -95,6 +99,7 @@ import ./make-test-python.nix ( "zfs create pool/syncoid", "udevadm settle", ) + target.succeed( "mkdir /mnt", "parted --script /dev/vdb -- mklabel msdos mkpart primary 1024M -1s", @@ -107,55 +112,45 @@ import ./make-test-python.nix ( "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( - "mkdir -m 700 -p /var/lib/syncoid", - "cat '${snakeOilPrivateKey}' > /var/lib/syncoid/id_ecdsa", - "chmod 600 /var/lib/syncoid/id_ecdsa", - ) + with subtest("Take snapshots with sanoid"): + source.succeed("touch /mnt/pool/sanoid/test.txt") + source.succeed("touch /mnt/pool/compat/test.txt") + source.systemctl("start --wait sanoid.service") + + # Add some unused dynamic users to the stateful allow list of ZFS datasets, + # simulating a state where they remain after the system crashed, + # to check they'll be correctly removed by the syncoid services. + # Each syncoid service run from now may reuse at most one of them for itself. + source.succeed( + "zfs allow -u $(printf %s, {61184..61200})65519 dedup pool", + "zfs allow -u $(printf %s, {61184..61200})65519 dedup pool/sanoid", + "zfs allow -u $(printf %s, {61184..61200})65519 dedup pool/syncoid", + ) + + with subtest("sync snapshots"): + target.wait_for_open_port(22) + source.succeed("touch /mnt/pool/syncoid/test.txt") + + source.systemctl("start --wait syncoid-pool-syncoid.service") + target.succeed("cat /mnt/pool/syncoid/test.txt") + + source.systemctl("start --wait syncoid-pool-sanoid{1,2}.service") + target.succeed("cat /mnt/pool/sanoid1/test.txt") + target.succeed("cat /mnt/pool/sanoid2/test.txt") - # Take snapshot with sanoid - source.succeed("touch /mnt/pool/sanoid/test.txt") - source.succeed("touch /mnt/pool/compat/test.txt") - source.systemctl("start --wait sanoid.service") + source.systemctl("start --wait syncoid-pool.service") + target.succeed("[[ -d /mnt/pool/full-pool/syncoid ]]") + + source.systemctl("start --wait syncoid-pool-compat.service") + target.succeed("cat /mnt/pool/compat/test.txt") assert len(source.succeed("zfs allow pool")) == 0, "Pool shouldn't have delegated permissions set after snapshotting" assert len(source.succeed("zfs allow pool/sanoid")) == 0, "Sanoid dataset shouldn't have delegated permissions set after snapshotting" assert len(source.succeed("zfs allow pool/syncoid")) == 0, "Syncoid dataset shouldn't have delegated permissions set after snapshotting" - - # Sync snapshots - target.wait_for_open_port(22) - source.succeed("touch /mnt/pool/syncoid/test.txt") - source.systemctl("start --wait syncoid-pool-sanoid.service") - target.succeed("cat /mnt/pool/sanoid/test.txt") - source.systemctl("start --wait syncoid-pool-syncoid.service") - source.systemctl("start --wait syncoid-pool-syncoid.service") - target.succeed("cat /mnt/pool/syncoid/test.txt") - assert(len(source.succeed("zfs list -H -t snapshot pool/syncoid").splitlines()) == 1), "Syncoid should only retain one sync snapshot" - # Sync snapshots - target.wait_for_open_port(22) - source.succeed("touch /mnt/pool/syncoid/test.txt") - source.systemctl("start --wait syncoid-pool-sanoid.service") - target.succeed("cat /mnt/pool/sanoid/test.txt") - source.systemctl("start --wait syncoid-pool-syncoid.service") - target.succeed("cat /mnt/pool/syncoid/test.txt") - source.systemctl("start --wait syncoid-pool-sanoid{1,2}.service") - target.succeed("cat /mnt/pool/sanoid1/test.txt") - target.succeed("cat /mnt/pool/sanoid2/test.txt") - - source.systemctl("start --wait syncoid-pool.service") - target.succeed("[[ -d /mnt/pool/full-pool/syncoid ]]") - - source.systemctl("start --wait syncoid-pool-compat.service") - target.succeed("cat /mnt/pool/compat/test.txt") - - assert len(source.succeed("zfs allow pool")) == 0, "Pool shouldn't have delegated permissions set after syncing snapshots" - assert len(source.succeed("zfs allow pool/sanoid")) == 0, "Sanoid dataset shouldn't have delegated permissions set after syncing snapshots" - assert len(source.succeed("zfs allow pool/syncoid")) == 0, "Syncoid dataset shouldn't have delegated permissions set after syncing snapshots" ''; } )