| 
									
										
										
										
											2018-04-21 12:12:43 +02:00
										 |  |  | { config, lib, pkgs, ... }: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | with lib; | 
					
						
							| 
									
										
										
										
											2019-01-23 21:29:02 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | let | 
					
						
							|  |  |  |   # Type for a valid systemd unit option. Needed for correctly passing "timerConfig" to "systemd.timers" | 
					
						
							|  |  |  |   unitOption = (import ../../system/boot/systemd-unit-options.nix { inherit config lib; }).unitOption; | 
					
						
							|  |  |  | in | 
					
						
							| 
									
										
										
										
											2018-04-21 12:12:43 +02:00
										 |  |  | { | 
					
						
							|  |  |  |   options.services.restic.backups = mkOption { | 
					
						
							|  |  |  |     description = ''
 | 
					
						
							|  |  |  |       Periodic backups to create with Restic. | 
					
						
							|  |  |  |     '';
 | 
					
						
							| 
									
										
										
										
											2018-07-20 20:56:59 +00:00
										 |  |  |     type = types.attrsOf (types.submodule ({ name, ... }: { | 
					
						
							| 
									
										
										
										
											2018-04-21 12:12:43 +02:00
										 |  |  |       options = { | 
					
						
							|  |  |  |         passwordFile = mkOption { | 
					
						
							|  |  |  |           type = types.str; | 
					
						
							|  |  |  |           description = ''
 | 
					
						
							|  |  |  |             Read the repository password from a file. | 
					
						
							|  |  |  |           '';
 | 
					
						
							|  |  |  |           example = "/etc/nixos/restic-password"; | 
					
						
							| 
									
										
										
										
											2018-05-30 22:30:12 -04:00
										 |  |  |         }; | 
					
						
							| 
									
										
										
										
											2018-04-21 12:12:43 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-05-30 22:30:12 -04:00
										 |  |  |         s3CredentialsFile = mkOption { | 
					
						
							|  |  |  |           type = with types; nullOr str; | 
					
						
							| 
									
										
										
										
											2018-08-01 22:52:41 -04:00
										 |  |  |           default = null; | 
					
						
							| 
									
										
										
										
											2018-05-30 22:30:12 -04:00
										 |  |  |           description = ''
 | 
					
						
							|  |  |  |             file containing the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY | 
					
						
							|  |  |  |             for an S3-hosted repository, in the format of an EnvironmentFile | 
					
						
							|  |  |  |             as described by systemd.exec(5) | 
					
						
							|  |  |  |           '';
 | 
					
						
							| 
									
										
										
										
											2018-04-21 12:12:43 +02:00
										 |  |  |         }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-05 16:25:33 -04:00
										 |  |  |         rcloneOptions = mkOption { | 
					
						
							|  |  |  |           type = with types; nullOr (attrsOf (oneOf [ str bool ])); | 
					
						
							|  |  |  |           default = null; | 
					
						
							|  |  |  |           description = ''
 | 
					
						
							|  |  |  |             Options to pass to rclone to control its behavior. | 
					
						
							|  |  |  |             See <link xlink:href="https://rclone.org/docs/#options"/> for | 
					
						
							|  |  |  |             available options. When specifying option names, strip the | 
					
						
							|  |  |  |             leading <literal>--</literal>. To set a flag such as | 
					
						
							|  |  |  |             <literal>--drive-use-trash</literal>, which does not take a value, | 
					
						
							|  |  |  |             set the value to the Boolean <literal>true</literal>. | 
					
						
							|  |  |  |           '';
 | 
					
						
							|  |  |  |           example = { | 
					
						
							|  |  |  |             bwlimit = "10M"; | 
					
						
							|  |  |  |             drive-use-trash = "true"; | 
					
						
							|  |  |  |           }; | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         rcloneConfig = mkOption { | 
					
						
							|  |  |  |           type = with types; nullOr (attrsOf (oneOf [ str bool ])); | 
					
						
							|  |  |  |           default = null; | 
					
						
							|  |  |  |           description = ''
 | 
					
						
							|  |  |  |             Configuration for the rclone remote being used for backup. | 
					
						
							|  |  |  |             See the remote's specific options under rclone's docs at | 
					
						
							|  |  |  |             <link xlink:href="https://rclone.org/docs/"/>. When specifying | 
					
						
							| 
									
										
										
										
											2020-08-07 14:43:58 +01:00
										 |  |  |             option names, use the "config" name specified in the docs. | 
					
						
							| 
									
										
										
										
											2020-07-05 16:25:33 -04:00
										 |  |  |             For example, to set <literal>--b2-hard-delete</literal> for a B2 | 
					
						
							|  |  |  |             remote, use <literal>hard_delete = true</literal> in the | 
					
						
							|  |  |  |             attribute set. | 
					
						
							|  |  |  |             Warning: Secrets set in here will be world-readable in the Nix | 
					
						
							|  |  |  |             store! Consider using the <literal>rcloneConfigFile</literal> | 
					
						
							|  |  |  |             option instead to specify secret values separately. Note that | 
					
						
							|  |  |  |             options set here will override those set in the config file. | 
					
						
							|  |  |  |           '';
 | 
					
						
							|  |  |  |           example = { | 
					
						
							|  |  |  |             type = "b2"; | 
					
						
							|  |  |  |             account = "xxx"; | 
					
						
							|  |  |  |             key = "xxx"; | 
					
						
							|  |  |  |             hard_delete = true; | 
					
						
							|  |  |  |           }; | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         rcloneConfigFile = mkOption { | 
					
						
							|  |  |  |           type = with types; nullOr path; | 
					
						
							|  |  |  |           default = null; | 
					
						
							|  |  |  |           description = ''
 | 
					
						
							|  |  |  |             Path to the file containing rclone configuration. This file | 
					
						
							|  |  |  |             must contain configuration for the remote specified in this backup | 
					
						
							|  |  |  |             set and also must be readable by root. Options set in | 
					
						
							|  |  |  |             <literal>rcloneConfig</literal> will override those set in this | 
					
						
							|  |  |  |             file. | 
					
						
							|  |  |  |           '';
 | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-21 12:12:43 +02:00
										 |  |  |         repository = mkOption { | 
					
						
							|  |  |  |           type = types.str; | 
					
						
							|  |  |  |           description = ''
 | 
					
						
							|  |  |  |             repository to backup to. | 
					
						
							|  |  |  |           '';
 | 
					
						
							|  |  |  |           example = "sftp:backup@192.168.1.100:/backups/${name}"; | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         paths = mkOption { | 
					
						
							|  |  |  |           type = types.listOf types.str; | 
					
						
							|  |  |  |           default = []; | 
					
						
							|  |  |  |           description = ''
 | 
					
						
							|  |  |  |             Which paths to backup. | 
					
						
							|  |  |  |           '';
 | 
					
						
							|  |  |  |           example = [ | 
					
						
							|  |  |  |             "/var/lib/postgresql" | 
					
						
							|  |  |  |             "/home/user/backup" | 
					
						
							|  |  |  |           ]; | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         timerConfig = mkOption { | 
					
						
							| 
									
										
										
										
											2019-01-23 21:29:02 +01:00
										 |  |  |           type = types.attrsOf unitOption; | 
					
						
							| 
									
										
										
										
											2018-04-21 12:12:43 +02:00
										 |  |  |           default = { | 
					
						
							|  |  |  |             OnCalendar = "daily"; | 
					
						
							|  |  |  |           }; | 
					
						
							|  |  |  |           description = ''
 | 
					
						
							|  |  |  |             When to run the backup. See man systemd.timer for details. | 
					
						
							|  |  |  |           '';
 | 
					
						
							|  |  |  |           example = { | 
					
						
							|  |  |  |             OnCalendar = "00:05"; | 
					
						
							|  |  |  |             RandomizedDelaySec = "5h"; | 
					
						
							|  |  |  |           }; | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         user = mkOption { | 
					
						
							|  |  |  |           type = types.str; | 
					
						
							|  |  |  |           default = "root"; | 
					
						
							|  |  |  |           description = ''
 | 
					
						
							|  |  |  |             As which user the backup should run. | 
					
						
							|  |  |  |           '';
 | 
					
						
							|  |  |  |           example = "postgresql"; | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         extraBackupArgs = mkOption { | 
					
						
							|  |  |  |           type = types.listOf types.str; | 
					
						
							|  |  |  |           default = []; | 
					
						
							|  |  |  |           description = ''
 | 
					
						
							|  |  |  |             Extra arguments passed to restic backup. | 
					
						
							|  |  |  |           '';
 | 
					
						
							|  |  |  |           example = [ | 
					
						
							|  |  |  |             "--exclude-file=/etc/nixos/restic-ignore" | 
					
						
							|  |  |  |           ]; | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         extraOptions = mkOption { | 
					
						
							|  |  |  |           type = types.listOf types.str; | 
					
						
							|  |  |  |           default = []; | 
					
						
							|  |  |  |           description = ''
 | 
					
						
							|  |  |  |             Extra extended options to be passed to the restic --option flag. | 
					
						
							|  |  |  |           '';
 | 
					
						
							|  |  |  |           example = [ | 
					
						
							|  |  |  |             "sftp.command='ssh backup@192.168.1.100 -i /home/user/.ssh/id_rsa -s sftp'" | 
					
						
							|  |  |  |           ]; | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         initialize = mkOption { | 
					
						
							|  |  |  |           type = types.bool; | 
					
						
							|  |  |  |           default = false; | 
					
						
							|  |  |  |           description = ''
 | 
					
						
							|  |  |  |             Create the repository if it doesn't exist. | 
					
						
							|  |  |  |           '';
 | 
					
						
							|  |  |  |         }; | 
					
						
							| 
									
										
										
										
											2018-07-21 22:24:19 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |         pruneOpts = mkOption { | 
					
						
							|  |  |  |           type = types.listOf types.str; | 
					
						
							|  |  |  |           default = []; | 
					
						
							|  |  |  |           description = ''
 | 
					
						
							|  |  |  |             A list of options (--keep-* et al.) for 'restic forget | 
					
						
							|  |  |  |             --prune', to automatically prune old snapshots.  The | 
					
						
							|  |  |  |             'forget' command is run *after* the 'backup' command, so | 
					
						
							|  |  |  |             keep that in mind when constructing the --keep-* options. | 
					
						
							|  |  |  |           '';
 | 
					
						
							|  |  |  |           example = [ | 
					
						
							|  |  |  |             "--keep-daily 7" | 
					
						
							|  |  |  |             "--keep-weekly 5" | 
					
						
							|  |  |  |             "--keep-monthly 12" | 
					
						
							|  |  |  |             "--keep-yearly 75" | 
					
						
							|  |  |  |           ]; | 
					
						
							|  |  |  |         }; | 
					
						
							| 
									
										
										
										
											2018-08-05 12:53:53 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |         dynamicFilesFrom = mkOption { | 
					
						
							|  |  |  |           type = with types; nullOr str; | 
					
						
							|  |  |  |           default = null; | 
					
						
							|  |  |  |           description = ''
 | 
					
						
							|  |  |  |             A script that produces a list of files to back up.  The | 
					
						
							|  |  |  |             results of this command are given to the '--files-from' | 
					
						
							|  |  |  |             option. | 
					
						
							|  |  |  |           '';
 | 
					
						
							|  |  |  |           example = "find /home/matt/git -type d -name .git"; | 
					
						
							|  |  |  |         }; | 
					
						
							| 
									
										
										
										
											2018-04-21 12:12:43 +02:00
										 |  |  |       }; | 
					
						
							|  |  |  |     })); | 
					
						
							|  |  |  |     default = {}; | 
					
						
							|  |  |  |     example = { | 
					
						
							|  |  |  |       localbackup = { | 
					
						
							|  |  |  |         paths = [ "/home" ]; | 
					
						
							|  |  |  |         repository = "/mnt/backup-hdd"; | 
					
						
							|  |  |  |         passwordFile = "/etc/nixos/secrets/restic-password"; | 
					
						
							|  |  |  |         initialize = true; | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |       remotebackup = { | 
					
						
							|  |  |  |         paths = [ "/home" ]; | 
					
						
							|  |  |  |         repository = "sftp:backup@host:/backups/home"; | 
					
						
							|  |  |  |         passwordFile = "/etc/nixos/secrets/restic-password"; | 
					
						
							|  |  |  |         extraOptions = [ | 
					
						
							|  |  |  |           "sftp.command='ssh backup@host -i /etc/nixos/secrets/backup-private-key -s sftp'" | 
					
						
							|  |  |  |         ]; | 
					
						
							|  |  |  |         timerConfig = { | 
					
						
							|  |  |  |           OnCalendar = "00:05"; | 
					
						
							|  |  |  |           RandomizedDelaySec = "5h"; | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   config = { | 
					
						
							|  |  |  |     systemd.services = | 
					
						
							|  |  |  |       mapAttrs' (name: backup: | 
					
						
							|  |  |  |         let | 
					
						
							|  |  |  |           extraOptions = concatMapStrings (arg: " -o ${arg}") backup.extraOptions; | 
					
						
							|  |  |  |           resticCmd = "${pkgs.restic}/bin/restic${extraOptions}"; | 
					
						
							| 
									
										
										
										
											2018-08-05 12:53:53 -04:00
										 |  |  |           filesFromTmpFile = "/run/restic-backups-${name}/includes"; | 
					
						
							|  |  |  |           backupPaths = if (backup.dynamicFilesFrom == null) | 
					
						
							|  |  |  |                         then concatStringsSep " " backup.paths | 
					
						
							|  |  |  |                         else "--files-from ${filesFromTmpFile}"; | 
					
						
							| 
									
										
										
										
											2020-01-31 22:06:19 -05:00
										 |  |  |           pruneCmd = optionals (builtins.length backup.pruneOpts > 0) [ | 
					
						
							| 
									
										
										
										
											2020-01-30 17:07:21 +00:00
										 |  |  |             ( resticCmd + " forget --prune " + (concatStringsSep " " backup.pruneOpts) ) | 
					
						
							|  |  |  |             ( resticCmd + " check" ) | 
					
						
							|  |  |  |           ]; | 
					
						
							| 
									
										
										
										
											2020-07-05 16:25:33 -04:00
										 |  |  |           # Helper functions for rclone remotes | 
					
						
							|  |  |  |           rcloneRemoteName = builtins.elemAt (splitString ":" backup.repository) 1; | 
					
						
							|  |  |  |           rcloneAttrToOpt = v: "RCLONE_" + toUpper (builtins.replaceStrings [ "-" ] [ "_" ] v); | 
					
						
							|  |  |  |           rcloneAttrToConf = v: "RCLONE_CONFIG_" + toUpper (rcloneRemoteName + "_" + v); | 
					
						
							|  |  |  |           toRcloneVal = v: if lib.isBool v then lib.boolToString v else v; | 
					
						
							| 
									
										
										
										
											2018-04-21 12:12:43 +02:00
										 |  |  |         in nameValuePair "restic-backups-${name}" ({ | 
					
						
							|  |  |  |           environment = { | 
					
						
							|  |  |  |             RESTIC_PASSWORD_FILE = backup.passwordFile; | 
					
						
							|  |  |  |             RESTIC_REPOSITORY = backup.repository; | 
					
						
							| 
									
										
										
										
											2020-07-05 16:25:33 -04:00
										 |  |  |           } // optionalAttrs (backup.rcloneOptions != null) (mapAttrs' (name: value: | 
					
						
							|  |  |  |             nameValuePair (rcloneAttrToOpt name) (toRcloneVal value) | 
					
						
							|  |  |  |           ) backup.rcloneOptions) // optionalAttrs (backup.rcloneConfigFile != null) { | 
					
						
							|  |  |  |             RCLONE_CONFIG = backup.rcloneConfigFile; | 
					
						
							|  |  |  |           } // optionalAttrs (backup.rcloneConfig != null) (mapAttrs' (name: value: | 
					
						
							|  |  |  |             nameValuePair (rcloneAttrToConf name) (toRcloneVal value) | 
					
						
							|  |  |  |           ) backup.rcloneConfig); | 
					
						
							| 
									
										
										
										
											2020-01-30 17:07:21 +00:00
										 |  |  |           path = [ pkgs.openssh ]; | 
					
						
							| 
									
										
										
										
											2018-04-21 12:12:43 +02:00
										 |  |  |           restartIfChanged = false; | 
					
						
							|  |  |  |           serviceConfig = { | 
					
						
							|  |  |  |             Type = "oneshot"; | 
					
						
							| 
									
										
											  
											
												nixos/restic: correct location of cache directory
By default, restic determines the location of the cache based on the XDG
base dir specification, which is `~/.cache/restic` when the environment
variable `$XDG_CACHE_HOME` isn't set.
As restic is executed as root by default, this resulted in the cache being
written to `/root/.cache/restic`, which is not quite right for a system
service and also meant, multiple backup services would use the same cache
directory - potentially causing issues with locking, data corruption,
etc.
The goal was to ensure, restic uses the correct cache location for a
system service - one cache per backup specification, using `/var/cache`
as the base directory for it.
systemd sets the environment variable `$CACHE_DIRECTORY` once
`CacheDirectory=` is defined, but restic doesn't change its behavior
based on the presence of this environment variable.
Instead, the specifier [1] `%C` can be used to point restic explicitly
towards the correct cache location using the `--cache-dir` argument.
Furthermore, the `CacheDirectoryMode=` was set to `0700`, as the default
of `0755` is far too open in this case, as the cache might contain
sensitive data.
[1] https://www.freedesktop.org/software/systemd/man/systemd.unit.html#Specifiers
											
										 
											2020-10-04 18:47:52 +02:00
										 |  |  |             ExecStart = [ "${resticCmd} backup --cache-dir=%C/restic-backups-${name} ${concatStringsSep " " backup.extraBackupArgs} ${backupPaths}" ] ++ pruneCmd; | 
					
						
							| 
									
										
										
										
											2018-04-21 12:12:43 +02:00
										 |  |  |             User = backup.user; | 
					
						
							| 
									
										
										
										
											2018-08-05 12:53:53 -04:00
										 |  |  |             RuntimeDirectory = "restic-backups-${name}"; | 
					
						
							| 
									
										
											  
											
												nixos/restic: correct location of cache directory
By default, restic determines the location of the cache based on the XDG
base dir specification, which is `~/.cache/restic` when the environment
variable `$XDG_CACHE_HOME` isn't set.
As restic is executed as root by default, this resulted in the cache being
written to `/root/.cache/restic`, which is not quite right for a system
service and also meant, multiple backup services would use the same cache
directory - potentially causing issues with locking, data corruption,
etc.
The goal was to ensure, restic uses the correct cache location for a
system service - one cache per backup specification, using `/var/cache`
as the base directory for it.
systemd sets the environment variable `$CACHE_DIRECTORY` once
`CacheDirectory=` is defined, but restic doesn't change its behavior
based on the presence of this environment variable.
Instead, the specifier [1] `%C` can be used to point restic explicitly
towards the correct cache location using the `--cache-dir` argument.
Furthermore, the `CacheDirectoryMode=` was set to `0700`, as the default
of `0755` is far too open in this case, as the cache might contain
sensitive data.
[1] https://www.freedesktop.org/software/systemd/man/systemd.unit.html#Specifiers
											
										 
											2020-10-04 18:47:52 +02:00
										 |  |  |             CacheDirectory = "restic-backups-${name}"; | 
					
						
							|  |  |  |             CacheDirectoryMode = "0700"; | 
					
						
							| 
									
										
										
										
											2018-05-30 22:30:12 -04:00
										 |  |  |           } // optionalAttrs (backup.s3CredentialsFile != null) { | 
					
						
							|  |  |  |             EnvironmentFile = backup.s3CredentialsFile; | 
					
						
							| 
									
										
										
										
											2018-04-21 12:12:43 +02:00
										 |  |  |           }; | 
					
						
							| 
									
										
										
										
											2020-01-30 17:07:21 +00:00
										 |  |  |         } // optionalAttrs (backup.initialize || backup.dynamicFilesFrom != null) { | 
					
						
							|  |  |  |           preStart = ''
 | 
					
						
							|  |  |  |             ${optionalString (backup.initialize) ''
 | 
					
						
							|  |  |  |               ${resticCmd} snapshots || ${resticCmd} init | 
					
						
							|  |  |  |             ''}
 | 
					
						
							|  |  |  |             ${optionalString (backup.dynamicFilesFrom != null) ''
 | 
					
						
							|  |  |  |               ${pkgs.writeScript "dynamicFilesFromScript" backup.dynamicFilesFrom} > ${filesFromTmpFile} | 
					
						
							|  |  |  |             ''}
 | 
					
						
							|  |  |  |           '';
 | 
					
						
							|  |  |  |         } // optionalAttrs (backup.dynamicFilesFrom != null) { | 
					
						
							| 
									
										
										
										
											2018-08-05 12:53:53 -04:00
										 |  |  |           postStart = ''
 | 
					
						
							|  |  |  |             rm ${filesFromTmpFile} | 
					
						
							| 
									
										
										
										
											2018-04-21 12:12:43 +02:00
										 |  |  |           '';
 | 
					
						
							|  |  |  |         }) | 
					
						
							|  |  |  |       ) config.services.restic.backups; | 
					
						
							|  |  |  |     systemd.timers = | 
					
						
							|  |  |  |       mapAttrs' (name: backup: nameValuePair "restic-backups-${name}" { | 
					
						
							|  |  |  |         wantedBy = [ "timers.target" ]; | 
					
						
							|  |  |  |         timerConfig = backup.timerConfig; | 
					
						
							|  |  |  |       }) config.services.restic.backups; | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | } |