{ pkgs, lib, ... }: let # sops-encrypted secrets (single file holds all service secrets). secretsFile = ../../secrets/secrets.yaml; # Root-only runtime files restic reads from (0600 root). runtimeDir = "/var/lib/restic"; passwordFile = "/var/lib/restic/.password"; environmentFile = "/var/lib/restic/environment"; # Backblaze B2 backend, per restic docs: b2:bucketname. repo = "b2:hetzner-severijnse"; # Materialize the restic password and B2 credentials from sops into # root-only files, so secrets are never world-readable in the Nix store. writeSecrets = pkgs.writeShellScript "restic-write-secrets" '' set -euo pipefail mkdir -p ${runtimeDir} ${pkgs.sops}/bin/sops \ --decrypt --extract '["restic_password"]' \ --input-type yaml --output-type yaml ${secretsFile} \ | tr -d '\n' > "${passwordFile}" chmod 0600 "${passwordFile}" : > "${environmentFile}" chmod 0600 "${environmentFile}" ${pkgs.sops}/bin/sops --decrypt --input-type yaml --output-type yaml ${secretsFile} | ${pkgs.gnused}/bin/sed -nE \ 's/^b2_key_id: (.*)/B2_ACCOUNT_ID=\1/p; s/^b2_application_key: (.*)/B2_ACCOUNT_KEY=\1/p' \ >> "${environmentFile}" ''; in { systemd.services.restic-password = { description = "Materialize restic repository password and B2 credentials from sops"; wantedBy = ["multi-user.target"]; # The age key lives in /etc/age/keys.txt; the service must know where it is # and needs a HOME for age to report its user config directory. environment.SOPS_AGE_KEY_FILE = "/etc/age/keys.txt"; serviceConfig = { Type = "oneshot"; Environment = ["HOME=/root"]; ExecStart = "${writeSecrets}"; }; }; # B2 credentials are supplied via environmentFile (B2_ACCOUNT_ID / B2_ACCOUNT_KEY), # matching the official module example which combines `repository` and `environmentFile`. services.restic.backups.localbackup = { repository = "b2:hetzner-severijnse"; environmentFile = environmentFile; passwordFile = passwordFile; initialize = true; paths = [ "/home/admin" "/var/lib/postgresql" "/var/lib/gitea" "/var/lib/caddy" "/var/lib/virtualcam" "/var/lib/stalwart" "/var/lib/bulwark" "/var/lib/coredns" "/etc/nixos" ]; exclude = [ "/home/admin/backups" "/home/admin/.opencode" "/home/admin/.local" "/home/admin/.npm" "/home/admin/.config" "*.log" "*.log.*" "**/.cache" ]; timerConfig = { OnCalendar = "Mon *-*-* 03:00:00"; Persistent = true; RandomizedDelaySec = "15m"; }; pruneOpts = [ "--keep-daily 7" "--keep-weekly 4" "--keep-monthly 6" ]; runCheck = true; }; # The backup must never run before the secrets exist. systemd.services."restic-backups-localbackup" = { requires = ["restic-password.service"]; after = ["restic-password.service"]; }; }