lol

Merge pull request #131118 from etu/sanoid-syncoid-improvements

nixos/{syncoid,sanoid}: Improve ZFS permission delegation

authored by

Elis Hirwing and committed by
GitHub
699ea654 1f615944

+347 -283
+13
nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
··· 702 702 option. 703 703 </para> 704 704 </listitem> 705 + <listitem> 706 + <para> 707 + The 708 + <link xlink:href="options.html#opt-services.syncoid.enable">services.syncoid.enable</link> 709 + module now properly drops ZFS permissions after usage. Before 710 + it delegated permissions to whole pools instead of datasets 711 + and didn’t clean up after execution. You can manually look 712 + this up for your pools by running 713 + <literal>zfs allow your-pool-name</literal> and use 714 + <literal>zfs unallow syncoid your-pool-name</literal> to clean 715 + this up. 716 + </para> 717 + </listitem> 705 718 </itemizedlist> 706 719 </section> 707 720 </section>
+2
nixos/doc/manual/release-notes/rl-2111.section.md
··· 183 183 - NSS modules which should come after `dns` should use mkAfter. 184 184 185 185 - 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. 186 + 187 + - 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.
+107 -96
nixos/modules/services/backup/sanoid.nix
··· 52 52 use_template = mkOption { 53 53 description = "Names of the templates to use for this dataset."; 54 54 type = types.listOf (types.enum (attrNames cfg.templates)); 55 - default = []; 55 + default = [ ]; 56 56 }; 57 57 useTemplate = use_template; 58 58 ··· 70 70 processChildrenOnly = process_children_only; 71 71 }; 72 72 73 - # Extract pool names from configured datasets 74 - pools = unique (map (d: head (builtins.match "([^/]+).*" d)) (attrNames cfg.datasets)); 73 + # Extract unique dataset names 74 + datasets = unique (attrNames cfg.datasets); 75 75 76 - configFile = let 77 - mkValueString = v: 78 - if builtins.isList v then concatStringsSep "," v 79 - else generators.mkValueStringDefault {} v; 76 + # Function to build "zfs allow" and "zfs unallow" commands for the 77 + # filesystems we've delegated permissions to. 78 + buildAllowCommand = zfsAction: permissions: dataset: lib.escapeShellArgs [ 79 + # Here we explicitly use the booted system to guarantee the stable API needed by ZFS 80 + "-+/run/booted-system/sw/bin/zfs" 81 + zfsAction 82 + "sanoid" 83 + (concatStringsSep "," permissions) 84 + dataset 85 + ]; 80 86 81 - mkKeyValue = k: v: if v == null then "" 82 - else if k == "processChildrenOnly" then "" 83 - else if k == "useTemplate" then "" 84 - else generators.mkKeyValueDefault { inherit mkValueString; } "=" k v; 85 - in generators.toINI { inherit mkKeyValue; } cfg.settings; 87 + configFile = 88 + let 89 + mkValueString = v: 90 + if builtins.isList v then concatStringsSep "," v 91 + else generators.mkValueStringDefault { } v; 86 92 87 - in { 93 + mkKeyValue = k: v: 94 + if v == null then "" 95 + else if k == "processChildrenOnly" then "" 96 + else if k == "useTemplate" then "" 97 + else generators.mkKeyValueDefault { inherit mkValueString; } "=" k v; 98 + in 99 + generators.toINI { inherit mkKeyValue; } cfg.settings; 88 100 89 - # Interface 101 + in 102 + { 90 103 91 - options.services.sanoid = { 92 - enable = mkEnableOption "Sanoid ZFS snapshotting service"; 104 + # Interface 93 105 94 - interval = mkOption { 95 - type = types.str; 96 - default = "hourly"; 97 - example = "daily"; 98 - description = '' 99 - Run sanoid at this interval. The default is to run hourly. 106 + options.services.sanoid = { 107 + enable = mkEnableOption "Sanoid ZFS snapshotting service"; 100 108 101 - The format is described in 102 - <citerefentry><refentrytitle>systemd.time</refentrytitle> 103 - <manvolnum>7</manvolnum></citerefentry>. 104 - ''; 105 - }; 109 + interval = mkOption { 110 + type = types.str; 111 + default = "hourly"; 112 + example = "daily"; 113 + description = '' 114 + Run sanoid at this interval. The default is to run hourly. 106 115 107 - datasets = mkOption { 108 - type = types.attrsOf (types.submodule ({config, options, ...}: { 109 - freeformType = datasetSettingsType; 110 - options = commonOptions // datasetOptions; 111 - config.use_template = mkAliasDefinitions (mkDefault options.useTemplate or {}); 112 - config.process_children_only = mkAliasDefinitions (mkDefault options.processChildrenOnly or {}); 113 - })); 114 - default = {}; 115 - description = "Datasets to snapshot."; 116 - }; 116 + The format is described in 117 + <citerefentry><refentrytitle>systemd.time</refentrytitle> 118 + <manvolnum>7</manvolnum></citerefentry>. 119 + ''; 120 + }; 121 + 122 + datasets = mkOption { 123 + type = types.attrsOf (types.submodule ({ config, options, ... }: { 124 + freeformType = datasetSettingsType; 125 + options = commonOptions // datasetOptions; 126 + config.use_template = mkAliasDefinitions (mkDefault options.useTemplate or { }); 127 + config.process_children_only = mkAliasDefinitions (mkDefault options.processChildrenOnly or { }); 128 + })); 129 + default = { }; 130 + description = "Datasets to snapshot."; 131 + }; 117 132 118 - templates = mkOption { 119 - type = types.attrsOf (types.submodule { 120 - freeformType = datasetSettingsType; 121 - options = commonOptions; 122 - }); 123 - default = {}; 124 - description = "Templates for datasets."; 125 - }; 133 + templates = mkOption { 134 + type = types.attrsOf (types.submodule { 135 + freeformType = datasetSettingsType; 136 + options = commonOptions; 137 + }); 138 + default = { }; 139 + description = "Templates for datasets."; 140 + }; 126 141 127 - settings = mkOption { 128 - type = types.attrsOf datasetSettingsType; 129 - description = '' 130 - Free-form settings written directly to the config file. See 131 - <link xlink:href="https://github.com/jimsalterjrs/sanoid/blob/master/sanoid.defaults.conf"/> 132 - for allowed values. 133 - ''; 134 - }; 142 + settings = mkOption { 143 + type = types.attrsOf datasetSettingsType; 144 + description = '' 145 + Free-form settings written directly to the config file. See 146 + <link xlink:href="https://github.com/jimsalterjrs/sanoid/blob/master/sanoid.defaults.conf"/> 147 + for allowed values. 148 + ''; 149 + }; 135 150 136 - extraArgs = mkOption { 137 - type = types.listOf types.str; 138 - default = []; 139 - example = [ "--verbose" "--readonly" "--debug" ]; 140 - description = '' 141 - Extra arguments to pass to sanoid. See 142 - <link xlink:href="https://github.com/jimsalterjrs/sanoid/#sanoid-command-line-options"/> 143 - for allowed options. 144 - ''; 145 - }; 151 + extraArgs = mkOption { 152 + type = types.listOf types.str; 153 + default = [ ]; 154 + example = [ "--verbose" "--readonly" "--debug" ]; 155 + description = '' 156 + Extra arguments to pass to sanoid. See 157 + <link xlink:href="https://github.com/jimsalterjrs/sanoid/#sanoid-command-line-options"/> 158 + for allowed options. 159 + ''; 146 160 }; 161 + }; 147 162 148 - # Implementation 163 + # Implementation 149 164 150 - config = mkIf cfg.enable { 151 - services.sanoid.settings = mkMerge [ 152 - (mapAttrs' (d: v: nameValuePair ("template_" + d) v) cfg.templates) 153 - (mapAttrs (d: v: v) cfg.datasets) 154 - ]; 165 + config = mkIf cfg.enable { 166 + services.sanoid.settings = mkMerge [ 167 + (mapAttrs' (d: v: nameValuePair ("template_" + d) v) cfg.templates) 168 + (mapAttrs (d: v: v) cfg.datasets) 169 + ]; 155 170 156 - systemd.services.sanoid = { 157 - description = "Sanoid snapshot service"; 158 - serviceConfig = { 159 - ExecStartPre = map (pool: lib.escapeShellArgs [ 160 - "+/run/booted-system/sw/bin/zfs" "allow" 161 - "sanoid" "snapshot,mount,destroy" pool 162 - ]) pools; 163 - ExecStart = lib.escapeShellArgs ([ 164 - "${pkgs.sanoid}/bin/sanoid" 165 - "--cron" 166 - "--configdir" (pkgs.writeTextDir "sanoid.conf" configFile) 167 - ] ++ cfg.extraArgs); 168 - ExecStopPost = map (pool: lib.escapeShellArgs [ 169 - "+/run/booted-system/sw/bin/zfs" "unallow" "sanoid" pool 170 - ]) pools; 171 - User = "sanoid"; 172 - Group = "sanoid"; 173 - DynamicUser = true; 174 - RuntimeDirectory = "sanoid"; 175 - CacheDirectory = "sanoid"; 176 - }; 177 - # Prevents missing snapshots during DST changes 178 - environment.TZ = "UTC"; 179 - after = [ "zfs.target" ]; 180 - startAt = cfg.interval; 171 + systemd.services.sanoid = { 172 + description = "Sanoid snapshot service"; 173 + serviceConfig = { 174 + ExecStartPre = (map (buildAllowCommand "allow" [ "snapshot" "mount" "destroy" ]) datasets); 175 + ExecStopPost = (map (buildAllowCommand "unallow" [ "snapshot" "mount" "destroy" ]) datasets); 176 + ExecStart = lib.escapeShellArgs ([ 177 + "${pkgs.sanoid}/bin/sanoid" 178 + "--cron" 179 + "--configdir" 180 + (pkgs.writeTextDir "sanoid.conf" configFile) 181 + ] ++ cfg.extraArgs); 182 + User = "sanoid"; 183 + Group = "sanoid"; 184 + DynamicUser = true; 185 + RuntimeDirectory = "sanoid"; 186 + CacheDirectory = "sanoid"; 181 187 }; 188 + # Prevents missing snapshots during DST changes 189 + environment.TZ = "UTC"; 190 + after = [ "zfs.target" ]; 191 + startAt = cfg.interval; 182 192 }; 193 + }; 183 194 184 - meta.maintainers = with maintainers; [ lopsided98 ]; 185 - } 195 + meta.maintainers = with maintainers; [ lopsided98 ]; 196 + }
+210 -186
nixos/modules/services/backup/syncoid.nix
··· 5 5 let 6 6 cfg = config.services.syncoid; 7 7 8 - # Extract the pool name of a local dataset (any dataset not containing "@") 9 - localPoolName = d: optionals (d != null) ( 10 - let m = builtins.match "([^/@]+)[^@]*" d; in 11 - optionals (m != null) m); 8 + # Extract local dasaset names (so no datasets containing "@") 9 + localDatasetName = d: optionals (d != null) ( 10 + let m = builtins.match "([^/@]+[^@]*)" d; in 11 + optionals (m != null) m 12 + ); 12 13 13 14 # Escape as required by: https://www.freedesktop.org/software/systemd/man/systemd.unit.html 14 15 escapeUnitName = name: 15 16 lib.concatMapStrings (s: if lib.isList s then "-" else s) 16 - (builtins.split "[^a-zA-Z0-9_.\\-]+" name); 17 - in { 17 + (builtins.split "[^a-zA-Z0-9_.\\-]+" name); 18 18 19 - # Interface 19 + # Function to build "zfs allow" and "zfs unallow" commands for the 20 + # filesystems we've delegated permissions to. 21 + buildAllowCommand = zfsAction: permissions: dataset: lib.escapeShellArgs [ 22 + # Here we explicitly use the booted system to guarantee the stable API needed by ZFS 23 + "-+/run/booted-system/sw/bin/zfs" 24 + zfsAction 25 + cfg.user 26 + (concatStringsSep "," permissions) 27 + dataset 28 + ]; 29 + in 30 + { 20 31 21 - options.services.syncoid = { 22 - enable = mkEnableOption "Syncoid ZFS synchronization service"; 32 + # Interface 23 33 24 - interval = mkOption { 25 - type = types.str; 26 - default = "hourly"; 27 - example = "*-*-* *:15:00"; 28 - description = '' 29 - Run syncoid at this interval. The default is to run hourly. 34 + options.services.syncoid = { 35 + enable = mkEnableOption "Syncoid ZFS synchronization service"; 30 36 31 - The format is described in 32 - <citerefentry><refentrytitle>systemd.time</refentrytitle> 33 - <manvolnum>7</manvolnum></citerefentry>. 34 - ''; 35 - }; 37 + interval = mkOption { 38 + type = types.str; 39 + default = "hourly"; 40 + example = "*-*-* *:15:00"; 41 + description = '' 42 + Run syncoid at this interval. The default is to run hourly. 36 43 37 - user = mkOption { 38 - type = types.str; 39 - default = "syncoid"; 40 - example = "backup"; 41 - description = '' 42 - The user for the service. ZFS privilege delegation will be 43 - automatically configured for any local pools used by syncoid if this 44 - option is set to a user other than root. The user will be given the 45 - "hold" and "send" privileges on any pool that has datasets being sent 46 - and the "create", "mount", "receive", and "rollback" privileges on 47 - any pool that has datasets being received. 48 - ''; 49 - }; 44 + The format is described in 45 + <citerefentry><refentrytitle>systemd.time</refentrytitle> 46 + <manvolnum>7</manvolnum></citerefentry>. 47 + ''; 48 + }; 50 49 51 - group = mkOption { 52 - type = types.str; 53 - default = "syncoid"; 54 - example = "backup"; 55 - description = "The group for the service."; 56 - }; 50 + user = mkOption { 51 + type = types.str; 52 + default = "syncoid"; 53 + example = "backup"; 54 + description = '' 55 + The user for the service. ZFS privilege delegation will be 56 + automatically configured for any local pools used by syncoid if this 57 + option is set to a user other than root. The user will be given the 58 + "hold" and "send" privileges on any pool that has datasets being sent 59 + and the "create", "mount", "receive", and "rollback" privileges on 60 + any pool that has datasets being received. 61 + ''; 62 + }; 57 63 58 - sshKey = mkOption { 59 - type = types.nullOr types.path; 60 - # Prevent key from being copied to store 61 - apply = mapNullable toString; 62 - default = null; 63 - description = '' 64 - SSH private key file to use to login to the remote system. Can be 65 - overridden in individual commands. 66 - ''; 67 - }; 64 + group = mkOption { 65 + type = types.str; 66 + default = "syncoid"; 67 + example = "backup"; 68 + description = "The group for the service."; 69 + }; 68 70 69 - commonArgs = mkOption { 70 - type = types.listOf types.str; 71 - default = []; 72 - example = [ "--no-sync-snap" ]; 73 - description = '' 74 - Arguments to add to every syncoid command, unless disabled for that 75 - command. See 76 - <link xlink:href="https://github.com/jimsalterjrs/sanoid/#syncoid-command-line-options"/> 77 - for available options. 78 - ''; 79 - }; 71 + sshKey = mkOption { 72 + type = types.nullOr types.path; 73 + # Prevent key from being copied to store 74 + apply = mapNullable toString; 75 + default = null; 76 + description = '' 77 + SSH private key file to use to login to the remote system. Can be 78 + overridden in individual commands. 79 + ''; 80 + }; 80 81 81 - service = mkOption { 82 - type = types.attrs; 83 - default = {}; 84 - description = '' 85 - Systemd configuration common to all syncoid services. 86 - ''; 87 - }; 82 + commonArgs = mkOption { 83 + type = types.listOf types.str; 84 + default = [ ]; 85 + example = [ "--no-sync-snap" ]; 86 + description = '' 87 + Arguments to add to every syncoid command, unless disabled for that 88 + command. See 89 + <link xlink:href="https://github.com/jimsalterjrs/sanoid/#syncoid-command-line-options"/> 90 + for available options. 91 + ''; 92 + }; 88 93 89 - commands = mkOption { 90 - type = types.attrsOf (types.submodule ({ name, ... }: { 91 - options = { 92 - source = mkOption { 93 - type = types.str; 94 - example = "pool/dataset"; 95 - description = '' 96 - Source ZFS dataset. Can be either local or remote. Defaults to 97 - the attribute name. 98 - ''; 99 - }; 94 + service = mkOption { 95 + type = types.attrs; 96 + default = { }; 97 + description = '' 98 + Systemd configuration common to all syncoid services. 99 + ''; 100 + }; 100 101 101 - target = mkOption { 102 - type = types.str; 103 - example = "user@server:pool/dataset"; 104 - description = '' 105 - Target ZFS dataset. Can be either local 106 - (<replaceable>pool/dataset</replaceable>) or remote 107 - (<replaceable>user@server:pool/dataset</replaceable>). 108 - ''; 109 - }; 102 + commands = mkOption { 103 + type = types.attrsOf (types.submodule ({ name, ... }: { 104 + options = { 105 + source = mkOption { 106 + type = types.str; 107 + example = "pool/dataset"; 108 + description = '' 109 + Source ZFS dataset. Can be either local or remote. Defaults to 110 + the attribute name. 111 + ''; 112 + }; 110 113 111 - recursive = mkEnableOption ''the transfer of child datasets''; 114 + target = mkOption { 115 + type = types.str; 116 + example = "user@server:pool/dataset"; 117 + description = '' 118 + Target ZFS dataset. Can be either local 119 + (<replaceable>pool/dataset</replaceable>) or remote 120 + (<replaceable>user@server:pool/dataset</replaceable>). 121 + ''; 122 + }; 112 123 113 - sshKey = mkOption { 114 - type = types.nullOr types.path; 115 - # Prevent key from being copied to store 116 - apply = mapNullable toString; 117 - description = '' 118 - SSH private key file to use to login to the remote system. 119 - Defaults to <option>services.syncoid.sshKey</option> option. 120 - ''; 121 - }; 124 + recursive = mkEnableOption ''the transfer of child datasets''; 122 125 123 - sendOptions = mkOption { 124 - type = types.separatedString " "; 125 - default = ""; 126 - example = "Lc e"; 127 - description = '' 128 - Advanced options to pass to zfs send. Options are specified 129 - without their leading dashes and separated by spaces. 130 - ''; 131 - }; 126 + sshKey = mkOption { 127 + type = types.nullOr types.path; 128 + # Prevent key from being copied to store 129 + apply = mapNullable toString; 130 + description = '' 131 + SSH private key file to use to login to the remote system. 132 + Defaults to <option>services.syncoid.sshKey</option> option. 133 + ''; 134 + }; 132 135 133 - recvOptions = mkOption { 134 - type = types.separatedString " "; 135 - default = ""; 136 - example = "ux recordsize o compression=lz4"; 137 - description = '' 138 - Advanced options to pass to zfs recv. Options are specified 139 - without their leading dashes and separated by spaces. 140 - ''; 141 - }; 136 + sendOptions = mkOption { 137 + type = types.separatedString " "; 138 + default = ""; 139 + example = "Lc e"; 140 + description = '' 141 + Advanced options to pass to zfs send. Options are specified 142 + without their leading dashes and separated by spaces. 143 + ''; 144 + }; 142 145 143 - useCommonArgs = mkOption { 144 - type = types.bool; 145 - default = true; 146 - description = '' 147 - Whether to add the configured common arguments to this command. 148 - ''; 149 - }; 146 + recvOptions = mkOption { 147 + type = types.separatedString " "; 148 + default = ""; 149 + example = "ux recordsize o compression=lz4"; 150 + description = '' 151 + Advanced options to pass to zfs recv. Options are specified 152 + without their leading dashes and separated by spaces. 153 + ''; 154 + }; 150 155 151 - service = mkOption { 152 - type = types.attrs; 153 - default = {}; 154 - description = '' 155 - Systemd configuration specific to this syncoid service. 156 - ''; 157 - }; 156 + useCommonArgs = mkOption { 157 + type = types.bool; 158 + default = true; 159 + description = '' 160 + Whether to add the configured common arguments to this command. 161 + ''; 162 + }; 158 163 159 - extraArgs = mkOption { 160 - type = types.listOf types.str; 161 - default = []; 162 - example = [ "--sshport 2222" ]; 163 - description = "Extra syncoid arguments for this command."; 164 - }; 164 + service = mkOption { 165 + type = types.attrs; 166 + default = { }; 167 + description = '' 168 + Systemd configuration specific to this syncoid service. 169 + ''; 165 170 }; 166 - config = { 167 - source = mkDefault name; 168 - sshKey = mkDefault cfg.sshKey; 171 + 172 + extraArgs = mkOption { 173 + type = types.listOf types.str; 174 + default = [ ]; 175 + example = [ "--sshport 2222" ]; 176 + description = "Extra syncoid arguments for this command."; 169 177 }; 170 - })); 171 - default = {}; 172 - example = literalExample '' 173 - { 174 - "pool/test".target = "root@target:pool/test"; 175 - } 176 - ''; 177 - description = "Syncoid commands to run."; 178 - }; 178 + }; 179 + config = { 180 + source = mkDefault name; 181 + sshKey = mkDefault cfg.sshKey; 182 + }; 183 + })); 184 + default = { }; 185 + example = literalExample '' 186 + { 187 + "pool/test".target = "root@target:pool/test"; 188 + } 189 + ''; 190 + description = "Syncoid commands to run."; 179 191 }; 192 + }; 180 193 181 - # Implementation 194 + # Implementation 182 195 183 - config = mkIf cfg.enable { 184 - users = { 185 - users = mkIf (cfg.user == "syncoid") { 186 - syncoid = { 187 - group = cfg.group; 188 - isSystemUser = true; 189 - # For syncoid to be able to create /var/lib/syncoid/.ssh/ 190 - # and to use custom ssh_config or known_hosts. 191 - home = "/var/lib/syncoid"; 192 - createHome = false; 193 - }; 194 - }; 195 - groups = mkIf (cfg.group == "syncoid") { 196 - syncoid = {}; 196 + config = mkIf cfg.enable { 197 + users = { 198 + users = mkIf (cfg.user == "syncoid") { 199 + syncoid = { 200 + group = cfg.group; 201 + isSystemUser = true; 202 + # For syncoid to be able to create /var/lib/syncoid/.ssh/ 203 + # and to use custom ssh_config or known_hosts. 204 + home = "/var/lib/syncoid"; 205 + createHome = false; 197 206 }; 198 207 }; 208 + groups = mkIf (cfg.group == "syncoid") { 209 + syncoid = { }; 210 + }; 211 + }; 199 212 200 - systemd.services = mapAttrs' (name: c: 213 + systemd.services = mapAttrs' 214 + (name: c: 201 215 nameValuePair "syncoid-${escapeUnitName name}" (mkMerge [ 202 - { description = "Syncoid ZFS synchronization from ${c.source} to ${c.target}"; 216 + { 217 + description = "Syncoid ZFS synchronization from ${c.source} to ${c.target}"; 203 218 after = [ "zfs.target" ]; 204 219 startAt = cfg.interval; 205 220 # syncoid may need zpool to get feature@extensible_dataset 206 221 path = [ "/run/booted-system/sw/bin/" ]; 207 222 serviceConfig = { 208 223 ExecStartPre = 209 - map (pool: lib.escapeShellArgs [ 210 - "+/run/booted-system/sw/bin/zfs" "allow" 211 - cfg.user "bookmark,hold,send,snapshot,destroy" pool 212 - # Permissions snapshot and destroy are in case --no-sync-snap is not used 213 - ]) (localPoolName c.source) ++ 214 - map (pool: lib.escapeShellArgs [ 215 - "+/run/booted-system/sw/bin/zfs" "allow" 216 - cfg.user "create,mount,receive,rollback" pool 217 - ]) (localPoolName c.target); 224 + # Permissions snapshot and destroy are in case --no-sync-snap is not used 225 + (map (buildAllowCommand "allow" [ "bookmark" "hold" "send" "snapshot" "destroy" ]) (localDatasetName c.source)) ++ 226 + (map (buildAllowCommand "allow" [ "create" "mount" "receive" "rollback" ]) (localDatasetName c.target)); 227 + ExecStopPost = 228 + # Permissions snapshot and destroy are in case --no-sync-snap is not used 229 + (map (buildAllowCommand "unallow" [ "bookmark" "hold" "send" "snapshot" "destroy" ]) (localDatasetName c.source)) ++ 230 + (map (buildAllowCommand "unallow" [ "create" "mount" "receive" "rollback" ]) (localDatasetName c.target)); 218 231 ExecStart = lib.escapeShellArgs ([ "${pkgs.sanoid}/bin/syncoid" ] 219 232 ++ optionals c.useCommonArgs cfg.commonArgs 220 233 ++ optional c.recursive "-r" 221 234 ++ optionals (c.sshKey != null) [ "--sshkey" c.sshKey ] 222 235 ++ c.extraArgs 223 - ++ [ "--sendoptions" c.sendOptions 224 - "--recvoptions" c.recvOptions 225 - "--no-privilege-elevation" 226 - c.source c.target 227 - ]); 236 + ++ [ 237 + "--sendoptions" 238 + c.sendOptions 239 + "--recvoptions" 240 + c.recvOptions 241 + "--no-privilege-elevation" 242 + c.source 243 + c.target 244 + ]); 228 245 User = cfg.user; 229 246 Group = cfg.group; 230 247 StateDirectory = [ "syncoid" ]; ··· 240 257 # systemd-analyze security | grep syncoid-'*' 241 258 AmbientCapabilities = ""; 242 259 CapabilityBoundingSet = ""; 243 - DeviceAllow = ["/dev/zfs"]; 260 + DeviceAllow = [ "/dev/zfs" ]; 244 261 LockPersonality = true; 245 262 MemoryDenyWriteExecute = true; 246 263 NoNewPrivileges = true; ··· 266 283 BindPaths = [ "/dev/zfs" ]; 267 284 BindReadOnlyPaths = [ builtins.storeDir "/etc" "/run" "/bin/sh" ]; 268 285 # Avoid useless mounting of RootDirectory= in the own RootDirectory= of ExecStart='s mount namespace. 269 - InaccessiblePaths = ["-+/run/syncoid/${escapeUnitName name}"]; 286 + InaccessiblePaths = [ "-+/run/syncoid/${escapeUnitName name}" ]; 270 287 MountAPIVFS = true; 271 288 # Create RootDirectory= in the host's mount namespace. 272 289 RuntimeDirectory = [ "syncoid/${escapeUnitName name}" ]; ··· 277 294 # perf stat -x, 2>perf.log -e 'syscalls:sys_enter_*' syncoid … 278 295 # awk >perf.syscalls -F "," '$1 > 0 {sub("syscalls:sys_enter_","",$3); print $3}' perf.log 279 296 # 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 ' ' 280 - "~@aio" "~@chown" "~@keyring" "~@memlock" "~@privileged" 281 - "~@resources" "~@setuid" "~@sync" "~@timer" 297 + "~@aio" 298 + "~@chown" 299 + "~@keyring" 300 + "~@memlock" 301 + "~@privileged" 302 + "~@resources" 303 + "~@setuid" 304 + "~@timer" 282 305 ]; 283 306 SystemCallArchitectures = "native"; 284 307 # This is for BindPaths= and BindReadOnlyPaths= ··· 288 311 } 289 312 cfg.service 290 313 c.service 291 - ])) cfg.commands; 292 - }; 314 + ])) 315 + cfg.commands; 316 + }; 293 317 294 - meta.maintainers = with maintainers; [ julm lopsided98 ]; 295 - } 318 + meta.maintainers = with maintainers; [ julm lopsided98 ]; 319 + }
+12
nixos/tests/sanoid.nix
··· 85 85 "chown -R syncoid:syncoid /var/lib/syncoid/", 86 86 ) 87 87 88 + assert len(source.succeed("zfs allow pool")) == 0, "Pool shouldn't have delegated permissions set before snapshotting" 89 + assert len(source.succeed("zfs allow pool/sanoid")) == 0, "Sanoid dataset shouldn't have delegated permissions set before snapshotting" 90 + assert len(source.succeed("zfs allow pool/syncoid")) == 0, "Syncoid dataset shouldn't have delegated permissions set before snapshotting" 91 + 88 92 # Take snapshot with sanoid 89 93 source.succeed("touch /mnt/pool/sanoid/test.txt") 90 94 source.systemctl("start --wait sanoid.service") 91 95 96 + assert len(source.succeed("zfs allow pool")) == 0, "Pool shouldn't have delegated permissions set after snapshotting" 97 + assert len(source.succeed("zfs allow pool/sanoid")) == 0, "Sanoid dataset shouldn't have delegated permissions set after snapshotting" 98 + assert len(source.succeed("zfs allow pool/syncoid")) == 0, "Syncoid dataset shouldn't have delegated permissions set after snapshotting" 99 + 92 100 # Sync snapshots 93 101 target.wait_for_open_port(22) 94 102 source.succeed("touch /mnt/pool/syncoid/test.txt") ··· 96 104 target.succeed("cat /mnt/pool/sanoid/test.txt") 97 105 source.systemctl("start --wait syncoid-pool-syncoid.service") 98 106 target.succeed("cat /mnt/pool/syncoid/test.txt") 107 + 108 + assert len(source.succeed("zfs allow pool")) == 0, "Pool shouldn't have delegated permissions set after syncing snapshots" 109 + assert len(source.succeed("zfs allow pool/sanoid")) == 0, "Sanoid dataset shouldn't have delegated permissions set after syncing snapshots" 110 + assert len(source.succeed("zfs allow pool/syncoid")) == 0, "Syncoid dataset shouldn't have delegated permissions set after syncing snapshots" 99 111 ''; 100 112 })
+3 -1
pkgs/tools/backup/sanoid/default.nix
··· 1 - { lib, stdenv, fetchFromGitHub, makeWrapper, zfs 1 + { lib, stdenv, fetchFromGitHub, nixosTests, makeWrapper, zfs 2 2 , perlPackages, procps, which, openssh, mbuffer, pv, lzop, gzip, pigz }: 3 3 4 4 with lib; ··· 16 16 17 17 nativeBuildInputs = [ makeWrapper ]; 18 18 buildInputs = with perlPackages; [ perl ConfigIniFiles CaptureTiny ]; 19 + 20 + passthru.tests = nixosTests.sanoid; 19 21 20 22 installPhase = '' 21 23 runHook preInstall