summary refs log tree commit diff
path: root/nixos/modules/services/backup
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules/services/backup')
-rw-r--r--nixos/modules/services/backup/sanoid.nix221
-rw-r--r--nixos/modules/services/backup/syncoid.nix473
-rw-r--r--nixos/modules/services/backup/znapzend.nix2
3 files changed, 405 insertions, 291 deletions
diff --git a/nixos/modules/services/backup/sanoid.nix b/nixos/modules/services/backup/sanoid.nix
index be44a43b6d3..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;
 
@@ -70,116 +70,127 @@ let
     processChildrenOnly = process_children_only;
   };
 
-  # Extract pool names from configured datasets
-  pools = unique (map (d: head (builtins.match "([^/]+).*" d)) (attrNames cfg.datasets));
-
-  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;
-
-in {
-
-    # Interface
-
-    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.
-
-          The format is described in
-          <citerefentry><refentrytitle>systemd.time</refentrytitle>
-          <manvolnum>7</manvolnum></citerefentry>.
-        '';
-      };
+  # 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
+        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;
+
+in
+{
+
+  # Interface
+
+  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.
+
+        The format is described in
+        <citerefentry><refentrytitle>systemd.time</refentrytitle>
+        <manvolnum>7</manvolnum></citerefentry>.
+      '';
+    };
 
-      datasets = mkOption {
-        type = types.attrsOf (types.submodule ({config, options, ...}: {
-          freeformType = datasetSettingsType;
-          options = commonOptions // datasetOptions;
-          config.use_template = mkAliasDefinitions (options.useTemplate or {});
-          config.process_children_only = mkAliasDefinitions (options.processChildrenOnly or {});
-        }));
-        default = {};
-        description = "Datasets to snapshot.";
-      };
+    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.";
-      };
+    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
-          <link xlink:href="https://github.com/jimsalterjrs/sanoid/blob/master/sanoid.defaults.conf"/>
-          for allowed values.
-        '';
-      };
+    settings = mkOption {
+      type = types.attrsOf datasetSettingsType;
+      description = ''
+        Free-form settings written directly to the config file. See
+        <link xlink:href="https://github.com/jimsalterjrs/sanoid/blob/master/sanoid.defaults.conf"/>
+        for allowed values.
+      '';
+    };
 
-      extraArgs = mkOption {
-        type = types.listOf types.str;
-        default = [];
-        example = [ "--verbose" "--readonly" "--debug" ];
-        description = ''
-          Extra arguments to pass to sanoid. See
-          <link xlink:href="https://github.com/jimsalterjrs/sanoid/#sanoid-command-line-options"/>
-          for allowed options.
-        '';
-      };
+    extraArgs = mkOption {
+      type = types.listOf types.str;
+      default = [ ];
+      example = [ "--verbose" "--readonly" "--debug" ];
+      description = ''
+        Extra arguments to pass to sanoid. See
+        <link xlink:href="https://github.com/jimsalterjrs/sanoid/#sanoid-command-line-options"/>
+        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 (pool: lib.escapeShellArgs [
-            "+/run/booted-system/sw/bin/zfs" "allow"
-            "sanoid" "snapshot,mount,destroy" pool
-          ]) pools;
-          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;
-          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;
+  # 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 ];
-  }
+  meta.maintainers = with maintainers; [ lopsided98 ];
+}
diff --git a/nixos/modules/services/backup/syncoid.nix b/nixos/modules/services/backup/syncoid.nix
index b764db1f14e..73b01d4b53f 100644
--- a/nixos/modules/services/backup/syncoid.nix
+++ b/nixos/modules/services/backup/syncoid.nix
@@ -5,212 +5,315 @@ with lib;
 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
-
-    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.
-
-          The format is described in
-          <citerefentry><refentrytitle>systemd.time</refentrytitle>
-          <manvolnum>7</manvolnum></citerefentry>.
-        '';
-      };
+  # Extract local dasaset names (so no datasets containing "@")
+  localDatasetName = d: optionals (d != null) (
+    let m = builtins.match "([^/@]+[^@]*)" d; in
+    optionals (m != null) m
+  );
 
