From 70862830f0a1415acd6c9fb7fc74ab59530f41dc Mon Sep 17 00:00:00 2001 From: Elis Hirwing Date: Sun, 25 Jul 2021 09:59:18 +0200 Subject: [PATCH 01/10] nixos/syncoid: Extract datasets rather than pools When sending or receiving datasets with the old implementation it wouldn't matter which dataset we were sending or receiving, we would always delegate permissions to the entire pool. --- nixos/modules/services/backup/syncoid.nix | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/nixos/modules/services/backup/syncoid.nix b/nixos/modules/services/backup/syncoid.nix index 888ef20f642..80a704a7d26 100644 --- a/nixos/modules/services/backup/syncoid.nix +++ b/nixos/modules/services/backup/syncoid.nix @@ -5,9 +5,9 @@ with lib; let cfg = config.services.syncoid; - # Extract the pool name of a local dataset (any dataset not containing "@") - localPoolName = d: optionals (d != null) ( - let m = builtins.match "([^/@]+)[^@]*" d; in + # Extract local dasaset names (so no datasets containing "@") + localDatasetName = d: optionals (d != null) ( + let m = builtins.match "([^/@]+[^@]*)" d; in optionals (m != null) m); # Escape as required by: https://www.freedesktop.org/software/systemd/man/systemd.unit.html @@ -206,15 +206,15 @@ in { path = [ "/run/booted-system/sw/bin/" ]; serviceConfig = { ExecStartPre = - map (pool: lib.escapeShellArgs [ + map (dataset: lib.escapeShellArgs [ "+/run/booted-system/sw/bin/zfs" "allow" - cfg.user "bookmark,hold,send,snapshot,destroy" pool + cfg.user "bookmark,hold,send,snapshot,destroy" dataset # Permissions snapshot and destroy are in case --no-sync-snap is not used - ]) (localPoolName c.source) ++ - map (pool: lib.escapeShellArgs [ + ]) (localDatasetName c.source) ++ + map (dataset: lib.escapeShellArgs [ "+/run/booted-system/sw/bin/zfs" "allow" - cfg.user "create,mount,receive,rollback" pool - ]) (localPoolName c.target); + cfg.user "create,mount,receive,rollback" dataset + ]) (localDatasetName c.target); ExecStart = lib.escapeShellArgs ([ "${pkgs.sanoid}/bin/syncoid" ] ++ optionals c.useCommonArgs cfg.commonArgs ++ optional c.recursive "-r" From bb35e7c4044432111ab9fec5ef9c4260ae651582 Mon Sep 17 00:00:00 2001 From: Elis Hirwing Date: Sun, 25 Jul 2021 10:00:37 +0200 Subject: [PATCH 02/10] nixos/sanoid: Extract datasets rather than pools When making new snapshots we only need to delegate permissions to the specific dataset rather than the entire pool. --- nixos/modules/services/backup/sanoid.nix | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/nixos/modules/services/backup/sanoid.nix b/nixos/modules/services/backup/sanoid.nix index abc4def1c61..9713581165b 100644 --- a/nixos/modules/services/backup/sanoid.nix +++ b/nixos/modules/services/backup/sanoid.nix @@ -70,8 +70,8 @@ let processChildrenOnly = process_children_only; }; - # Extract pool names from configured datasets - pools = unique (map (d: head (builtins.match "([^/]+).*" d)) (attrNames cfg.datasets)); + # Extract unique dataset names + datasets = unique (attrNames cfg.datasets); configFile = let mkValueString = v: @@ -156,18 +156,18 @@ in { systemd.services.sanoid = { description = "Sanoid snapshot service"; serviceConfig = { - ExecStartPre = map (pool: lib.escapeShellArgs [ + ExecStartPre = map (dataset: lib.escapeShellArgs [ "+/run/booted-system/sw/bin/zfs" "allow" - "sanoid" "snapshot,mount,destroy" pool - ]) pools; + "sanoid" "snapshot,mount,destroy" dataset + ]) datasets; ExecStart = lib.escapeShellArgs ([ "${pkgs.sanoid}/bin/sanoid" "--cron" "--configdir" (pkgs.writeTextDir "sanoid.conf" configFile) ] ++ cfg.extraArgs); - ExecStopPost = map (pool: lib.escapeShellArgs [ - "+/run/booted-system/sw/bin/zfs" "unallow" "sanoid" pool - ]) pools; + ExecStopPost = map (dataset: lib.escapeShellArgs [ + "+/run/booted-system/sw/bin/zfs" "unallow" "sanoid" dataset + ]) datasets; User = "sanoid"; Group = "sanoid"; DynamicUser = true; From ecd32b8104e6cca16fe1b2cfb89f39a8c7c01731 Mon Sep 17 00:00:00 2001 From: Elis Hirwing Date: Sun, 25 Jul 2021 18:22:05 +0200 Subject: [PATCH 03/10] nixos/syncoid: Build unallow commands as a post job to drop permissions --- nixos/modules/services/backup/syncoid.nix | 24 ++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/nixos/modules/services/backup/syncoid.nix b/nixos/modules/services/backup/syncoid.nix index 80a704a7d26..71007f6c38e 100644 --- a/nixos/modules/services/backup/syncoid.nix +++ b/nixos/modules/services/backup/syncoid.nix @@ -14,6 +14,14 @@ let escapeUnitName = name: lib.concatMapStrings (s: if lib.isList s then "-" else s) (builtins.split "[^a-zA-Z0-9_.\\-]+" name); + + # Function to build "zfs allow" and "zfs unallow" commands for the + # filesystems we've delegated permissions to. + buildAllowCommand = zfsAction: permissions: dataset: lib.escapeShellArgs [ + # Here we explicitly use the booted system to guarantee the stable API needed by ZFS + "-+/run/booted-system/sw/bin/zfs" zfsAction + cfg.user (concatStringsSep "," permissions) dataset + ]; in { # Interface @@ -206,15 +214,13 @@ in { path = [ "/run/booted-system/sw/bin/" ]; serviceConfig = { ExecStartPre = - map (dataset: lib.escapeShellArgs [ - "+/run/booted-system/sw/bin/zfs" "allow" - cfg.user "bookmark,hold,send,snapshot,destroy" dataset - # Permissions snapshot and destroy are in case --no-sync-snap is not used - ]) (localDatasetName c.source) ++ - map (dataset: lib.escapeShellArgs [ - "+/run/booted-system/sw/bin/zfs" "allow" - cfg.user "create,mount,receive,rollback" dataset - ]) (localDatasetName c.target); + # Permissions snapshot and destroy are in case --no-sync-snap is not used + (map (buildAllowCommand "allow" [ "bookmark" "hold" "send" "snapshot" "destroy" ]) (localDatasetName c.source)) ++ + (map (buildAllowCommand "allow" [ "create" "mount" "receive" "rollback" ]) (localDatasetName c.target)); + ExecStopPost = + # Permissions snapshot and destroy are in case --no-sync-snap is not used + (map (buildAllowCommand "unallow" [ "bookmark" "hold" "send" "snapshot" "destroy" ]) (localDatasetName c.source)) ++ + (map (buildAllowCommand "unallow" [ "create" "mount" "receive" "rollback" ]) (localDatasetName c.target)); ExecStart = lib.escapeShellArgs ([ "${pkgs.sanoid}/bin/syncoid" ] ++ optionals c.useCommonArgs cfg.commonArgs ++ optional c.recursive "-r" From b9f98165ab22b3981d7017ce88f268c4176f8072 Mon Sep 17 00:00:00 2001 From: Elis Hirwing Date: Sun, 25 Jul 2021 18:27:36 +0200 Subject: [PATCH 04/10] nixos/sanoid: Use a function to build allow/unallow commands --- nixos/modules/services/backup/sanoid.nix | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/nixos/modules/services/backup/sanoid.nix b/nixos/modules/services/backup/sanoid.nix index 9713581165b..c7f276e3f04 100644 --- a/nixos/modules/services/backup/sanoid.nix +++ b/nixos/modules/services/backup/sanoid.nix @@ -73,6 +73,17 @@ let # Extract unique dataset names datasets = unique (attrNames cfg.datasets); + # Function to build "zfs allow" and "zfs unallow" commands for the + # filesystems we've delegated permissions to. + buildAllowCommand = zfsAction: permissions: dataset: lib.escapeShellArgs [ + # Here we explicitly use the booted system to guarantee the stable API needed by ZFS + "-+/run/booted-system/sw/bin/zfs" + zfsAction + "sanoid" + (concatStringsSep "," permissions) + dataset + ]; + configFile = let mkValueString = v: if builtins.isList v then concatStringsSep "," v @@ -156,18 +167,13 @@ in { systemd.services.sanoid = { description = "Sanoid snapshot service"; serviceConfig = { - ExecStartPre = map (dataset: lib.escapeShellArgs [ - "+/run/booted-system/sw/bin/zfs" "allow" - "sanoid" "snapshot,mount,destroy" dataset - ]) datasets; + ExecStartPre = (map (buildAllowCommand "allow" [ "snapshot" "mount" "destroy" ]) datasets); + ExecStopPost = (map (buildAllowCommand "unallow" [ "snapshot" "mount" "destroy" ]) datasets); ExecStart = lib.escapeShellArgs ([ "${pkgs.sanoid}/bin/sanoid" "--cron" "--configdir" (pkgs.writeTextDir "sanoid.conf" configFile) ] ++ cfg.extraArgs); - ExecStopPost = map (dataset: lib.escapeShellArgs [ - "+/run/booted-system/sw/bin/zfs" "unallow" "sanoid" dataset - ]) datasets; User = "sanoid"; Group = "sanoid"; DynamicUser = true; From fa58d89b24611461304f591c64a5539bf1c13a7c Mon Sep 17 00:00:00 2001 From: Elis Hirwing Date: Sun, 25 Jul 2021 18:31:19 +0200 Subject: [PATCH 05/10] nixos/syncoid: Reformat file with nixpkgs-fmt --- nixos/modules/services/backup/syncoid.nix | 393 ++++++++++++---------- 1 file changed, 206 insertions(+), 187 deletions(-) diff --git a/nixos/modules/services/backup/syncoid.nix b/nixos/modules/services/backup/syncoid.nix index 71007f6c38e..61a905a488a 100644 --- a/nixos/modules/services/backup/syncoid.nix +++ b/nixos/modules/services/backup/syncoid.nix @@ -8,206 +8,213 @@ let # Extract local dasaset names (so no datasets containing "@") localDatasetName = d: optionals (d != null) ( let m = builtins.match "([^/@]+[^@]*)" d; in - optionals (m != null) m); + optionals (m != null) m + ); # Escape as required by: https://www.freedesktop.org/software/systemd/man/systemd.unit.html escapeUnitName = name: lib.concatMapStrings (s: if lib.isList s then "-" else s) - (builtins.split "[^a-zA-Z0-9_.\\-]+" name); + (builtins.split "[^a-zA-Z0-9_.\\-]+" name); # Function to build "zfs allow" and "zfs unallow" commands for the # filesystems we've delegated permissions to. buildAllowCommand = zfsAction: permissions: dataset: lib.escapeShellArgs [ # Here we explicitly use the booted system to guarantee the stable API needed by ZFS - "-+/run/booted-system/sw/bin/zfs" zfsAction - cfg.user (concatStringsSep "," permissions) dataset + "-+/run/booted-system/sw/bin/zfs" + zfsAction + cfg.user + (concatStringsSep "," permissions) + dataset ]; -in { +in +{ - # Interface + # Interface - options.services.syncoid = { - enable = mkEnableOption "Syncoid ZFS synchronization service"; + options.services.syncoid = { + enable = mkEnableOption "Syncoid ZFS synchronization service"; - interval = mkOption { - type = types.str; - default = "hourly"; - example = "*-*-* *:15:00"; - description = '' - Run syncoid at this interval. The default is to run hourly. + interval = mkOption { + type = types.str; + default = "hourly"; + example = "*-*-* *:15:00"; + description = '' + Run syncoid at this interval. The default is to run hourly. - The format is described in - systemd.time - 7. - ''; - }; + The format is described in + systemd.time + 7. + ''; + }; - user = mkOption { - type = types.str; - default = "syncoid"; - example = "backup"; - description = '' - 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. - ''; - }; + user = mkOption { + type = types.str; + default = "syncoid"; + example = "backup"; + description = '' + 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."; - }; + group = mkOption { + type = types.str; + default = "syncoid"; + example = "backup"; + description = "The group for the service."; + }; - sshKey = mkOption { - type = types.nullOr types.path; - # Prevent key from being copied to store - apply = mapNullable toString; - default = null; - description = '' - SSH private key file to use to login to the remote system. Can be - overridden in individual commands. - ''; - }; + sshKey = mkOption { + type = types.nullOr types.path; + # Prevent key from being copied to store + apply = mapNullable toString; + default = null; + description = '' + SSH private key file to use to login to the remote system. Can be + overridden in individual commands. + ''; + }; - commonArgs = mkOption { - type = types.listOf types.str; - default = []; - example = [ "--no-sync-snap" ]; - description = '' - Arguments to add to every syncoid command, unless disabled for that - command. See - - for available options. - ''; - }; + commonArgs = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "--no-sync-snap" ]; + description = '' + Arguments to add to every syncoid command, unless disabled for that + command. See + + for available options. + ''; + }; - service = mkOption { - type = types.attrs; - default = {}; - description = '' - Systemd configuration common to all syncoid services. - ''; - }; + service = mkOption { + type = types.attrs; + default = { }; + description = '' + Systemd configuration common to all syncoid services. + ''; + }; - commands = mkOption { - type = types.attrsOf (types.submodule ({ name, ... }: { - options = { - source = mkOption { - type = types.str; - example = "pool/dataset"; - description = '' - Source ZFS dataset. Can be either local or remote. Defaults to - the attribute name. - ''; - }; - - target = mkOption { - type = types.str; - example = "user@server:pool/dataset"; - description = '' - Target ZFS dataset. Can be either local - (pool/dataset) or remote - (user@server:pool/dataset). - ''; - }; - - recursive = mkEnableOption ''the transfer of child datasets''; - - sshKey = mkOption { - type = types.nullOr types.path; - # Prevent key from being copied to store - apply = mapNullable toString; - description = '' - SSH private key file to use to login to the remote system. - Defaults to option. - ''; - }; - - sendOptions = mkOption { - type = types.separatedString " "; - default = ""; - example = "Lc e"; - description = '' - Advanced options to pass to zfs send. Options are specified - without their leading dashes and separated by spaces. - ''; - }; - - recvOptions = mkOption { - type = types.separatedString " "; - default = ""; - example = "ux recordsize o compression=lz4"; - description = '' - Advanced options to pass to zfs recv. Options are specified - without their leading dashes and separated by spaces. - ''; - }; - - useCommonArgs = mkOption { - type = types.bool; - default = true; - description = '' - Whether to add the configured common arguments to this command. - ''; - }; - - service = mkOption { - type = types.attrs; - default = {}; - description = '' - Systemd configuration specific to this syncoid service. - ''; - }; - - extraArgs = mkOption { - type = types.listOf types.str; - default = []; - example = [ "--sshport 2222" ]; - description = "Extra syncoid arguments for this command."; - }; + commands = mkOption { + type = types.attrsOf (types.submodule ({ name, ... }: { + options = { + source = mkOption { + type = types.str; + example = "pool/dataset"; + description = '' + Source ZFS dataset. Can be either local or remote. Defaults to + the attribute name. + ''; }; - config = { - source = mkDefault name; - sshKey = mkDefault cfg.sshKey; + + target = mkOption { + type = types.str; + example = "user@server:pool/dataset"; + description = '' + Target ZFS dataset. Can be either local + (pool/dataset) or remote + (user@server:pool/dataset). + ''; }; - })); - default = {}; - example = literalExample '' - { - "pool/test".target = "root@target:pool/test"; - } - ''; - description = "Syncoid commands to run."; + + recursive = mkEnableOption ''the transfer of child datasets''; + + sshKey = mkOption { + type = types.nullOr types.path; + # Prevent key from being copied to store + apply = mapNullable toString; + description = '' + SSH private key file to use to login to the remote system. + Defaults to option. + ''; + }; + + sendOptions = mkOption { + type = types.separatedString " "; + default = ""; + example = "Lc e"; + description = '' + Advanced options to pass to zfs send. Options are specified + without their leading dashes and separated by spaces. + ''; + }; + + recvOptions = mkOption { + type = types.separatedString " "; + default = ""; + example = "ux recordsize o compression=lz4"; + description = '' + Advanced options to pass to zfs recv. Options are specified + without their leading dashes and separated by spaces. + ''; + }; + + useCommonArgs = mkOption { + type = types.bool; + default = true; + description = '' + Whether to add the configured common arguments to this command. + ''; + }; + + service = mkOption { + type = types.attrs; + default = { }; + description = '' + Systemd configuration specific to this syncoid service. + ''; + }; + + extraArgs = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "--sshport 2222" ]; + description = "Extra syncoid arguments for this command."; + }; + }; + config = { + source = mkDefault name; + sshKey = mkDefault cfg.sshKey; + }; + })); + default = { }; + example = literalExample '' + { + "pool/test".target = "root@target:pool/test"; + } + ''; + description = "Syncoid commands to run."; + }; + }; + + # Implementation + + config = mkIf cfg.enable { + users = { + users = mkIf (cfg.user == "syncoid") { + syncoid = { + group = cfg.group; + isSystemUser = true; + # For syncoid to be able to create /var/lib/syncoid/.ssh/ + # and to use custom ssh_config or known_hosts. + home = "/var/lib/syncoid"; + createHome = false; + }; + }; + groups = mkIf (cfg.group == "syncoid") { + syncoid = { }; }; }; - # Implementation - - config = mkIf cfg.enable { - users = { - users = mkIf (cfg.user == "syncoid") { - syncoid = { - group = cfg.group; - isSystemUser = true; - # For syncoid to be able to create /var/lib/syncoid/.ssh/ - # and to use custom ssh_config or known_hosts. - home = "/var/lib/syncoid"; - createHome = false; - }; - }; - groups = mkIf (cfg.group == "syncoid") { - syncoid = {}; - }; - }; - - systemd.services = mapAttrs' (name: c: + systemd.services = mapAttrs' + (name: c: nameValuePair "syncoid-${escapeUnitName name}" (mkMerge [ - { description = "Syncoid ZFS synchronization from ${c.source} to ${c.target}"; + { + description = "Syncoid ZFS synchronization from ${c.source} to ${c.target}"; after = [ "zfs.target" ]; startAt = cfg.interval; # syncoid may need zpool to get feature@extensible_dataset @@ -226,11 +233,15 @@ in { ++ optional c.recursive "-r" ++ optionals (c.sshKey != null) [ "--sshkey" c.sshKey ] ++ c.extraArgs - ++ [ "--sendoptions" c.sendOptions - "--recvoptions" c.recvOptions - "--no-privilege-elevation" - c.source c.target - ]); + ++ [ + "--sendoptions" + c.sendOptions + "--recvoptions" + c.recvOptions + "--no-privilege-elevation" + c.source + c.target + ]); User = cfg.user; Group = cfg.group; StateDirectory = [ "syncoid" ]; @@ -246,7 +257,7 @@ in { # systemd-analyze security | grep syncoid-'*' AmbientCapabilities = ""; CapabilityBoundingSet = ""; - DeviceAllow = ["/dev/zfs"]; + DeviceAllow = [ "/dev/zfs" ]; LockPersonality = true; MemoryDenyWriteExecute = true; NoNewPrivileges = true; @@ -272,7 +283,7 @@ in { BindPaths = [ "/dev/zfs" ]; BindReadOnlyPaths = [ builtins.storeDir "/etc" "/run" "/bin/sh" ]; # Avoid useless mounting of RootDirectory= in the own RootDirectory= of ExecStart='s mount namespace. - InaccessiblePaths = ["-+/run/syncoid/${escapeUnitName name}"]; + InaccessiblePaths = [ "-+/run/syncoid/${escapeUnitName name}" ]; MountAPIVFS = true; # Create RootDirectory= in the host's mount namespace. RuntimeDirectory = [ "syncoid/${escapeUnitName name}" ]; @@ -283,8 +294,15 @@ in { # perf stat -x, 2>perf.log -e 'syscalls:sys_enter_*' syncoid … # awk >perf.syscalls -F "," '$1 > 0 {sub("syscalls:sys_enter_","",$3); print $3}' perf.log # systemd-analyze syscall-filter | grep -v -e '#' | sed -e ':loop; /^[^ ]/N; s/\n //; t loop' | grep $(printf ' -e \\<%s\\>' $(cat perf.syscalls)) | cut -f 1 -d ' ' - "~@aio" "~@chown" "~@keyring" "~@memlock" "~@privileged" - "~@resources" "~@setuid" "~@sync" "~@timer" + "~@aio" + "~@chown" + "~@keyring" + "~@memlock" + "~@privileged" + "~@resources" + "~@setuid" + "~@sync" + "~@timer" ]; SystemCallArchitectures = "native"; # This is for BindPaths= and BindReadOnlyPaths= @@ -294,8 +312,9 @@ in { } cfg.service c.service - ])) cfg.commands; - }; + ])) + cfg.commands; + }; - meta.maintainers = with maintainers; [ julm lopsided98 ]; - } + meta.maintainers = with maintainers; [ julm lopsided98 ]; +} From ea9d5876a05a413c199f404696320d8c5f2cb70d Mon Sep 17 00:00:00 2001 From: Elis Hirwing Date: Sun, 25 Jul 2021 18:31:42 +0200 Subject: [PATCH 06/10] nixos/sanoid: Reformat file with nixpkgs-fmt --- nixos/modules/services/backup/sanoid.nix | 197 ++++++++++++----------- 1 file changed, 101 insertions(+), 96 deletions(-) diff --git a/nixos/modules/services/backup/sanoid.nix b/nixos/modules/services/backup/sanoid.nix index c7f276e3f04..41d0e2e1df6 100644 --- a/nixos/modules/services/backup/sanoid.nix +++ b/nixos/modules/services/backup/sanoid.nix @@ -52,7 +52,7 @@ let use_template = mkOption { description = "Names of the templates to use for this dataset."; type = types.listOf (types.enum (attrNames cfg.templates)); - default = []; + default = [ ]; }; useTemplate = use_template; @@ -84,108 +84,113 @@ let dataset ]; - configFile = let - mkValueString = v: - if builtins.isList v then concatStringsSep "," v - else generators.mkValueStringDefault {} v; + configFile = + let + mkValueString = v: + if builtins.isList v then concatStringsSep "," v + else generators.mkValueStringDefault { } v; - mkKeyValue = k: v: if v == null then "" - else if k == "processChildrenOnly" then "" - else if k == "useTemplate" then "" - else generators.mkKeyValueDefault { inherit mkValueString; } "=" k v; - in generators.toINI { inherit mkKeyValue; } cfg.settings; + mkKeyValue = k: v: + if v == null then "" + else if k == "processChildrenOnly" then "" + else if k == "useTemplate" then "" + else generators.mkKeyValueDefault { inherit mkValueString; } "=" k v; + in + generators.toINI { inherit mkKeyValue; } cfg.settings; -in { +in +{ - # Interface + # Interface - options.services.sanoid = { - enable = mkEnableOption "Sanoid ZFS snapshotting service"; + options.services.sanoid = { + enable = mkEnableOption "Sanoid ZFS snapshotting service"; - interval = mkOption { - type = types.str; - default = "hourly"; - example = "daily"; - description = '' - Run sanoid at this interval. The default is to run hourly. + interval = mkOption { + type = types.str; + default = "hourly"; + example = "daily"; + description = '' + Run sanoid at this interval. The default is to run hourly. - The format is described in - systemd.time - 7. - ''; - }; - - datasets = mkOption { - type = types.attrsOf (types.submodule ({config, options, ...}: { - freeformType = datasetSettingsType; - options = commonOptions // datasetOptions; - config.use_template = mkAliasDefinitions (mkDefault options.useTemplate or {}); - config.process_children_only = mkAliasDefinitions (mkDefault options.processChildrenOnly or {}); - })); - default = {}; - description = "Datasets to snapshot."; - }; - - templates = mkOption { - type = types.attrsOf (types.submodule { - freeformType = datasetSettingsType; - options = commonOptions; - }); - default = {}; - description = "Templates for datasets."; - }; - - settings = mkOption { - type = types.attrsOf datasetSettingsType; - description = '' - Free-form settings written directly to the config file. See - - for allowed values. - ''; - }; - - extraArgs = mkOption { - type = types.listOf types.str; - default = []; - example = [ "--verbose" "--readonly" "--debug" ]; - description = '' - Extra arguments to pass to sanoid. See - - for allowed options. - ''; - }; + The format is described in + systemd.time + 7. + ''; }; - # Implementation - - config = mkIf cfg.enable { - services.sanoid.settings = mkMerge [ - (mapAttrs' (d: v: nameValuePair ("template_" + d) v) cfg.templates) - (mapAttrs (d: v: v) cfg.datasets) - ]; - - systemd.services.sanoid = { - description = "Sanoid snapshot service"; - serviceConfig = { - ExecStartPre = (map (buildAllowCommand "allow" [ "snapshot" "mount" "destroy" ]) datasets); - ExecStopPost = (map (buildAllowCommand "unallow" [ "snapshot" "mount" "destroy" ]) datasets); - ExecStart = lib.escapeShellArgs ([ - "${pkgs.sanoid}/bin/sanoid" - "--cron" - "--configdir" (pkgs.writeTextDir "sanoid.conf" configFile) - ] ++ cfg.extraArgs); - User = "sanoid"; - Group = "sanoid"; - DynamicUser = true; - RuntimeDirectory = "sanoid"; - CacheDirectory = "sanoid"; - }; - # Prevents missing snapshots during DST changes - environment.TZ = "UTC"; - after = [ "zfs.target" ]; - startAt = cfg.interval; - }; + datasets = mkOption { + type = types.attrsOf (types.submodule ({ config, options, ... }: { + freeformType = datasetSettingsType; + options = commonOptions // datasetOptions; + config.use_template = mkAliasDefinitions (mkDefault options.useTemplate or { }); + config.process_children_only = mkAliasDefinitions (mkDefault options.processChildrenOnly or { }); + })); + default = { }; + description = "Datasets to snapshot."; }; - meta.maintainers = with maintainers; [ lopsided98 ]; - } + templates = mkOption { + type = types.attrsOf (types.submodule { + freeformType = datasetSettingsType; + options = commonOptions; + }); + default = { }; + description = "Templates for datasets."; + }; + + settings = mkOption { + type = types.attrsOf datasetSettingsType; + description = '' + Free-form settings written directly to the config file. See + + for allowed values. + ''; + }; + + extraArgs = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "--verbose" "--readonly" "--debug" ]; + description = '' + Extra arguments to pass to sanoid. See + + for allowed options. + ''; + }; + }; + + # Implementation + + config = mkIf cfg.enable { + services.sanoid.settings = mkMerge [ + (mapAttrs' (d: v: nameValuePair ("template_" + d) v) cfg.templates) + (mapAttrs (d: v: v) cfg.datasets) + ]; + + systemd.services.sanoid = { + description = "Sanoid snapshot service"; + serviceConfig = { + ExecStartPre = (map (buildAllowCommand "allow" [ "snapshot" "mount" "destroy" ]) datasets); + ExecStopPost = (map (buildAllowCommand "unallow" [ "snapshot" "mount" "destroy" ]) datasets); + ExecStart = lib.escapeShellArgs ([ + "${pkgs.sanoid}/bin/sanoid" + "--cron" + "--configdir" + (pkgs.writeTextDir "sanoid.conf" configFile) + ] ++ cfg.extraArgs); + User = "sanoid"; + Group = "sanoid"; + DynamicUser = true; + RuntimeDirectory = "sanoid"; + CacheDirectory = "sanoid"; + }; + # Prevents missing snapshots during DST changes + environment.TZ = "UTC"; + after = [ "zfs.target" ]; + startAt = cfg.interval; + }; + }; + + meta.maintainers = with maintainers; [ lopsided98 ]; +} From a9d29a1d0ddaad92dd0db5fa913ff714f0e37ebe Mon Sep 17 00:00:00 2001 From: Elis Hirwing Date: Mon, 26 Jul 2021 09:17:10 +0200 Subject: [PATCH 07/10] nixos/syncoid: Drop ~[at]sync from the systemcallfilter to avoid coredumps --- nixos/modules/services/backup/syncoid.nix | 1 - 1 file changed, 1 deletion(-) diff --git a/nixos/modules/services/backup/syncoid.nix b/nixos/modules/services/backup/syncoid.nix index 61a905a488a..73b01d4b53f 100644 --- a/nixos/modules/services/backup/syncoid.nix +++ b/nixos/modules/services/backup/syncoid.nix @@ -301,7 +301,6 @@ in "~@privileged" "~@resources" "~@setuid" - "~@sync" "~@timer" ]; SystemCallArchitectures = "native"; From bd263441e227e9fa3022abc518504c7b6086b413 Mon Sep 17 00:00:00 2001 From: Elis Hirwing Date: Mon, 26 Jul 2021 09:26:02 +0200 Subject: [PATCH 08/10] nixos/rl-notes/21.11: Add note about remaining syncoid permissions --- .../from_md/release-notes/rl-2111.section.xml | 13 +++++++++++++ nixos/doc/manual/release-notes/rl-2111.section.md | 2 ++ 2 files changed, 15 insertions(+) diff --git a/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml index f573f731365..9dd98a5262f 100644 --- a/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml +++ b/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml @@ -702,6 +702,19 @@ option. + + + The + services.syncoid.enable + module now properly drops ZFS permissions after usage. Before + it delegated permissions to whole pools instead of datasets + and didn’t clean up after execution. You can manually look + this up for your pools by running + zfs allow your-pool-name and use + zfs unallow syncoid your-pool-name to clean + this up. + + diff --git a/nixos/doc/manual/release-notes/rl-2111.section.md b/nixos/doc/manual/release-notes/rl-2111.section.md index 5e379ad6fd1..12e1a938433 100644 --- a/nixos/doc/manual/release-notes/rl-2111.section.md +++ b/nixos/doc/manual/release-notes/rl-2111.section.md @@ -183,3 +183,5 @@ pt-services.clipcat.enable). - NSS modules which should come after `dns` should use mkAfter. - The [networking.wireless.iwd](options.html#opt-networking.wireless.iwd.enable) module has a new [networking.wireless.iwd.settings](options.html#opt-networking.wireless.iwd.settings) option. + +- The [services.syncoid.enable](options.html#opt-services.syncoid.enable) module now properly drops ZFS permissions after usage. Before it delegated permissions to whole pools instead of datasets and didn't clean up after execution. You can manually look this up for your pools by running `zfs allow your-pool-name` and use `zfs unallow syncoid your-pool-name` to clean this up. From 764e4acee1e0e48383f8fc2648078e5b0828766a Mon Sep 17 00:00:00 2001 From: Elis Hirwing Date: Mon, 26 Jul 2021 10:00:14 +0200 Subject: [PATCH 09/10] nixos/tests/sanoid: Improve tests by checking that no permissions are left behind --- nixos/tests/sanoid.nix | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/nixos/tests/sanoid.nix b/nixos/tests/sanoid.nix index 7cef51e4201..3bdbe0a8d8d 100644 --- a/nixos/tests/sanoid.nix +++ b/nixos/tests/sanoid.nix @@ -85,10 +85,18 @@ in { "chown -R syncoid:syncoid /var/lib/syncoid/", ) + assert len(source.succeed("zfs allow pool")) == 0, "Pool shouldn't have delegated permissions set before snapshotting" + assert len(source.succeed("zfs allow pool/sanoid")) == 0, "Sanoid dataset shouldn't have delegated permissions set before snapshotting" + assert len(source.succeed("zfs allow pool/syncoid")) == 0, "Syncoid dataset shouldn't have delegated permissions set before snapshotting" + # Take snapshot with sanoid source.succeed("touch /mnt/pool/sanoid/test.txt") source.systemctl("start --wait sanoid.service") + 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") @@ -96,5 +104,9 @@ in { target.succeed("cat /mnt/pool/sanoid/test.txt") source.systemctl("start --wait syncoid-pool-syncoid.service") target.succeed("cat /mnt/pool/syncoid/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" ''; }) From bf36e52f64e1fef287b51d3ff07102bb35a00b8c Mon Sep 17 00:00:00 2001 From: Elis Hirwing Date: Mon, 26 Jul 2021 10:06:06 +0200 Subject: [PATCH 10/10] sanoid: Passthru sanoid tests --- pkgs/tools/backup/sanoid/default.nix | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkgs/tools/backup/sanoid/default.nix b/pkgs/tools/backup/sanoid/default.nix index 3a59dcc0640..5c61763f258 100644 --- a/pkgs/tools/backup/sanoid/default.nix +++ b/pkgs/tools/backup/sanoid/default.nix @@ -1,4 +1,4 @@ -{ lib, stdenv, fetchFromGitHub, makeWrapper, zfs +{ lib, stdenv, fetchFromGitHub, nixosTests, makeWrapper, zfs , perlPackages, procps, which, openssh, mbuffer, pv, lzop, gzip, pigz }: with lib; @@ -17,6 +17,8 @@ stdenv.mkDerivation rec { nativeBuildInputs = [ makeWrapper ]; buildInputs = with perlPackages; [ perl ConfigIniFiles CaptureTiny ]; + passthru.tests = nixosTests.sanoid; + installPhase = '' runHook preInstall