aboutsummaryrefslogtreecommitdiffstats
path: root/modules/by-name/ba/backup
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--modules/by-name/ba/backup/env.vars1
-rw-r--r--modules/by-name/ba/backup/module.nix244
-rw-r--r--modules/by-name/ba/backup/secrets/local/repository_password.age20
-rw-r--r--modules/by-name/ba/backup/secrets/storagebox/repository_password.age14
-rw-r--r--modules/by-name/ba/backup/secrets/storagebox/ssh_key.age22
5 files changed, 274 insertions, 27 deletions
diff --git a/modules/by-name/ba/backup/env.vars b/modules/by-name/ba/backup/env.vars
new file mode 100644
index 00000000..339c5a5b
--- /dev/null
+++ b/modules/by-name/ba/backup/env.vars
@@ -0,0 +1 @@
+RESTIC_COMPRESSION=max
diff --git a/modules/by-name/ba/backup/module.nix b/modules/by-name/ba/backup/module.nix
index 92700bf2..031445cf 100644
--- a/modules/by-name/ba/backup/module.nix
+++ b/modules/by-name/ba/backup/module.nix
@@ -1,51 +1,241 @@
+# 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,
+ libraries,
...
}: 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";
+ enable = libraries.base.options.mkEnable "backups via restic";
+
+ 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.";
- };
+ soispha.impermanence.directories = lib.mkMerge [
+ (lib.mkIf cfg.storagebox.enable [
+ "/var/cache/restic-backups-storagebox"
+
+ # Generate the base systemd.mount from impermanence (we will override this, so it
+ # points to the non-admin one instead)
+ "/var/cache/restic-backups-storagebox-admin"
+ ])
+ (lib.mkIf cfg.local.enable [
+ "/var/cache/restic-backups-local"
+ ])
+ ];
+
+ # TODO: Make this work (`systemd.mounts` is a list, so _this_ doesn't work.) <2026-07-07>
+ # systemd.mounts."/var/cache/restic-backups-storagebox-admin" = {
+ # # TODO: Don't hardcode the path, but use
+ # # `systemd.mounts."/var/cache/restic-backups-storagebox".what` instead <2026-07-07>
+ # what = "/srv/var/cache/restic-backups-storagebox";
+ # };
+
+ 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";
+ };
+ };
+
+ programs.fuse.enable = cfg.storagebox.enable || cfg.local.enable;
+
+ 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"
+ "${homeDir}/soispha/.local/share/lutris/install"
+ "${homeDir}/soispha/.local/share/Steam/steamapps/common"
+ ];
+ extraBackupArgs = [
+ # Don't scan the filesystem to determine an estimate.
+ "--no-scan"
+ "--verbose=2"
+ ];
+ environmentFile = "${./env.vars}";
+ in {
+ local = lib.mkIf cfg.local.enable {
+ inhibitsSleep = true;
+ initialize = true;
+
+ inherit paths exclude extraBackupArgs environmentFile;
+
+ 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 environmentFile;
+
+ 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 parse-able 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 = true;
+
+ # Don't create the repository if it doesn't exist yet.
+ initialize = false;
+
+ inherit paths exclude extraBackupArgs environmentFile;
+
+ 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 parse-able by restic.
+ repository = "rclone: ";
+
+ timerConfig = null;
+ };
};
};
}
diff --git a/modules/by-name/ba/backup/secrets/local/repository_password.age b/modules/by-name/ba/backup/secrets/local/repository_password.age
new file mode 100644
index 00000000..b1508e49
--- /dev/null
+++ b/modules/by-name/ba/backup/secrets/local/repository_password.age
@@ -0,0 +1,20 @@
+-----BEGIN AGE ENCRYPTED FILE-----
+YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqVCtBZzNBWFlxQnhlb3c2
+NG0xMjV3V2hQUWhNb01ROGpFbzM0c3NjRWlvCjRqMTdxNlNsY1lDM2VCUGRjcldZ
+SUhScGRSejY3TFlwVjlweTRERkU4ZTQKLT4gc3NoLWVkMjU1MTkgelpFb25nIGtj
+cVN4RzFseTJCVXhOc0tEWi96ckpGQjhNdUZrb1FVWnJ6TU1vSHZCMUkKc1JwM2Ir
+TzNzZEV5VEE4QUp4cDdTNWZvTFlZR0tUbUFZTnNEZE1McnhISQotPiBzc2gtZWQy
+NTUxOSA3SGZGVXcgYVQzRHdaa1NYUUpLODUyL0orV3d4Z1o1V2hhekZXVWFzNW50
+enhSYnlWbwpzbnZwK1dSczJ5SGZsRjNyRWZraUcreWlBakZOZkVyZ01CWml5V0E4
+TzY4Ci0+ICpMUC1ncmVhc2UgV0IKUTZVMzR1QTVGdWRyNWprVVpjaWhCTTFSRzY3
+bHVtdHpJQXlWQVFHZ3FzbkZncHRsQnJIaHlEekl5ZE15Sko0YQpSVEUKLS0tIGFy
+N1Y2OUREY1NOL2xPODd4Y3Y4Wk83NFJ5ZVBpNnJYelo3RmZPL1V4Q28KcA+6qt2d
+LrL0FS279XL1uqSeXiDdBBBh5i951dRx9ML33g7IoQxaGqSWU0yP/y7fcSp3B/rT
+/alQGRL/uYnCS2z8zCngr2YCQBXAI8bOgf+th7fzc15FXNB14WMGvrCVRB8Rr6JZ
+itTxcAebY192xeINJG269iL6Ef0YrmZLUufKQ9lcVob/G32tG0vxSbSBBx3asf7y
+toiNdlXpxJPrIpFx5mc3dnkaXCTSRuXjiAqnncW/HNxovCI90sY3dyL+XRC8iEOD
+sql7C7jUUvLubptk1gGW5pHmGvqeCLr+fJ6XrVqXE5VxOSbzvSUsdS84WOBInLiO
+V36aUtthEzdRXd81P/n7U26O93GWpIwzctu0WwSHHHsPKhxqFY4RuWa3mO4Zk6fu
+O6fsYnpAqYiCUd/k2zKrDtLosTnwuwc7Vm1glm4DEEtvteVibw47SR1LLfUds5XL
+j1OR7Z6/ljIEKPjE1a81pQjYRfHDUtceXUvrlL6iCID30zU=
+-----END AGE ENCRYPTED FILE-----
diff --git a/modules/by-name/ba/backup/secrets/storagebox/repository_password.age b/modules/by-name/ba/backup/secrets/storagebox/repository_password.age
new file mode 100644
index 00000000..a2aa984a
--- /dev/null
+++ b/modules/by-name/ba/backup/secrets/storagebox/repository_password.age
@@ -0,0 +1,14 @@
+-----BEGIN AGE ENCRYPTED FILE-----
+YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzbnJ5Z3NXRFRtdEdCNk1z
+NG9Cb2FUTEZ2U2hTdUptWGpneHd2RGxGQlY0ClhqcHlMSUZlLzBPZko4a2hKdnE1
+Q1hnc3ErWlJiSk1HVXFLcSswQlV0WTAKLT4gc3NoLWVkMjU1MTkgelpFb25nIG5I
+cFpMY21ZdU9TUHY5VksraG1xeFByQlM1WE1INEk0SWFJQzIvSXhZbXMKSWxiNlIr
+NjZ6bEgySUJGdnpoZTY2MmxxajJMaUVTM3RkTmhpdkxaRnNCQQotPiBzc2gtZWQy
+NTUxOSA3SGZGVXcgRGtLMHFORm9mUDUwZnBXM0djcFlxTExJbWVDeDJmbVVSRi9l
+QTFmekpROApZY3Z0Zlc1SUdCcllKQ01ZNmdFQkI3UnViVGxzNTJNUkhqc2hOejY3
+MndrCi0+IG1Hbi1ncmVhc2UgUDJJdgpGUmVHa1NPWGtzUmZmMXM3eWkxVnJkek1h
+MUxZeS9qMlArendXZ1ltdFgvMTBabTlPM2hzSjhuVXBQNktHKzc1ClF6bjZob09B
+Ci0tLSB3dEZpbnpVR0pPdGpFVmNyM0JzUGl6eGcreXpkTG9xQUc5S09ndUthaEpR
+CuHgXCIq/AUCuIQWepBRapAab6zczOwxEBpDv5R2kw2mw34UVRLuFs7hB1cHE79y
+7Fc=
+-----END AGE ENCRYPTED FILE-----
diff --git a/modules/by-name/ba/backup/secrets/storagebox/ssh_key.age b/modules/by-name/ba/backup/secrets/storagebox/ssh_key.age
new file mode 100644
index 00000000..a7f30c7c
--- /dev/null
+++ b/modules/by-name/ba/backup/secrets/storagebox/ssh_key.age
@@ -0,0 +1,22 @@
+-----BEGIN AGE ENCRYPTED FILE-----
+YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJOTFaQVpnQ2VJN1pJbmdu
+STF2UTF1Y3lyU2p3OUJlcWRFWlg3Mm9vSEFFCjg4RklaZ0hqdWdZSlJ0dGx0ckJN
+eHE4R1QyTEJEVkZudnVQV2VpblBYTW8KLT4gc3NoLWVkMjU1MTkgelpFb25nIDR0
+Z2RwTXJGV1VYT2Y1OUcxbXpnOGNEbG9BWmFxeWUzZ1huNW1YQk5BM3cKK1hLNnYx
+VnNvRDZWM3Y4VUpCQTRtbHJOcDJLdTBOUXJ1ak52Mkp3TE9OcwotPiBzc2gtZWQy
+NTUxOSA3SGZGVXcgSHFSOXM1Vm1lY0ErRkZobndEU0xvQjVXbnhDbUlGNmE2dStY
+SEFqSk9TMAo2VU5UbW9rc29kendvcHNuU3hFQ2lBQWNjRUxUSE10RmtjbkdwWGlv
+cTRVCi0+IDwtZ3JlYXNlIHNPT3N1IHNkZzcoQzUgLUM3Ml1Tcgorb3FQcWlDSkJQ
+L1g4WisydkYxdnJMeDNISVVDTGlZdmUyRkV0TkFUdW4ycXZBSlF3UWM2R3FTYjRs
+ckEyaTg2ClVrbwotLS0gdHF5Q3BqV3h2TzEySCs4OFNIYmx1U0hsTWo1RDlTV3VC
+T1RmelZaWXh4YwoeRkYbirDGX9aZj6OVxZ8sVsm2Sz23ikcBhAdTIXKErM7eQcdp
+Y2nA5kUebPVznbSq/XSQNbqvCfSv9wVVTZWww7AqkrltjFS8yxQVMKB37IfnJsZI
+92OCjJKArxAbvS7ERKY51Cf9oY/VqGk5WqxExH5ZvdbL144UI8JzrxhDopVXBWNd
+fCrHirRAdlu3wgVILXV3vk1kmE69sspuV4cer5f/kOBKT1w09s2NFFHH4TO3cLTJ
+Obgi/HWYHBjqGSgwd4bDtNOW2UDE1LN3YeUhloutWhuX8yt+NFzZJtl+CBrRqLKA
+0CK+ZPKPWWKYPCNlzKT4vDF9BZeyCfoEj9dTXvxrNPEB6YYi16aQ8DhI8wxADl41
+ShrULlEmB8rfz7V+8TFmERKISBRSSH0p+XJi8e2DWh2dSco16rizmbFFNDdcYiGT
+WWbXYCFNzj+kNFI/vEtt9dEWmfXlgg01khnyoOuRLedtoCDSuF+8z7YYy6oYt/2h
+gmf4NHOUgyxK56c17JMQ3xT97d2odgqmwB+YZQ13IJCjt3M3pmSssD4sTgmA5alH
+vtUlci6dZdO801h5vOesbAJplXCEyw==
+-----END AGE ENCRYPTED FILE-----