-      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.
-        '';
-      };
+  # 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);
 
-      group = mkOption {
-        type = types.str;
-        default = "syncoid";
-        example = "backup";
-        description = "The group for the service.";
-      };
+  # 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
+{
 
-      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.
-        '';
-      };
+  # Interface
 
-      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
-          <link xlink:href="https://github.com/jimsalterjrs/sanoid/#syncoid-command-line-options"/>
-          for available options.
-        '';
-      };
+  options.services.syncoid = {
+    enable = mkEnableOption "Syncoid ZFS synchronization service";
 
-      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.
-              '';
-            };
+    interval = mkOption {
+      type = types.str;
+      default = "hourly";
+      example = "*-*-* *:15:00";
+      description = ''
+        Run syncoid at this interval. The default is to run hourly.
 
-            target = mkOption {
-              type = types.str;
-              example = "user@server:pool/dataset";
-              description = ''
-                Target ZFS dataset. Can be either local
-                (<replaceable>pool/dataset</replaceable>) or remote
-                (<replaceable>user@server:pool/dataset</replaceable>).
-              '';
-            };
+        The format is described in
+        <citerefentry><refentrytitle>systemd.time</refentrytitle>
+        <manvolnum>7</manvolnum></citerefentry>.
+      '';
+    };
 
-            recursive = mkOption {
-              type = types.bool;
-              default = false;
-              description = ''
-                Whether to also transfer child datasets.
-              '';
-            };
+    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.
+      '';
+    };
 
-            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>services.syncoid.sshKey</option> option.
-              '';
-            };
+    group = mkOption {
+      type = types.str;
+      default = "syncoid";
+      example = "backup";
+      description = "The group for the service.";
+    };
 
-            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.
-              '';
-            };
+    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.
+      '';
+    };
 
-            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.
-              '';
-            };
+    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
+        <link xlink:href="https://github.com/jimsalterjrs/sanoid/#syncoid-command-line-options"/>
+        for available options.
+      '';
+    };
 
-            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 common to all syncoid services.
+      '';
+    };
 
-            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
+              (<replaceable>pool/dataset</replaceable>) or remote
+              (<replaceable>user@server:pool/dataset</replaceable>).
+            '';
           };
-        }));
-        default = {};
-        example = literalExample ''
-          {
-            "pool/test".target = "root@target:pool/test";
-          }
-        '';
-        description = "Syncoid commands to run.";
-      };
-    };
 
-    # Implementation
+          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>services.syncoid.sshKey</option> 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.
+            '';
+          };
 
-    config = mkIf cfg.enable {
-      users =  {
-        users = mkIf (cfg.user == "syncoid") {
-          syncoid = {
-            group = cfg.group;
-            isSystemUser = true;
+          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.";
           };
         };
-        groups = mkIf (cfg.group == "syncoid") {
-          syncoid = {};
+        config = {
+          source = mkDefault name;
+          sshKey = mkDefault cfg.sshKey;
         };
-      };
+      }));
+      default = { };
+      example = literalExample ''
+        {
+          "pool/test".target = "root@target:pool/test";
+        }
+      '';
+      description = "Syncoid commands to run.";
+    };
+  };
 
-      systemd.services.syncoid = {
-        description = "Syncoid ZFS synchronization service";
-        script = concatMapStringsSep "\n" (c: lib.escapeShellArgs
-          ([ "${pkgs.sanoid}/bin/syncoid" ]
-            ++ (optionals c.useCommonArgs cfg.commonArgs)
-            ++ (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
-               ])) (attrValues cfg.commands);
-        after = [ "zfs.target" ];
-        serviceConfig = {
-          ExecStartPre = let
-            allowCmd = permissions: pool: lib.escapeShellArgs [
-              "+/run/booted-system/sw/bin/zfs" "allow"
-              cfg.user (concatStringsSep "," permissions) pool
-            ];
-          in
-            (map (allowCmd [ "hold" "send" "snapshot" "destroy" ]) (getPools "source")) ++
-            (map (allowCmd [ "create" "mount" "receive" "rollback" ]) (getPools "target"));
-          User = cfg.user;
-          Group = cfg.group;
+  # 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;
         };
-        startAt = cfg.interval;
+      };
+      groups = mkIf (cfg.group == "syncoid") {
+        syncoid = { };
       };
     };
 
