diff options
Diffstat (limited to '')
-rw-r--r-- | modules/by-name/ba/backup/module.nix | 212 |
1 files changed, 184 insertions, 28 deletions
diff --git a/modules/by-name/ba/backup/module.nix b/modules/by-name/ba/backup/module.nix index 92700bf2..d0805092 100644 --- a/modules/by-name/ba/backup/module.nix +++ b/modules/by-name/ba/backup/module.nix @@ -1,51 +1,207 @@ +# nixos-config - My current NixOS configuration +# +# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of my nixos-config. +# +# You should have received a copy of the License along with this program. +# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. { lib, - pkgs, config, + pkgs, ... }: let - backup-script = pkgs.writeShellScriptBin "backsnap" '' - set -xeu; - - ${pkgs.util-linux}/bin/mount --mkdir "/dev/disk/by-uuid/${cfg.backupDiskUuid}" "/run/media/${cfg.backupDiskUuid}"; - ${pkgs.snap-sync-forked}/bin/snap-sync-forked --UUID "${cfg.backupDiskUuid}" --noconfirm; - ${pkgs.util-linux}/bin/umount "/run/media/${cfg.backupDiskUuid}"; - ''; - cfg = config.soispha.services.backup; + + snapshotDir = "/srv/last_snapshot"; in { options.soispha.services.backup = { - enable = lib.mkEnableOption "backups with my forked snap-sync"; - backupDiskUuid = lib.mkOption { - type = lib.types.str; - example = lib.literalExpression "d1d20ae7-3d8a-44da-86da-677dbbb10c89"; - description = "The UUID of the backup disk"; + 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; + }; + }; + + 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 { - systemd = { - services.backup = { - wantedBy = lib.mkForce []; - unitConfig = { - Description = "Backup the last snapshots of the persitent-storage subvolume."; - }; + config = { + age.secrets = { + resticStorageboxSshKey = lib.mkIf cfg.storagebox.enable { + file = cfg.storagebox.sshKey; + mode = "0700"; + owner = "root"; + group = "root"; + }; + 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 = 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"; - ExecStart = "${backup-script}/bin/backsnap"; + + User = "root"; + Group = "root"; + + # TODO: Hardening <2025-05-04> }; }; - timers.backup = { - wantedBy = ["timers.target"]; - unitConfig = { - Description = "Backup 15min after boot and every 8 hours"; + 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 + homeDir = "${snapshotDir}/home"; + + paths = [ + snapshotDir + ]; + exclude = [ + "${homeDir}/soispha/.cache" + ]; + extraBackupArgs = [ + "--verbose=2" + ]; + in { + local = lib.mkIf cfg.local.enable { + inhibitsSleep = true; + initialize = true; + + 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.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 + # the command on the remote. + # As such, the value does not matter and must only be parseable by restic. + repository = "rclone: "; + timerConfig = { - OnBootSec = "15min"; - OnUnitActiveSec = "8h"; + Requires = "network-online.target"; + OnActiveSec = "30m"; + OnUnitInactiveSec = "2h"; + 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; + }; }; }; } |