This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

modules.backup: encrypted repos in per-machine storage box sub-accounts

Every machine now backs up into its own Hetzner storage box sub-account
(isolation: one machine's ssh key can't read or delete another's
archives), into ./borg in the sub-account's home. lass.backup.user replaces
lass.backup.repo and enables the job; host, port 23 and the box's host key
are module defaults, so a machine's backup.nix is only the sub-account.

Repos are always encrypted (repokey-blake2). The passphrase comes from a
per-machine clan vars generator borgbackup-repokey; it lives in the
password store, so passphrase + repo are enough to restore. The borgbackup
ssh key generator is unchanged.

neoprism moves to a new repo on u550643-sub1 (doInit creates it); the old
unencrypted repo u550643@...:/./neoprism is no longer referenced and is
removed by hand. Its paths, excludes and hooks are unchanged. The job stays
`hetzner`: with fresh repos nothing gains from a rename, and the unit name
is used for the postBackup hooks.

The timers get a fixed random delay of up to 1h after 03:00, so the
machines sharing the box don't all start at once.

+37 -21
+36 -10
modules/backup.nix
··· 8 8 cfg = config.lass.backup; 9 9 states = lib.attrValues cfg.state; 10 10 key = config.clan.core.vars.generators.borgbackup.files."borgbackup.ssh.id25519".path; 11 + passphrase = config.clan.core.vars.generators.borgbackup-repokey.files.passphrase.path; 11 12 12 13 # Runs every state's hook in its own script, keeps going when one fails 13 14 # and fails at the end if any did. ··· 30 31 in 31 32 { 32 33 options.lass.backup = { 33 - repo = lib.mkOption { 34 + user = lib.mkOption { 34 35 type = lib.types.nullOr lib.types.str; 35 36 default = null; 36 - example = "user@backup.example.com:/./machine"; 37 + example = "u550643-sub1"; 37 38 description = '' 38 - Borg repository the machine backs up to. Without it no backup job is 39 - generated, so modules can declare their state everywhere. 39 + Storage box (sub-)account the machine backs up to, into ./borg in its 40 + home. Without it no backup job is generated, so modules can declare 41 + their state everywhere. 40 42 ''; 43 + }; 44 + 45 + host = lib.mkOption { 46 + type = lib.types.str; 47 + default = "u550643.your-storagebox.de"; 41 48 }; 42 49 43 50 sshPort = lib.mkOption { 44 51 type = lib.types.port; 45 - default = 22; 52 + default = 23; 46 53 }; 47 54 48 55 knownHosts = lib.mkOption { 49 56 type = lib.types.lines; 57 + default = '' 58 + [u550643.your-storagebox.de]:23 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIICf9svRenC/PLKIL9nk6K/pxQgoiFC41wTNvoIncOxs 59 + ''; 50 60 description = "known_hosts lines pinning the host key of the repo's server."; 51 61 }; 52 62 ··· 103 113 }; 104 114 }; 105 115 106 - config = lib.mkIf (cfg.repo != null) { 116 + config = lib.mkIf (cfg.user != null) { 107 117 clan.core.vars.generators.borgbackup = { 108 118 files."borgbackup.ssh.id25519" = { }; 109 119 files."borgbackup.ssh.id25519.pub".secret = false; ··· 116 126 ''; 117 127 }; 118 128 129 + clan.core.vars.generators.borgbackup-repokey = { 130 + files.passphrase = { }; 131 + runtimeInputs = [ pkgs.openssl ]; 132 + script = '' 133 + openssl rand -hex 32 > "$out"/passphrase 134 + ''; 135 + }; 136 + 119 137 systemd.tmpfiles.rules = [ "d /var/backup 0700 root root -" ]; 120 138 121 - # Archives are named and pruned by <hostname>-<job>; another job name 122 - # would leave the existing archives unpruned. 139 + # Archives are named and pruned by <hostname>-<job>. 123 140 services.borgbackup.jobs.hetzner = { 124 - inherit (cfg) repo; 141 + repo = "${cfg.user}@${cfg.host}:./borg"; 125 142 paths = lib.unique ([ "/var/backup" ] ++ lib.concatMap (state: state.folders) states); 126 143 exclude = lib.unique ([ "*.pyc" ] ++ lib.concatMap (state: state.exclude) states); 127 144 readWritePaths = lib.unique ( 128 145 [ "/var/backup" ] ++ lib.concatMap (state: state.readWritePaths) states 129 146 ); 130 147 preHook = "${runHooks "preBackup"}"; 131 - encryption.mode = "none"; 148 + encryption = { 149 + mode = "repokey-blake2"; 150 + passCommand = "cat ${passphrase}"; 151 + }; 132 152 compression = "auto,zstd"; 133 153 startAt = "*-*-* 03:00:00"; 134 154 environment.BORG_RSH = "ssh -oPort=${toString cfg.sshPort} -oUserKnownHostsFile=${pkgs.writeText "backup-known-hosts" cfg.knownHosts} -i ${key}"; ··· 138 158 weekly = 4; 139 159 monthly = 0; 140 160 }; 161 + }; 162 + 163 + # spreads the machines sharing the storage box over 03:00-04:00 164 + systemd.timers.borgbackup-job-hetzner.timerConfig = { 165 + RandomizedDelaySec = "1h"; 166 + FixedRandomDelay = true; 141 167 }; 142 168 143 169 systemd.services.borgbackup-job-hetzner.serviceConfig.ExecStopPost = lib.mkIf (lib.any (
+1 -11
machines/neoprism/backup.nix
··· 1 - { 2 - # Hetzner storage box; what gets backed up is declared next to each service 3 - # (lass.backup.state). 4 - lass.backup = { 5 - repo = "u550643@u550643.your-storagebox.de:/./neoprism"; 6 - sshPort = 23; 7 - knownHosts = '' 8 - [u550643.your-storagebox.de]:23 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIICf9svRenC/PLKIL9nk6K/pxQgoiFC41wTNvoIncOxs 9 - ''; 10 - }; 11 - } 1 + { lass.backup.user = "u550643-sub1"; }