@jaspermayone.com's dotfiles

disabled: backups

+636
+62
hosts/alastor/configuration.nix
··· 9 9 ../../modules/knot/sync.nix 10 10 ../../modules/bluesky-pds/default.nix 11 11 ../../modules/atuin-server 12 + ../../modules/restic 12 13 inputs.tangled.nixosModules.knot 13 14 ]; 14 15 ··· 130 131 owner = "jsp"; 131 132 mode = "400"; 132 133 }; 134 + # Restic backup secrets (uncomment when ready) 135 + # "restic/env" = { 136 + # file = ../../secrets/restic/env.age; 137 + # mode = "400"; 138 + # }; 139 + # "restic/repo" = { 140 + # file = ../../secrets/restic/repo.age; 141 + # mode = "400"; 142 + # }; 143 + # "restic/password" = { 144 + # file = ../../secrets/restic/password.age; 145 + # mode = "400"; 146 + # }; 133 147 }; 134 148 135 149 # FRP tunnel server ··· 212 226 systemd.services.caddy.serviceConfig.EnvironmentFile = config.age.secrets.cloudflare-credentials.path; 213 227 214 228 networking.firewall.allowedTCPPorts = [ 80 443 2222 ]; # 2222 for knot SSH 229 + 230 + # Castle backup system (disabled for now - enable when secrets are ready) 231 + # To enable: 232 + # 1. Create secrets: agenix -e secrets/restic/env.age (B2_ACCOUNT_ID=..., B2_ACCOUNT_KEY=...) 233 + # 2. Create secrets: agenix -e secrets/restic/repo.age (b2:bucket-name:/backup-path) 234 + # 3. Create secrets: agenix -e secrets/restic/password.age (repository encryption password) 235 + # 4. Uncomment the age.secrets above 236 + # 5. Uncomment castle.backup below 237 + # 238 + # castle.backup = { 239 + # enable = true; 240 + # services = { 241 + # knot = { 242 + # paths = [ "/var/lib/knot" "/home/git" ]; 243 + # exclude = [ "*.log" ".git" ]; 244 + # tags = [ "service:knot" "type:git" ]; 245 + # preBackup = '' 246 + # systemctl stop tangled-knot || true 247 + # ''; 248 + # postBackup = '' 249 + # systemctl start tangled-knot || true 250 + # ''; 251 + # }; 252 + # pds = { 253 + # paths = [ "/var/lib/pds" ]; 254 + # exclude = [ "*.log" "node_modules" ]; 255 + # tags = [ "service:pds" "type:atproto" ]; 256 + # preBackup = '' 257 + # systemctl stop bluesky-pds || true 258 + # ''; 259 + # postBackup = '' 260 + # systemctl start bluesky-pds || true 261 + # ''; 262 + # }; 263 + # atuin = { 264 + # paths = [ "/var/lib/atuin-server" ]; 265 + # exclude = [ "*.log" ]; 266 + # tags = [ "service:atuin" "type:sqlite" ]; 267 + # preBackup = '' 268 + # sqlite3 /var/lib/atuin-server/atuin.db "PRAGMA wal_checkpoint(TRUNCATE);" || true 269 + # systemctl stop atuin-server || true 270 + # ''; 271 + # postBackup = '' 272 + # systemctl start atuin-server || true 273 + # ''; 274 + # }; 275 + # }; 276 + # }; 215 277 216 278 # Automatic garbage collection 217 279 nix.gc = {
+2
hosts/remus/default.nix
··· 26 26 27 27 # Laptop-specific nix packages 28 28 environment.systemPackages = with pkgs; [ 29 + coreutils 30 + libyaml 29 31 yubikey-manager 30 32 ]; 31 33
+361
modules/restic/cli.nix
··· 1 + # castle CLI - Hogwarts infrastructure management 2 + # Credit: Based on implementation by krn (https://github.com/taciturnaxolotl/dots) 3 + # 4 + # Commands: 5 + # sudo castle - Interactive menu 6 + # sudo castle backup - Backup management submenu 7 + # sudo castle backup status - Show backup status for all services 8 + # sudo castle backup list - List snapshots 9 + # sudo castle backup run - Trigger manual backup 10 + # sudo castle backup restore - Interactive restore wizard 11 + # sudo castle backup dr - Disaster recovery mode 12 + # 13 + # Future modules: 14 + # castle status - Service health dashboard 15 + # castle secrets - Manage agenix secrets 16 + # castle deploy - Remote deployment tools 17 + # castle logs - Service log viewer 18 + 19 + { config, lib, pkgs, ... }: 20 + 21 + let 22 + cfg = config.castle.backup; 23 + 24 + # Get all configured backup services 25 + allBackupServices = lib.attrNames cfg.services; 26 + 27 + # Generate manifest for disaster recovery 28 + backupManifest = pkgs.writeText "backup-manifest.json" (builtins.toJSON { 29 + version = 1; 30 + generated = "nixos-rebuild"; 31 + services = lib.mapAttrs (name: backupCfg: { 32 + paths = backupCfg.paths; 33 + exclude = backupCfg.exclude or []; 34 + tags = backupCfg.tags or []; 35 + }) cfg.services; 36 + }); 37 + 38 + castleCliScript = pkgs.writeShellScript "castle" '' 39 + set -e 40 + 41 + # Must be run as root 42 + if [ "$(id -u)" -ne 0 ]; then 43 + echo "Error: castle must be run as root (use sudo)" 44 + exit 1 45 + fi 46 + 47 + # Colors via gum 48 + style() { ${pkgs.gum}/bin/gum style "$@"; } 49 + confirm() { ${pkgs.gum}/bin/gum confirm "$@"; } 50 + choose() { ${pkgs.gum}/bin/gum choose "$@"; } 51 + input() { ${pkgs.gum}/bin/gum input "$@"; } 52 + spin() { ${pkgs.gum}/bin/gum spin "$@"; } 53 + 54 + # Load B2 credentials for backup commands 55 + load_backup_env() { 56 + set -a 57 + source ${config.age.secrets."restic/env".path} 58 + set +a 59 + } 60 + 61 + # Restic wrapper with secrets 62 + restic_cmd() { 63 + ${pkgs.restic}/bin/restic \ 64 + --repository-file ${config.age.secrets."restic/repo".path} \ 65 + --password-file ${config.age.secrets."restic/password".path} \ 66 + "$@" 67 + } 68 + 69 + # Available backup services 70 + BACKUP_SERVICES="${lib.concatStringsSep " " allBackupServices}" 71 + MANIFEST="${backupManifest}" 72 + 73 + # ========== BACKUP COMMANDS ========== 74 + 75 + backup_status() { 76 + load_backup_env 77 + style --bold --foreground 212 "Backup Status" 78 + echo 79 + 80 + for svc in $BACKUP_SERVICES; do 81 + latest=$(restic_cmd snapshots --tag "service:$svc" --json --latest 1 2>/dev/null | ${pkgs.jq}/bin/jq -r '.[0] // empty') 82 + 83 + if [ -n "$latest" ]; then 84 + time=$(echo "$latest" | ${pkgs.jq}/bin/jq -r '.time' | cut -d'T' -f1) 85 + hostname=$(echo "$latest" | ${pkgs.jq}/bin/jq -r '.hostname') 86 + style --foreground 35 "✓ $svc" 87 + style --foreground 117 " Last backup: $time on $hostname" 88 + else 89 + style --foreground 214 "! $svc" 90 + style --foreground 117 " No backups found" 91 + fi 92 + done 93 + } 94 + 95 + backup_list() { 96 + load_backup_env 97 + style --bold --foreground 212 "List Snapshots" 98 + echo 99 + 100 + svc=$(echo "$BACKUP_SERVICES" | tr ' ' '\n' | choose --header "Select service:") 101 + 102 + if [ -z "$svc" ]; then 103 + style --foreground 196 "No service selected" 104 + exit 1 105 + fi 106 + 107 + style --foreground 117 "Snapshots for $svc:" 108 + echo 109 + 110 + restic_cmd snapshots --tag "service:$svc" --compact 111 + } 112 + 113 + backup_run() { 114 + style --bold --foreground 212 "Manual Backup" 115 + echo 116 + 117 + svc=$(echo "all $BACKUP_SERVICES" | tr ' ' '\n' | choose --header "Select service to backup:") 118 + 119 + if [ -z "$svc" ]; then 120 + style --foreground 196 "No service selected" 121 + exit 1 122 + fi 123 + 124 + if [ "$svc" = "all" ]; then 125 + for s in $BACKUP_SERVICES; do 126 + style --foreground 117 "Backing up $s..." 127 + systemctl start "restic-backups-$s.service" || style --foreground 214 "! Failed to backup $s" 128 + done 129 + else 130 + style --foreground 117 "Backing up $svc..." 131 + systemctl start "restic-backups-$svc.service" 132 + fi 133 + 134 + style --foreground 35 "✓ Backup triggered" 135 + } 136 + 137 + backup_restore() { 138 + load_backup_env 139 + style --bold --foreground 212 "Restore Wizard" 140 + echo 141 + 142 + svc=$(echo "$BACKUP_SERVICES" | tr ' ' '\n' | choose --header "Select service to restore:") 143 + 144 + if [ -z "$svc" ]; then 145 + style --foreground 196 "No service selected" 146 + exit 1 147 + fi 148 + 149 + style --foreground 117 "Fetching snapshots for $svc..." 150 + snapshots=$(restic_cmd snapshots --tag "service:$svc" --json 2>/dev/null) 151 + 152 + if [ "$(echo "$snapshots" | ${pkgs.jq}/bin/jq 'length')" = "0" ]; then 153 + style --foreground 196 "No snapshots found for $svc" 154 + exit 1 155 + fi 156 + 157 + snapshot_list=$(echo "$snapshots" | ${pkgs.jq}/bin/jq -r '.[] | "\(.short_id) - \(.time | split("T")[0]) - \(.paths | join(", "))"') 158 + 159 + selected=$(echo "$snapshot_list" | choose --header "Select snapshot:") 160 + snapshot_id=$(echo "$selected" | cut -d' ' -f1) 161 + 162 + if [ -z "$snapshot_id" ]; then 163 + style --foreground 196 "No snapshot selected" 164 + exit 1 165 + fi 166 + 167 + restore_mode=$(choose --header "Restore mode:" "Inspect (restore to /tmp)" "In-place (DANGEROUS)") 168 + 169 + case "$restore_mode" in 170 + "Inspect"*) 171 + target="/tmp/restore-$svc-$snapshot_id" 172 + mkdir -p "$target" 173 + 174 + style --foreground 117 "Restoring to $target..." 175 + restic_cmd restore "$snapshot_id" --target "$target" 176 + 177 + style --foreground 35 "✓ Restored to $target" 178 + style --foreground 117 " Inspect files, then copy what you need" 179 + ;; 180 + 181 + "In-place"*) 182 + style --foreground 196 --bold "⚠ WARNING: This will overwrite existing data!" 183 + echo 184 + 185 + if ! confirm "Stop $svc and restore data?"; then 186 + style --foreground 214 "Restore cancelled" 187 + exit 0 188 + fi 189 + 190 + style --foreground 117 "Stopping $svc..." 191 + systemctl stop "$svc" 2>/dev/null || true 192 + 193 + style --foreground 117 "Restoring snapshot $snapshot_id..." 194 + restic_cmd restore "$snapshot_id" --target / 195 + 196 + style --foreground 117 "Starting $svc..." 197 + systemctl start "$svc" 198 + 199 + style --foreground 35 "✓ Restore complete" 200 + ;; 201 + esac 202 + } 203 + 204 + backup_dr() { 205 + load_backup_env 206 + style --bold --foreground 196 "⚠ DISASTER RECOVERY MODE" 207 + echo 208 + style --foreground 214 "This will restore ALL services from backup." 209 + style --foreground 214 "Only use this on a fresh NixOS install." 210 + echo 211 + 212 + if ! confirm "Continue with full disaster recovery?"; then 213 + style --foreground 117 "Cancelled" 214 + exit 0 215 + fi 216 + 217 + style --foreground 117 "Reading backup manifest..." 218 + 219 + for svc in $BACKUP_SERVICES; do 220 + style --foreground 212 "Restoring $svc..." 221 + 222 + snapshot_id=$(restic_cmd snapshots --tag "service:$svc" --json --latest 1 2>/dev/null | ${pkgs.jq}/bin/jq -r '.[0].short_id // empty') 223 + 224 + if [ -z "$snapshot_id" ]; then 225 + style --foreground 214 " ! No snapshots found, skipping" 226 + continue 227 + fi 228 + 229 + systemctl stop "$svc" 2>/dev/null || true 230 + restic_cmd restore "$snapshot_id" --target / 231 + systemctl start "$svc" 2>/dev/null || true 232 + 233 + style --foreground 35 " ✓ Restored from $snapshot_id" 234 + done 235 + 236 + echo 237 + style --foreground 35 --bold "✓ Disaster recovery complete" 238 + } 239 + 240 + backup_menu() { 241 + style --bold --foreground 212 "Backup Management" 242 + echo 243 + 244 + action=$(choose \ 245 + "Status - Show backup status" \ 246 + "List - Browse snapshots" \ 247 + "Run - Trigger manual backup" \ 248 + "Restore - Restore from backup" \ 249 + "DR - Disaster recovery mode" \ 250 + "← Back") 251 + 252 + case "$action" in 253 + Status*) backup_status ;; 254 + List*) backup_list ;; 255 + Run*) backup_run ;; 256 + Restore*) backup_restore ;; 257 + DR*) backup_dr ;; 258 + *) main_menu ;; 259 + esac 260 + } 261 + 262 + # ========== MAIN MENU ========== 263 + 264 + main_menu() { 265 + style --bold --foreground 212 "🏰 Castle - Hogwarts Infrastructure" 266 + echo 267 + 268 + action=$(choose \ 269 + "Backup - Manage backups and restores" \ 270 + "Exit") 271 + 272 + case "$action" in 273 + Backup*) backup_menu ;; 274 + *) exit 0 ;; 275 + esac 276 + } 277 + 278 + show_help() { 279 + echo "Usage: castle [command] [subcommand]" 280 + echo 281 + echo "Commands:" 282 + echo " backup Backup management menu" 283 + echo " backup status Show backup status for all services" 284 + echo " backup list List snapshots" 285 + echo " backup run Trigger manual backup" 286 + echo " backup restore Interactive restore wizard" 287 + echo " backup dr Disaster recovery mode" 288 + echo 289 + echo "Run without arguments for interactive menu." 290 + echo 291 + echo "Note: Must be run as root (use sudo)" 292 + } 293 + 294 + # ========== MAIN ========== 295 + 296 + case "''${1:-}" in 297 + backup) 298 + case "''${2:-}" in 299 + status) backup_status ;; 300 + list) backup_list ;; 301 + run) backup_run ;; 302 + restore) backup_restore ;; 303 + dr|disaster-recovery) backup_dr ;; 304 + "") backup_menu ;; 305 + *) 306 + style --foreground 196 "Unknown backup command: $2" 307 + exit 1 308 + ;; 309 + esac 310 + ;; 311 + --help|-h) 312 + show_help 313 + ;; 314 + "") 315 + main_menu 316 + ;; 317 + *) 318 + style --foreground 196 "Unknown command: $1" 319 + echo "Run 'castle --help' for usage." 320 + exit 1 321 + ;; 322 + esac 323 + ''; 324 + 325 + castleCli = pkgs.stdenv.mkDerivation { 326 + pname = "castle"; 327 + version = "1.0.0"; 328 + 329 + dontUnpack = true; 330 + 331 + nativeBuildInputs = [ pkgs.installShellFiles ]; 332 + 333 + bashCompletionSrc = ./completions/castle.bash; 334 + zshCompletionSrc = ./completions/castle.zsh; 335 + fishCompletionSrc = ./completions/castle.fish; 336 + 337 + installPhase = '' 338 + mkdir -p $out/bin 339 + cp ${castleCliScript} $out/bin/castle 340 + chmod +x $out/bin/castle 341 + 342 + # Install completions 343 + installShellCompletion --bash --name castle $bashCompletionSrc 344 + installShellCompletion --zsh --name _castle $zshCompletionSrc 345 + installShellCompletion --fish --name castle.fish $fishCompletionSrc 346 + ''; 347 + 348 + meta = with lib; { 349 + description = "Hogwarts castle infrastructure management CLI"; 350 + license = licenses.mit; 351 + }; 352 + }; 353 + 354 + in { 355 + config = lib.mkIf cfg.enable { 356 + environment.systemPackages = [ castleCli pkgs.gum pkgs.jq ]; 357 + 358 + # Store manifest for reference 359 + environment.etc."castle/backup-manifest.json".source = backupManifest; 360 + }; 361 + }
+25
modules/restic/completions/castle.bash
··· 1 + # Bash completion for castle CLI 2 + # Credit: Based on implementation by krn (https://github.com/taciturnaxolotl/dots) 3 + 4 + _castle() { 5 + local cur prev words cword 6 + _init_completion || return 7 + 8 + local commands="backup" 9 + local backup_commands="status list run restore dr" 10 + 11 + case "${prev}" in 12 + castle) 13 + COMPREPLY=($(compgen -W "${commands} --help" -- "${cur}")) 14 + return 15 + ;; 16 + backup) 17 + COMPREPLY=($(compgen -W "${backup_commands}" -- "${cur}")) 18 + return 19 + ;; 20 + esac 21 + 22 + COMPREPLY=() 23 + } 24 + 25 + complete -F _castle castle
+16
modules/restic/completions/castle.fish
··· 1 + # Fish completion for castle CLI 2 + # Credit: Based on implementation by krn (https://github.com/taciturnaxolotl/dots) 3 + 4 + # Disable file completions 5 + complete -c castle -f 6 + 7 + # Top-level commands 8 + complete -c castle -n "__fish_use_subcommand" -a "backup" -d "Manage backups and restores" 9 + complete -c castle -n "__fish_use_subcommand" -a "--help" -d "Show help message" 10 + 11 + # Backup subcommands 12 + complete -c castle -n "__fish_seen_subcommand_from backup" -a "status" -d "Show backup status for all services" 13 + complete -c castle -n "__fish_seen_subcommand_from backup" -a "list" -d "List snapshots" 14 + complete -c castle -n "__fish_seen_subcommand_from backup" -a "run" -d "Trigger manual backup" 15 + complete -c castle -n "__fish_seen_subcommand_from backup" -a "restore" -d "Interactive restore wizard" 16 + complete -c castle -n "__fish_seen_subcommand_from backup" -a "dr" -d "Disaster recovery mode"
+31
modules/restic/completions/castle.zsh
··· 1 + #compdef castle 2 + # Zsh completion for castle CLI 3 + # Credit: Based on implementation by krn (https://github.com/taciturnaxolotl/dots) 4 + 5 + _castle() { 6 + local -a commands backup_commands 7 + 8 + commands=( 9 + 'backup:Manage backups and restores' 10 + '--help:Show help message' 11 + ) 12 + 13 + backup_commands=( 14 + 'status:Show backup status for all services' 15 + 'list:List snapshots' 16 + 'run:Trigger manual backup' 17 + 'restore:Interactive restore wizard' 18 + 'dr:Disaster recovery mode' 19 + ) 20 + 21 + case "${words[2]}" in 22 + backup) 23 + _describe -t backup_commands 'backup command' backup_commands 24 + ;; 25 + *) 26 + _describe -t commands 'castle command' commands 27 + ;; 28 + esac 29 + } 30 + 31 + _castle "$@"
+131
modules/restic/default.nix
··· 1 + # Restic backup module for NixOS 2 + # Credit: Based on implementation by krn (https://github.com/taciturnaxolotl/dots) 3 + # 4 + # Provides automated backups to Backblaze B2 (or any restic-compatible backend) 5 + # with per-service configuration, database handling, and an interactive CLI. 6 + { 7 + config, 8 + lib, 9 + pkgs, 10 + ... 11 + }: 12 + let 13 + cfg = config.castle.backup; 14 + 15 + # Create a restic backup job for a service 16 + mkBackupJob = name: serviceCfg: { 17 + inherit (serviceCfg) paths; 18 + exclude = serviceCfg.exclude; 19 + 20 + initialize = true; 21 + 22 + # Use secrets from agenix 23 + environmentFile = config.age.secrets."restic/env".path; 24 + repositoryFile = config.age.secrets."restic/repo".path; 25 + passwordFile = config.age.secrets."restic/password".path; 26 + 27 + # Tags for easier filtering during restore 28 + extraBackupArgs = 29 + (map (t: "--tag ${t}") (serviceCfg.tags or [ "service:${name}" ])) 30 + ++ [ "--verbose" ]; 31 + 32 + # Retention policy 33 + pruneOpts = [ 34 + "--keep-last 3" 35 + "--keep-daily 7" 36 + "--keep-weekly 5" 37 + "--keep-monthly 12" 38 + "--tag service:${name}" 39 + ]; 40 + 41 + # Backup schedule (nightly at 2 AM + random delay) 42 + timerConfig = { 43 + OnCalendar = "02:00"; 44 + RandomizedDelaySec = "2h"; 45 + Persistent = true; 46 + }; 47 + 48 + # Pre/post backup hooks for database consistency 49 + backupPrepareCommand = lib.optionalString (serviceCfg.preBackup or null != null) serviceCfg.preBackup; 50 + backupCleanupCommand = lib.optionalString (serviceCfg.postBackup or null != null) serviceCfg.postBackup; 51 + }; 52 + 53 + in 54 + { 55 + imports = [ ./cli.nix ]; 56 + 57 + options.castle.backup = { 58 + enable = lib.mkEnableOption "Restic backup system"; 59 + 60 + services = lib.mkOption { 61 + type = lib.types.attrsOf ( 62 + lib.types.submodule { 63 + options = { 64 + enable = lib.mkOption { 65 + type = lib.types.bool; 66 + default = true; 67 + description = "Enable backups for this service"; 68 + }; 69 + 70 + paths = lib.mkOption { 71 + type = lib.types.listOf lib.types.str; 72 + description = "Paths to back up"; 73 + }; 74 + 75 + exclude = lib.mkOption { 76 + type = lib.types.listOf lib.types.str; 77 + default = [ "*.log" "node_modules" ".git" ]; 78 + description = "Glob patterns to exclude from backup"; 79 + }; 80 + 81 + tags = lib.mkOption { 82 + type = lib.types.listOf lib.types.str; 83 + default = [ ]; 84 + description = "Tags to apply to snapshots"; 85 + }; 86 + 87 + preBackup = lib.mkOption { 88 + type = lib.types.nullOr lib.types.str; 89 + default = null; 90 + description = "Command to run before backup (e.g., stop service, checkpoint DB)"; 91 + }; 92 + 93 + postBackup = lib.mkOption { 94 + type = lib.types.nullOr lib.types.str; 95 + default = null; 96 + description = "Command to run after backup (e.g., restart service)"; 97 + }; 98 + }; 99 + } 100 + ); 101 + default = { }; 102 + description = "Per-service backup configurations"; 103 + }; 104 + }; 105 + 106 + config = lib.mkIf cfg.enable { 107 + # Ensure secrets are defined 108 + assertions = [ 109 + { 110 + assertion = config.age.secrets ? "restic/env"; 111 + message = "castle.backup requires age.secrets.\"restic/env\" to be defined"; 112 + } 113 + { 114 + assertion = config.age.secrets ? "restic/repo"; 115 + message = "castle.backup requires age.secrets.\"restic/repo\" to be defined"; 116 + } 117 + { 118 + assertion = config.age.secrets ? "restic/password"; 119 + message = "castle.backup requires age.secrets.\"restic/password\" to be defined"; 120 + } 121 + ]; 122 + 123 + # Create restic backup jobs for each enabled service 124 + services.restic.backups = lib.mapAttrs mkBackupJob ( 125 + lib.filterAttrs (n: v: v.enable) cfg.services 126 + ); 127 + 128 + # Add restic and sqlite to system packages for manual operations 129 + environment.systemPackages = [ pkgs.restic pkgs.sqlite ]; 130 + }; 131 + }
+8
secrets/secrets.nix
··· 62 62 # NPM registry tokens 63 63 # Contains: npmjs.org and GitHub packages auth tokens 64 64 "npmrc.age".publicKeys = all; 65 + 66 + # Restic backup secrets (for B2 or any S3-compatible storage) 67 + # restic/env.age: B2_ACCOUNT_ID and B2_ACCOUNT_KEY (or AWS_ACCESS_KEY_ID, etc.) 68 + # restic/repo.age: Repository URL (e.g., b2:bucket-name:/path) 69 + # restic/password.age: Repository encryption password 70 + "restic/env.age".publicKeys = [ jsp alastor ]; 71 + "restic/repo.age".publicKeys = [ jsp alastor ]; 72 + "restic/password.age".publicKeys = [ jsp alastor ]; 65 73 }