diff options
Diffstat (limited to '')
| -rw-r--r-- | modules/by-name/ba/backup/module.nix | 193 |
1 files changed, 155 insertions, 38 deletions
diff --git a/modules/by-name/ba/backup/module.nix b/modules/by-name/ba/backup/module.nix index 3e07fbd1..7a788764 100644 --- a/modules/by-name/ba/backup/module.nix +++ b/modules/by-name/ba/backup/module.nix @@ -11,78 +11,177 @@ lib, config, pkgs, + libraries, ... }: let cfg = config.soispha.services.backup; + + snapshotDir = "/srv/last_snapshot"; in { options.soispha.services.backup = { - enable = lib.mkEnableOption "backups via restic to a storagebox"; + enable = libraries.base.options.mkEnable "backups via restic"; - user = lib.mkOption { - type = lib.types.str; - description = "The storagebox-user to use"; - example = "u384702-sub2"; - }; - privateSshKey = lib.mkOption { - type = lib.types.path; - description = "The age-encrypted ssh-key, passed to agenix"; + storagebox = { + enable = lib.mkEnableOption "remote backups"; + user = lib.mkOption { + type = lib.types.str; + description = "The storagebox-user to use"; + example = "u384702-sub2"; + }; + + sshKey = lib.mkOption { + type = lib.types.path; + description = "The age-encrypted ssh-key, passed to agenix"; + default = ./secrets/storagebox/ssh_key.age; + }; + + repositoryPassword = lib.mkOption { + type = lib.types.path; + description = "The age-encrypted restic password, passed to agenix"; + default = ./secrets/storagebox/repository_password.age; + }; }; - privatePassword = lib.mkOption { - type = lib.types.path; - description = "The age-encrypted restic password, passed to agenix"; + + local = { + enable = lib.mkEnableOption "local backups"; + + repositoryPassword = lib.mkOption { + type = lib.types.path; + description = "The age-encrypted restic password, passed to agenix"; + default = ./secrets/local/repository_password.age; + }; + + backupMountPoint = lib.mkOption { + type = lib.types.path; + description = "The path where to expect the mounted backup disk"; + default = "/mnt/backup"; + }; }; }; config = lib.mkIf cfg.enable { + soispha.impermanence.directories = lib.mkMerge [ + (lib.mkIf cfg.storagebox.enable [ + "/var/cache/restic-backups-storagebox" + ]) + (lib.mkIf cfg.local.enable [ + "/var/cache/restic-backups-local" + ]) + ]; + age.secrets = { - resticpass = { - file = cfg.privatePassword; + resticStorageboxSshKey = lib.mkIf cfg.storagebox.enable { + file = cfg.storagebox.sshKey; mode = "0700"; owner = "root"; group = "root"; }; - resticssh = { - file = cfg.privateSshKey; + resticStorageboxRepositoryPassword = lib.mkIf cfg.storagebox.enable { + file = cfg.storagebox.repositoryPassword; + mode = "0700"; + owner = "root"; + group = "root"; + }; + resticLocalRepositoryPassword = lib.mkIf cfg.local.enable { + file = cfg.local.repositoryPassword; mode = "0700"; owner = "root"; group = "root"; }; }; - soispha.programs.ssh = { + soispha.programs.ssh = lib.mkIf cfg.storagebox.enable { enable = true; rootKnownHosts = { "[u459143-sub1.your-storagebox.de]:23" = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIICf9svRenC/PLKIL9nk6K/pxQgoiFC41wTNvoIncOxs"; }; }; + systemd.services = { + prepare-backup = { + requires = []; + after = []; + + description = "Prepare a backup by snapshotting the system."; + + serviceConfig = { + ExecStart = lib.getExe (pkgs.writeShellApplication { + name = "prepareBackup"; + text = '' + set -x + + [ -d "${snapshotDir}" ] && btrfs subvolume delete "${snapshotDir}" + + # -r := Make the snapshot read-only + btrfs subvolume snapshot -r /srv "${snapshotDir}"; + ''; + + inheritPath = false; + runtimeInputs = [ + pkgs.btrfs-progs + ]; + }); + + Type = "oneshot"; + + User = "root"; + Group = "root"; + + # TODO: Hardening <2025-05-04> + }; + }; + + restic-backups-storagebox = lib.mkIf cfg.storagebox.enable { + requires = ["prepare-backup.service"]; + after = ["prepare-backup.service"]; + }; + + restic-backups-local = lib.mkIf cfg.local.enable { + requires = ["prepare-backup.service"]; + after = ["prepare-backup.service"]; + + serviceConfig = { + ConditionPathIsDirectory = "${cfg.local.backupMountPoint}"; + }; + }; + }; + services.restic.backups = let - snapshotDir = "/srv/last_snapshot"; homeDir = "${snapshotDir}/home"; + + paths = [ + snapshotDir + ]; + exclude = [ + "${homeDir}/soispha/.cache" + ]; + extraBackupArgs = [ + "--verbose=2" + ]; in { - storagebox = { + local = lib.mkIf cfg.local.enable { + inhibitsSleep = true; initialize = true; - backupPrepareCommand = - # bash - '' - [ -d "${snapshotDir}" ] && ${lib.getExe' pkgs.btrfs-progs "btrfs"} subvolume delete "${snapshotDir}" - - # -r := Make the snapshot read-only - ${lib.getExe' pkgs.btrfs-progs "btrfs"} subvolume snapshot -r /srv "${snapshotDir}"; - ''; - paths = [ - snapshotDir - ]; - exclude = [ - "${homeDir}/soispha/.cache" - ]; - extraBackupArgs = [ - "--verbose=2" - ]; - passwordFile = config.age.secrets.resticpass.path; + inherit paths exclude extraBackupArgs; + + passwordFile = config.age.secrets.resticLocalRepositoryPassword.path; + + repository = "${cfg.local.backupMountPoint}/restic-backup-data/"; + + # Start on demand. + timerConfig = null; + }; + + storagebox = lib.mkIf cfg.storagebox.enable { + inhibitsSleep = true; + initialize = true; + + inherit paths exclude extraBackupArgs; + + passwordFile = config.age.secrets.resticStorageboxRepositoryPassword.path; extraOptions = [ - "rclone.program='ssh -p 23 ${cfg.user}@${cfg.user}.your-storagebox.de -i ${config.age.secrets.resticssh.path} command_forced_on_remote'" + "rclone.program='ssh -p 23 ${cfg.storagebox.user}@${cfg.storagebox.user}.your-storagebox.de -i ${config.age.secrets.resticStorageboxSshKey.path} command_forced_on_remote'" ]; # This setting is normally passed to rclone, but we force @@ -97,6 +196,24 @@ in { Persistent = true; }; }; + + # This is only for listing, pruning and such stuff. + storagebox-admin = lib.mkIf cfg.storagebox.enable { + inhibitsSleep = false; + initialize = false; + + passwordFile = config.age.secrets.resticStorageboxRepositoryPassword.path; + extraOptions = [ + "rclone.program='ssh -p 23 ${cfg.storagebox.user}@${cfg.storagebox.user}.your-storagebox.de command_forced_on_remote'" + ]; + + # This setting is normally passed to rclone, but we force + # the command on the remote. + # As such, the value does not matter and must only be parseable by restic. + repository = "rclone: "; + + timerConfig = null; + }; }; }; } |