-    meta.maintainers = with maintainers; [ lopsided98 ];
-  }
+    systemd.services = mapAttrs'
+      (name: c:
+        nameValuePair "syncoid-${escapeUnitName name}" (mkMerge [
+          {
+            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
+            path = [ "/run/booted-system/sw/bin/" ];
+            serviceConfig = {
+              ExecStartPre =
+                # 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"
+                ++ optionals (c.sshKey != null) [ "--sshkey" c.sshKey ]
+                ++ c.extraArgs
+                ++ [
+                "--sendoptions"
+                c.sendOptions
+                "--recvoptions"
+                c.recvOptions
+                "--no-privilege-elevation"
+                c.source
+                c.target
+              ]);
+              User = cfg.user;
+              Group = cfg.group;
+              StateDirectory = [ "syncoid" ];
+              StateDirectoryMode = "700";
+              # Prevent SSH control sockets of different syncoid services from interfering
+              PrivateTmp = true;
+              # Permissive access to /proc because syncoid
+              # calls ps(1) to detect ongoing `zfs receive`.
+              ProcSubset = "all";
+              ProtectProc = "default";
+
+              # The following options are only for optimizing:
+              # systemd-analyze security | grep syncoid-'*'
+              AmbientCapabilities = "";
+              CapabilityBoundingSet = "";
+              DeviceAllow = [ "/dev/zfs" ];
+              LockPersonality = true;
+              MemoryDenyWriteExecute = true;
+              NoNewPrivileges = true;
+              PrivateDevices = true;
+              PrivateMounts = true;
+              PrivateNetwork = mkDefault false;
+              PrivateUsers = true;
+              ProtectClock = true;
+              ProtectControlGroups = true;
+              ProtectHome = true;
+              ProtectHostname = true;
+              ProtectKernelLogs = true;
+              ProtectKernelModules = true;
+              ProtectKernelTunables = true;
+              ProtectSystem = "strict";
+              RemoveIPC = true;
+              RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+              RestrictNamespaces = true;
+              RestrictRealtime = true;
+              RestrictSUIDSGID = true;
+              RootDirectory = "/run/syncoid/${escapeUnitName name}";
+              RootDirectoryStartOnly = true;
+              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}" ];
+              MountAPIVFS = true;
+              # Create RootDirectory= in the host's mount namespace.
+              RuntimeDirectory = [ "syncoid/${escapeUnitName name}" ];
+              RuntimeDirectoryMode = "700";
+              SystemCallFilter = [
+                "@system-service"
+                # Groups in @system-service which do not contain a syscall listed by:
+                # 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"
+                "~@timer"
+              ];
+              SystemCallArchitectures = "native";
+              # This is for BindPaths= and BindReadOnlyPaths=
+              # to allow traversal of directories they create in RootDirectory=.
+              UMask = "0066";
+            };
+          }
+          cfg.service
+          c.service
+        ]))
+      cfg.commands;
+  };
+
+  meta.maintainers = with maintainers; [ julm lopsided98 ];
+}
diff --git a/nixos/modules/services/backup/znapzend.nix b/nixos/modules/services/backup/znapzend.nix
index 0ca71b413ce..debb2a39705 100644
--- a/nixos/modules/services/backup/znapzend.nix
+++ b/nixos/modules/services/backup/znapzend.nix
@@ -279,7 +279,7 @@ let
     src_plan = plan;
     tsformat = timestampFormat;
     zend_delay = toString sendDelay;
-  } // fold (a: b: a // b) {} (
+  } // foldr (a: b: a // b) {} (
     map mkDestAttrs (builtins.attrValues destinations)
   );