diff --git a/README.md b/README.md index 33c8468..497d519 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ home-manager switch --flake github:Logan-Lin/nix-config#yanlin@hs │ ├── ghostty.nix # GPU-accelerated terminal emulator │ ├── syncthing.nix # File synchronization service (includes package) │ ├── tailscale.nix # Secure networking and VPN service +│ ├── borg.nix # Borg backup system with automated scheduling │ └── homebrew.nix # Homebrew and nix-homebrew configuration ├── config/ # Configuration files │ ├── firefox/ # Firefox browser configuration @@ -782,6 +783,75 @@ hms - **Tmux**: Copy mode automatically uses system clipboard - **Terminal**: Standard Cmd+C/V works everywhere +## 📦 Automated Backups: Borg + +**Configuration**: `modules/borg.nix` +**Purpose**: Deduplicating archiver with compression and encryption for automated backups + +### Key Features: +- **Encrypted Backups**: Repository encrypted with passphrase for security +- **Deduplication**: Space-efficient incremental backups +- **Automated Scheduling**: Systemd timer for unattended daily backups +- **Flexible Configuration**: Host-specific backup paths, retention policies, and frequencies +- **Progress Monitoring**: Detailed logging and status reporting + +### Default Configuration (Home Server): +- **Backup Paths**: `/home` and `/var/lib/containers` +- **Repository**: `ssh://storage-box/./hs` (Hetzner Storage Box via SSH) +- **Schedule**: Daily backups with 30-minute random delay +- **Retention**: 7 daily, 4 weekly, 6 monthly, 2 yearly +- **Compression**: LZ4 with level 6 (balanced speed/size) + +### Command Line Usage: + +#### Manual Backup Operations: +```bash +# Initialize repository (first-time setup) +borg-init # Initialize encrypted repository + +# Start manual backup +borg-backup-now # Trigger immediate backup + +# Check backup status +borg-status # View service and timer status +borg-logs # Follow backup logs in real-time +``` + +#### Direct Borg Commands: +```bash +# Set up environment for direct borg commands +export BORG_REPO=ssh://storage-box/./hs +export BORG_RSH="ssh -F /home/yanlin/.ssh/config" + +# Browse backup contents +borg list # List all archives +borg list :: # List files in specific archive + +# Extract files +borg extract :: # Extract entire archive +borg extract :: path/to/file # Extract specific files + +# Repository maintenance +borg check # Verify repository consistency +borg info :: # Show archive details and statistics +``` + +### Configuration Options: +- **repositoryUrl**: Local path or remote SSH URL for backup storage +- **backupPaths**: List of directories to include in backups +- **backupFrequency**: Systemd timer frequency (daily, hourly, or OnCalendar format) +- **retention**: Flexible policy for keeping daily/weekly/monthly/yearly backups +- **excludePatterns**: Comprehensive list of files/directories to skip +- **compressionLevel**: Balance between backup speed and storage efficiency + +### Security Setup: +```bash +# Create passphrase file (required for repository encryption) +# Format: BORG_PASSPHRASE=yourpassphrase +echo "BORG_PASSPHRASE=your-secure-passphrase" | sudo tee /etc/borg-passphrase +sudo chmod 600 /etc/borg-passphrase +``` + ## 🔒 Secure Networking: Tailscale **Configuration**: `modules/tailscale.nix` diff --git a/hosts/nixos/hs/system.nix b/hosts/nixos/hs/system.nix index 6208744..410a8f7 100644 --- a/hosts/nixos/hs/system.nix +++ b/hosts/nixos/hs/system.nix @@ -7,6 +7,7 @@ ../../../modules/traefik.nix ../../../modules/samba.nix ../../../modules/disk-health.nix + ../../../modules/borg.nix ]; # GRUB bootloader with ZFS support @@ -219,6 +220,34 @@ # Enable experimental nix features nix.settings.experimental-features = [ "nix-command" "flakes" ]; + # Borg backup configuration + services.borgbackup-custom = { + enable = true; + # Use SSH alias from SSH config for remote backup + repositoryUrl = "ssh://storage-box/./hs"; + backupPaths = [ "/home" "/var/lib/containers" ]; + # Examples: + # backupFrequency = "daily"; # Midnight (default) + # backupFrequency = "*-*-* 03:00:00"; # Every day at 3:00 AM + # backupFrequency = "*-*-* 22:30:00"; # Every day at 10:30 PM + # backupFrequency = "Mon,Wed,Fri 02:00:00"; # Mon/Wed/Fri at 2:00 AM + backupFrequency = "daily"; + retention = { + keepDaily = 7; + keepWeekly = 4; + keepMonthly = 6; + keepYearly = 2; + }; + passphraseFile = "/etc/borg-passphrase"; + preHook = '' + echo "$(date): Starting Borg backup of ${config.networking.hostName}" + ''; + postHook = '' + echo "$(date): Borg backup of ${config.networking.hostName} completed successfully" + # Optional: Send notification or update monitoring system + ''; + }; + # This value determines the NixOS release from which the default # settings for stateful data, like file locations and database versions # on your system were taken. It's perfectly fine and recommended to leave diff --git a/modules/borg.nix b/modules/borg.nix new file mode 100644 index 0000000..cddda2d --- /dev/null +++ b/modules/borg.nix @@ -0,0 +1,309 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.borgbackup-custom; +in + +{ + options.services.borgbackup-custom = { + enable = mkEnableOption "Borg backup service"; + + repositoryUrl = mkOption { + type = types.str; + example = "/mnt/backup/borg-repo"; + description = "Borg repository URL (local path or remote SSH URL)"; + }; + + backupPaths = mkOption { + type = types.listOf types.str; + default = [ "/home" "/var/lib/containers" ]; + example = [ "/home" "/var/lib/containers" "/etc" ]; + description = "List of directories to backup"; + }; + + backupFrequency = mkOption { + type = types.str; + default = "daily"; + example = "hourly"; + description = "Systemd timer frequency (OnCalendar format or shortcuts like daily, hourly)"; + }; + + retention = mkOption { + type = types.submodule { + options = { + keepDaily = mkOption { + type = types.int; + default = 7; + description = "Number of daily backups to keep"; + }; + keepWeekly = mkOption { + type = types.int; + default = 4; + description = "Number of weekly backups to keep"; + }; + keepMonthly = mkOption { + type = types.int; + default = 6; + description = "Number of monthly backups to keep"; + }; + keepYearly = mkOption { + type = types.int; + default = 2; + description = "Number of yearly backups to keep"; + }; + }; + }; + default = {}; + description = "Backup retention policy"; + }; + + excludePatterns = mkOption { + type = types.listOf types.str; + default = [ + # Temporary and cache files + "*.tmp" + "*.temp" + "*/.cache/*" + "*/.local/share/Trash/*" + "*/tmp/*" + "*/temp/*" + + # System files + "/proc/*" + "/sys/*" + "/dev/*" + "/run/*" + "/var/tmp/*" + "/var/cache/*" + "/var/log/*" + + # Container runtime files + "*/overlay2/*" + "*/containers/storage/overlay/*" + + # macOS metadata + ".DS_Store" + "._.DS_Store" + ".Spotlight-V100" + ".TemporaryItems" + ".Trashes" + ".fseventsd" + + # Build artifacts and dependencies + "node_modules/*" + "target/*" + "*.o" + "*.so" + "*.pyc" + "__pycache__/*" + + # Editor and IDE files + ".vscode/*" + "*.swp" + "*.swo" + "*~" + ]; + description = "List of patterns to exclude from backup"; + }; + + compressionLevel = mkOption { + type = types.int; + default = 6; + description = "Borg compression level (0-9, where 6 is balanced)"; + }; + + passphraseFile = mkOption { + type = types.str; + default = "/etc/borg-passphrase"; + description = "Path to file containing BORG_PASSPHRASE=yourpassphrase"; + }; + + sshCommand = mkOption { + type = types.str; + default = "ssh -F /home/yanlin/.ssh/config -o StrictHostKeyChecking=accept-new"; + description = "SSH command for remote repositories (uses SSH config for host aliases)"; + }; + + preHook = mkOption { + type = types.str; + default = ""; + example = "echo 'Starting backup...'"; + description = "Commands to run before backup"; + }; + + postHook = mkOption { + type = types.str; + default = ""; + example = "echo 'Backup completed.'"; + description = "Commands to run after successful backup"; + }; + }; + + config = mkIf cfg.enable { + # Install Borg package + environment.systemPackages = [ pkgs.borgbackup ]; + + # Create backup user for better isolation (optional) + users.users.borg-backup = { + isSystemUser = true; + group = "borg-backup"; + description = "Borg backup user"; + }; + users.groups.borg-backup = {}; + + # Systemd service for backup + systemd.services.borg-backup = { + description = "Borg Backup Service"; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + + # Add borg to the service's PATH + path = [ pkgs.borgbackup pkgs.openssh ]; + + serviceConfig = { + Type = "oneshot"; + User = "root"; # Need root to access all backup paths + Group = "root"; + + # Security settings + PrivateTmp = true; + ProtectSystem = "strict"; + # Disable ProtectHome for SSH repositories to allow SSH key access + ProtectHome = mkIf (!(lib.hasPrefix "ssh://" cfg.repositoryUrl)) "read-only"; + # Only add ReadWritePaths for local repositories + ReadWritePaths = mkIf (!(lib.hasPrefix "ssh://" cfg.repositoryUrl)) [ cfg.repositoryUrl ]; + + # Environment + Environment = [ + "BORG_REPO=${cfg.repositoryUrl}" + "BORG_RELOCATED_REPO_ACCESS_IS_OK=yes" + "BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=no" + ]; + EnvironmentFile = mkIf (cfg.passphraseFile != "") cfg.passphraseFile; + }; + + script = let + excludeArgs = concatMapStrings (pattern: " --exclude '${pattern}'") cfg.excludePatterns; + backupPathsStr = concatStringsSep " " (map (path: "'${path}'") cfg.backupPaths); + retentionArgs = with cfg.retention; concatStringsSep " " [ + "--keep-daily ${toString keepDaily}" + "--keep-weekly ${toString keepWeekly}" + "--keep-monthly ${toString keepMonthly}" + "--keep-yearly ${toString keepYearly}" + ]; + in '' + set -e + + # Set SSH command for remote repositories + export BORG_RSH="${cfg.sshCommand}" + + # Load passphrase from environment file + if [ -f "${cfg.passphraseFile}" ]; then + source "${cfg.passphraseFile}" + fi + + # Ensure root has access to SSH keys for remote repositories + if [[ "${cfg.repositoryUrl}" == ssh://* ]]; then + mkdir -p /root/.ssh + chmod 700 /root/.ssh + + # Copy SSH config if it exists + if [ -f /home/yanlin/.ssh/config ]; then + cp /home/yanlin/.ssh/config /root/.ssh/config + chmod 600 /root/.ssh/config + fi + + # Copy necessary SSH keys + if [ -d /home/yanlin/.ssh/keys ]; then + cp -r /home/yanlin/.ssh/keys /root/.ssh/ + chmod -R 600 /root/.ssh/keys + fi + fi + + # Pre-hook + ${cfg.preHook} + + # Initialize repository if it doesn't exist + if ! borg info > /dev/null 2>&1; then + echo "Initializing Borg repository at ${cfg.repositoryUrl}" + borg init --encryption=repokey-blake2 + fi + + # Create backup archive with timestamp + ARCHIVE_NAME="backup-$(date +%Y-%m-%d_%H-%M-%S)" + echo "Creating backup archive: $ARCHIVE_NAME" + + borg create \ + --verbose \ + --stats \ + --progress \ + --compression lz4,${toString cfg.compressionLevel} \ + --exclude-caches \ + ${excludeArgs} \ + "::$ARCHIVE_NAME" \ + ${backupPathsStr} + + # Prune old backups + echo "Pruning old backups..." + borg prune \ + --list \ + --prefix 'backup-' \ + --show-rc \ + ${retentionArgs} + + # Compact repository to free space + echo "Compacting repository..." + borg compact + + # Post-hook + ${cfg.postHook} + + echo "Backup completed successfully" + ''; + }; + + # Systemd timer for scheduled backups + systemd.timers.borg-backup = { + description = "Borg Backup Timer"; + wantedBy = [ "timers.target" ]; + + timerConfig = { + OnCalendar = cfg.backupFrequency; + Persistent = true; + RandomizedDelaySec = "30min"; # Add some randomization to avoid load spikes + }; + }; + + # Enable and start the timer + systemd.targets.multi-user.wants = [ "borg-backup.timer" ]; + + # Create a convenience script for manual backups + environment.etc."borg-backup-manual" = { + text = '' + #!/usr/bin/env bash + + echo "Starting manual Borg backup..." + systemctl start borg-backup.service + + echo "Checking backup status..." + systemctl status borg-backup.service + + echo "Recent backup logs:" + journalctl -u borg-backup.service -n 20 + ''; + mode = "0755"; + }; + + # Helpful aliases for managing backups + environment.shellAliases = { + borg-init = "BORG_REPO='${cfg.repositoryUrl}' BORG_RSH='${cfg.sshCommand}' borg init --encryption=repokey-blake2"; + borg-status = "systemctl status borg-backup.service borg-backup.timer"; + borg-logs = "journalctl -u borg-backup.service -f"; + borg-backup-now = "sudo systemctl start borg-backup.service"; + borg-list = "BORG_REPO='${cfg.repositoryUrl}' BORG_RSH='${cfg.sshCommand}' borg list"; + borg-info = "BORG_REPO='${cfg.repositoryUrl}' BORG_RSH='${cfg.sshCommand}' borg info"; + }; + }; +} diff --git a/modules/ssh.nix b/modules/ssh.nix index fefa89d..fae45e2 100644 --- a/modules/ssh.nix +++ b/modules/ssh.nix @@ -45,6 +45,13 @@ identityFile = "~/.ssh/keys/hetzner"; }; + "storage-box" = { + hostname = "u448310.your-storagebox.de"; + user = "u448310"; + identityFile = "~/.ssh/keys/storage-box"; + port = 23; + }; + }; }; }