From 78ecdd278007bf38c788450259767bd7098b3e1a Mon Sep 17 00:00:00 2001 From: oddlama Date: Sat, 20 Jan 2024 03:02:26 +0100 Subject: [PATCH] feat: add paperless and radicale backups to hetzner --- hosts/sire/guests/paperless.nix | 27 ++++++ hosts/sire/guests/samba.nix | 34 +------- hosts/ward/guests/radicale.nix | 6 ++ hosts/ward/guests/vaultwarden.nix | 32 +------ hosts/ward/net.nix | 6 -- modules/backups.nix | 79 ++++++++++++++++++ modules/default.nix | 1 + .../restic-encryption-password.age | 10 +++ .../sire-paperless/restic-ssh-privkey.age | Bin 0 -> 807 bytes .../restic-encryption-password.age | Bin 0 -> 378 bytes .../ward-radicale/restic-ssh-privkey.age | Bin 0 -> 788 bytes secrets/global.nix.age | Bin 1731 -> 1870 bytes 12 files changed, 128 insertions(+), 67 deletions(-) create mode 100644 modules/backups.nix create mode 100644 secrets/generated/sire-paperless/restic-encryption-password.age create mode 100644 secrets/generated/sire-paperless/restic-ssh-privkey.age create mode 100644 secrets/generated/ward-radicale/restic-encryption-password.age create mode 100644 secrets/generated/ward-radicale/restic-ssh-privkey.age diff --git a/hosts/sire/guests/paperless.nix b/hosts/sire/guests/paperless.nix index 83c4988..d399398 100644 --- a/hosts/sire/guests/paperless.nix +++ b/hosts/sire/guests/paperless.nix @@ -1,10 +1,12 @@ { config, + lib, nodes, ... }: let sentinelCfg = nodes.sentinel.config; paperlessDomain = "paperless.${sentinelCfg.repo.secrets.local.personalDomain}"; + paperlessBackupDir = "/var/cache/paperless-backup"; in { microvm.mem = 1024 * 6; microvm.vcpu = 8; @@ -92,4 +94,29 @@ in { }; systemd.services.paperless.serviceConfig.RestartSec = "600"; # Retry every 10 minutes + + systemd.tmpfiles.settings."10-paperless".${paperlessBackupDir}.d = { + inherit (config.services.paperless) user; + mode = "0700"; + }; + + systemd.services.paperless-backup = let + cfg = config.systemd.services.paperless-consumer; + in { + description = "Paperless documents backup"; + serviceConfig = lib.recursiveUpdate cfg.serviceConfig { + ExecStart = "${config.services.paperless.package}/bin/paperless-ngx document_exporter -na -nt -f -d ${paperlessBackupDir}"; + ReadWritePaths = cfg.serviceConfig.ReadWritePaths ++ [paperlessBackupDir]; + Restart = "no"; + Type = "oneshot"; + }; + inherit (cfg) environment; + requiredBy = ["restic-backups-storage-box-dusk.service"]; + }; + + backups.storageBoxes.dusk = { + subuser = "paperless"; + user = "paperless"; + paths = [paperlessBackupDir]; + }; } diff --git a/hosts/sire/guests/samba.nix b/hosts/sire/guests/samba.nix index e790548..9a618e3 100644 --- a/hosts/sire/guests/samba.nix +++ b/hosts/sire/guests/samba.nix @@ -246,7 +246,7 @@ in { "/shares/users/${user}-paperless".d = { user = "paperless"; group = "paperless"; - mode = "0750"; + mode = "0550"; }; "/paperless/consume/${user}".d = { user = "paperless"; @@ -347,37 +347,9 @@ in { } // lib.mapAttrs (_: cfg: {gid = cfg.id;}) (smbUsers // smbGroups); - # Backups - # ======================================================================== - - age.secrets.restic-encryption-password.generator.script = "alnum"; - age.secrets.restic-ssh-privkey.generator.script = "ssh-ed25519"; - - services.restic.backups.main = { - hetznerStorageBox = let - box = config.repo.secrets.global.hetzner.storageboxes.dusk; - in { - enable = true; - inherit (box) mainUser; - inherit (box.users.samba) subUid path; - sshAgeSecret = "restic-ssh-privkey"; - }; - - # We need to backup stuff from other users, so run as root. + backups.storageBoxes.dusk = { + subuser = "samba"; user = "root"; - timerConfig = { - OnCalendar = "06:15"; - RandomizedDelaySec = "3h"; - Persistent = true; - }; - initialize = true; - passwordFile = config.age.secrets.restic-encryption-password.path; paths = ["/bunker"]; - pruneOpts = [ - "--keep-daily 14" - "--keep-weekly 7" - "--keep-monthly 12" - "--keep-yearly 75" - ]; }; } diff --git a/hosts/ward/guests/radicale.nix b/hosts/ward/guests/radicale.nix index 20c9e2f..889e998 100644 --- a/hosts/ward/guests/radicale.nix +++ b/hosts/ward/guests/radicale.nix @@ -82,4 +82,10 @@ in { }; systemd.services.radicale.serviceConfig.RestartSec = "600"; # Retry every 10 minutes + + backups.storageBoxes.dusk = { + subuser = "radicale"; + user = "radicale"; + paths = ["/var/lib/radicale"]; + }; } diff --git a/hosts/ward/guests/vaultwarden.nix b/hosts/ward/guests/vaultwarden.nix index 5c04682..bfefd13 100644 --- a/hosts/ward/guests/vaultwarden.nix +++ b/hosts/ward/guests/vaultwarden.nix @@ -74,7 +74,6 @@ in { smtpSecurity = "force_tls"; smtpPort = 465; }; - #backupDir = "/data/backup"; environmentFile = config.age.secrets.vaultwarden-env.path; }; @@ -85,36 +84,9 @@ in { RestartSec = "600"; # Retry every 10 minutes }; - # Backups - # ======================================================================== - - age.secrets.restic-encryption-password.generator.script = "alnum"; - age.secrets.restic-ssh-privkey.generator.script = "ssh-ed25519"; - - services.restic.backups.main = { - hetznerStorageBox = let - box = config.repo.secrets.global.hetzner.storageboxes.dusk; - in { - enable = true; - inherit (box) mainUser; - inherit (box.users.vaultwarden) subUid path; - sshAgeSecret = "restic-ssh-privkey"; - }; - + backups.storageBoxes.dusk = { + subuser = "vaultwarden"; user = "vaultwarden"; - timerConfig = { - OnCalendar = "06:15"; - RandomizedDelaySec = "3h"; - Persistent = true; - }; - initialize = true; - passwordFile = config.age.secrets.restic-encryption-password.path; paths = [config.services.vaultwarden.backupDir]; - pruneOpts = [ - "--keep-daily 14" - "--keep-weekly 7" - "--keep-monthly 12" - "--keep-yearly 75" - ]; }; } diff --git a/hosts/ward/net.nix b/hosts/ward/net.nix index 45034dd..86f6fbc 100644 --- a/hosts/ward/net.nix +++ b/hosts/ward/net.nix @@ -104,12 +104,6 @@ in { masquerade = true; }; - # Rule needed to allow local-vms wireguard traffic - lan-to-local = { - from = ["lan"]; - to = ["local"]; - }; - outbound = { from = ["lan"]; to = ["lan" "untrusted"]; diff --git a/modules/backups.nix b/modules/backups.nix new file mode 100644 index 0000000..3d8d7ef --- /dev/null +++ b/modules/backups.nix @@ -0,0 +1,79 @@ +{ + config, + lib, + ... +}: let + inherit + (lib) + attrValues + flip + mkIf + mkMerge + mkOption + types + ; +in { + options.backups.storageBoxes = mkOption { + description = "Backups to Hetzner Storage Boxes using restic"; + default = {}; + type = types.attrsOf (types.submodule (submod: { + options = { + name = mkOption { + description = "The name of the storage box to backup to. The box must be defined in the global secrets. Defaults to the attribute name."; + default = submod.config._module.args.name; + type = types.str; + }; + + subuser = mkOption { + description = "The name of the storage box subuser as defined in the global secrets, mapping this user to a subuser id."; + type = types.str; + }; + + user = mkOption { + description = "The user as which restic should run."; + type = types.str; + }; + + paths = mkOption { + description = "The paths to backup."; + type = types.listOf types.path; + }; + }; + })); + }; + + config = mkIf (config.backups.storageBoxes != {}) { + age.secrets.restic-encryption-password.generator.script = "alnum"; + age.secrets.restic-ssh-privkey.generator.script = "ssh-ed25519"; + + services.restic.backups = mkMerge (flip map (attrValues config.backups.storageBoxes) + (boxCfg: { + "storage-box-${boxCfg.name}" = { + hetznerStorageBox = let + box = config.repo.secrets.global.hetzner.storageboxes.${boxCfg.name}; + in { + enable = true; + inherit (box) mainUser; + inherit (box.users.${boxCfg.subuser}) subUid path; + sshAgeSecret = "restic-ssh-privkey"; + }; + + # We need to backup stuff from other users, so run as root. + inherit (boxCfg) user paths; + timerConfig = { + OnCalendar = "06:15"; + RandomizedDelaySec = "3h"; + Persistent = true; + }; + initialize = true; + passwordFile = config.age.secrets.restic-encryption-password.path; + pruneOpts = [ + "--keep-daily 14" + "--keep-weekly 7" + "--keep-monthly 12" + "--keep-yearly 75" + ]; + }; + })); + }; +} diff --git a/modules/default.nix b/modules/default.nix index 337663b..46bf981 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -27,6 +27,7 @@ ./config/users.nix ./acme-wildcard.nix + ./backups.nix ./deterministic-ids.nix ./distributed-config.nix ./kanidm.nix diff --git a/secrets/generated/sire-paperless/restic-encryption-password.age b/secrets/generated/sire-paperless/restic-encryption-password.age new file mode 100644 index 0000000..2abdffe --- /dev/null +++ b/secrets/generated/sire-paperless/restic-encryption-password.age @@ -0,0 +1,10 @@ +age-encryption.org/v1 +-> X25519 yVph6Vasomk5FHVnvYCP9EIWNTDUV9VLjT3tLWO6V2U +jiNBCi4nwRT5BraQtwlF1NLEC8ed6kEvg1D6XKT808g +-> piv-p256 xqSe8Q Aj4vwj2hYJUA4EhprNegsyBlBERNaC4JOQSYFDTA4JPL +EEqBpXe1tPZfngPeuZ4I/cUkFhoG5Z4P2bjUQ8xIRoM +-> n-grease g)hu +9qNhz-| +oXZBv+XwPtRbS7tlKyekxGJAhn1Qje/y2nAjjFg+8Ap+oxwpdMwJ4iFG2yxsWdew +XcdgOj/61A +--- ZSNOvD2AbvfGqKJ+bGh4n6EZOuiIs0KsB+CDGjmojJY +œ(™ŽôÄI .MþülB‹'ùбð¶B½ˆ<±`¿ºE¯iÐ[¾&%†„Kø‘@"?ÿ±§§ˆAà¼ÁëXn"£!ñ üTØÅ \ No newline at end of file diff --git a/secrets/generated/sire-paperless/restic-ssh-privkey.age b/secrets/generated/sire-paperless/restic-ssh-privkey.age new file mode 100644 index 0000000000000000000000000000000000000000..5cc9c4635236634274b88acf4954bc4de3891c5f GIT binary patch literal 807 zcmV+?1K9jwXJsvAZewzJaCB*JZZ2R_pdUGpgQffzJLpVk;LvspHMPgAadPZbMFKAmZFKt#rc1~F7|Tv{Dq= zpy|*r_}K70HPhaa<=NZ3;)qG%Lr7n_%L@f~3wU6g{^hsE8hrAU5UZHuo;t~-mc=(X%EM@^P5F@5ug#y;`d~h$KdPQ!fVM0Q8HFr=0MyL0kNa8#+mTk02V7yH zXDX-}ZlQ3B(vVGagz7ynGuRKPHB5Q9I`Xea2nM;=p1$A zKf*|i#J*K)v`Y9Yn~ckHPd_arjqr)WeSF-zLPDLEJ4sloPx3a!edRu+uNKg@Lvi@a zg_eRNx!de8Qap7i>PnYm9>S)M1&-{3>d*VC!$+#DiyUn7wh(RxF9cjaiZcB-UG!OG zajSF{m+)Hk@QqO3AJE78j@F)ABmSCMXdQEFmwDwjo4a=B5mn;)01uC9W=Syfb2dAeIf zvZrgMV{uq{R+)>Bv8Q`LacZS=MV@a+PM}*@MrKlwSw2^)isZ$pI*XsHb*u{|8TIuy zu3?_>R;>B)PkqrPTT%nsY}ZRvGoJ0YTAj6cZ#P4AH*04@Rz_-9V?kIsG;DBiZBYtnS!qgGO*CdmO-e9oba^;zN_uut zc~V(KRA)(PcXT*NL3v|KMlnJ&Sw#vhJ|J*ub}eu+H8vo4aZ_bDQ6NESR8w$kZ)|r# zPcc__SW9?IYEMToZb?WvQCe+qO>uWIF*Igrbu~G1VK53aN>@!uFk~}PLN!r$NHb7D zGeR|OFEv6jMrLkxZ*pX4XERtsY%_OBVQUI4J|H$mEoX9NVRK~)VRJHdR#HJmS#U)% zb4pfKdTv)pZ!=FzZ#G#-OJQMIWoHU4EiE8Od1G#3Zdze_M`|!~MKw@GP-Qnxd2mB- zXk;;HOhs=-Sx-o4T2C}oO=k-75Fq<3CkX%Dzw#xB3YJ|LlmYDW3h{{NCwm3`?xrWo z;sL7OL8{Qc%Oq^gx)()l>v=C#_VQB{u^`lEHtr#1!uUSs7dX_Nn)Lv{JQy0{KcLlv zGoR$pb%=(l2X~5_$X9z1)X{S<%E~o>pH7NX?p6_D=?WNW@~^2373qZj4O_G-By_Zm zy%HG}-Jr}qC+)vX?#4S(QzB}SBr!c1L&N^l3WK9*q;4wX^bKthB_i5n@`at zE8h{Rb(#jzR_&>ocCJM?Gak2~SBnj{4Fh1nIcy=X)t)(#iH2Bm*gD-R{TIbBSp z`TCVtTQ8heDJxrli>B?`C3Va-<^ypV0z;&Xu#wwT>iQxyR-ZlKY3@?@0r>T!*fY9= SDIN52sm@AXR(bj@j$fOvL{Ve_ literal 0 HcmV?d00001 diff --git a/secrets/global.nix.age b/secrets/global.nix.age index 5adc6c0fe343fc4e38f43c19108b3200b12cbc6d..4cc52dd0622be259fbeba8173b83d17dfd9e6741 100644 GIT binary patch delta 1859 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 N&ak5~Ks&JZnnH~O9!CHG