Skip to content

Commit

Permalink
nixos/syncoid: zfs-unallow unused dynamic users
Browse files Browse the repository at this point in the history
  • Loading branch information
ju1m committed Dec 23, 2024
1 parent bd461ab commit cbb9954
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 48 deletions.
26 changes: 26 additions & 0 deletions nixos/modules/services/backup/syncoid.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]
Expand All @@ -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
Expand Down
91 changes: 43 additions & 48 deletions nixos/tests/sanoid.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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",
Expand All @@ -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"
'';
}
)

0 comments on commit cbb9954

Please sign in to comment.