about summary refs log tree commit diff stats
path: root/modules/by-name/ba/backup/module.nix
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--modules/by-name/ba/backup/module.nix212
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;
+      };
     };
   };
 }