1. Overview

SSH Backup (BackupScheduler) is a .NET 10 Windows application that provides automated, scheduled backups to remote SSH/SFTP servers. It supports two backup strategies—ZIP archives and incremental folder sync—and can be controlled through a WPF GUI or run unattended from Windows Task Scheduler in headless mode.

📦 Dual Backup Modes

Choose ZIP (versioned, encrypted) or incremental sync (rsync-like) per folder.

🔒 AES-256 Encryption

Password-protected ZIP files keep your data secure at rest on the server.

🤖 Headless Mode

Run ssh_backup.exe --headless for fully automated, unattended backups.

🌐 Remote API

Centralized configuration & log collection via HTTP API for multi-machine management.

🔄 Incremental Sync

Pure .NET solution—no external rsync required. Only uploads changed files.

🗂️ Organized Storage

Each folder gets its own subdirectory on the server with automatic old-backup cleanup.

Backup Organization on Server

/mnt/backups/scott/ ├── workstuff/ (ZIP mode) │ ├── backup_20240115_123456.zip │ ├── backup_20240116_143022.zip │ └── backup_20240117_150000.zip ├── Documents/ (Sync mode) │ ├── file1.txt │ ├── file2.docx │ └── subfolder/ │ └── notes.md └── Photos/ (Sync mode) ├── photo1.jpg └── vacation/ └── photo2.jpg

2. Getting Started

System Requirements

  • OS: Windows 10 / 11 (64-bit)
  • .NET Runtime: .NET 10.0 Desktop Runtime (x64)
  • Network: Access to an SSH/SFTP server

Quick-Start Steps

  1. Install .NET 10 Desktop Runtimedownload here
  2. Launch ssh_backup.exe
  3. Click Settings and fill in your SSH host, port, username, password, remote path, and (optionally) ZIP password
  4. Add folders/files with the Add Folder / Add File buttons
  5. Choose mode per folder: leave Incremental Sync unchecked for ZIP, checked for sync
  6. Click Start Backup to run a test
  7. Verify files on the remote server
  8. Set up Task Scheduler for automation (see Section 5)
💡 Tip: Always test a manual backup and a restoration before scheduling automated runs.

3. Backup Modes

📦 ZIP Backup Mode

Creates timestamped, AES-256-encrypted ZIP archives.
Old archives are automatically deleted when the retention limit is reached.

  • File format: backup_YYYYMMDD_HHMMSS.zip
  • Uploaded to /remote/path/{foldername}/
  • Configurable retention via Max Backup Files
  • Point-in-time recovery from any version

Best for: documents, databases, source code, anything that compresses well or needs versioning.

🔄 Incremental Sync Mode

Pure .NET implementation—no external rsync needed.
Compares size + modification time; uploads only new or changed files.

  • 2-second timestamp tolerance for filesystem differences
  • Preserves modification times on remote
  • Creates missing directories automatically
  • Real-time progress: uploaded vs. skipped counts

Best for: media libraries, large file collections, data that changes frequently.

Feature Comparison

FeatureZIP ModeIncremental Sync
Versioning✓ Multiple versions✗ Single copy
Encryption✓ AES-256✗ (SSH transport only)
Compression✓ ZIP compression✗ Raw files
BandwidthRe-uploads full archiveOnly changes
Retention Limit✓ ConfigurableN/A (single copy)
External DependenciesNoneNone

Mixed-Mode Backups

Different folders can use different modes in the same backup job:

C:\Documents\ → ZIP Mode (versioned, encrypted) C:\Pictures\ → Sync Mode (incremental, efficient) C:\Work\Projects\ → ZIP Mode (versioned, encrypted) D:\Media\Videos\ → Sync Mode (incremental, efficient)

How Incremental Sync Works

For each local file: 1. Check if file exists on remote server 2. If exists → compare size and modification time (±2 sec) • Both match → skip (file unchanged) 3. If missing or changed → upload 4. Create any missing remote directories

4. GUI Application

Main Window

  • Folder list — shows path, method (ZIP / Incremental), and sync checkbox
  • Add Folder / Add File — browse and add items to backup
  • Remove Selected — remove a folder from the list
  • Settings — open configuration dialog
  • Start Backup — run an immediate backup
  • View Log — open the built-in log viewer
  • About — version and credits
  • Progress bar & status text — real-time feedback
  • Remote Path display — shows current destination with source indicator (local / API)
  • Last Backup time — displayed in the header area

Settings Window

FieldRequiredDescription
SSH HostYesHostname or IP of SFTP server
SSH PortNoDefault: 22
UsernameYesSSH login name
SSH PasswordYesStored encrypted via DPAPI
Remote PathYesDestination directory, e.g. /backup
ZIP PasswordNoAES-256 encryption for archives
Max Backup FilesNoRetention limit per folder (0 = unlimited)
Remote Server URLNoBase URL for management API
Remote API KeyNoAPI authentication key (encrypted)
Upload Log FileNoSend logs to API after backup

5. Task Scheduler & Headless Mode

SSH Backup can run without a GUI by passing --headless. This makes it ideal for Windows Task Scheduler, scripts, or any automation tool.

Command-Line Usage

# Normal GUI ssh_backup.exe # Headless (no GUI) ssh_backup.exe --headless

Exit Codes

CodeMeaning
0Backup completed successfully
1Backup failed (check logs)
2Missing required settings — configure via GUI first
3Remote API fetch failed
4Backups disabled by remote server

Quick Setup (5 Minutes)

  1. Configure settings — open the GUI, fill in SSH details, add folders, test a backup
  2. Test headless mode — open PowerShell:
    cd "C:\Program Files\SSHBackup" .\ssh_backup.exe --headless echo $LASTEXITCODE # should be 0
  3. Open Task SchedulerWin+R → taskschd.msc
  4. Create Basic Task:
    • Name: SSH Backup - Daily
    • Trigger: Daily at 2:00 AM
    • Action: Start a program
    • Program: C:\Program Files\SSHBackup\ssh_backup.exe
    • Arguments: --headless
  5. Advanced settings:
    • ☑ Run whether user is logged on or not
    • ☑ Run with highest privileges
    • ☑ If the task fails, restart every 15 minutes (up to 3 times)
  6. Test — right-click the task → Run, then check the log

PowerShell One-Liners

# Daily at 2 AM schtasks /create /tn "SSH Backup Daily" /tr "\"C:\Program Files\SSHBackup\ssh_backup.exe\" --headless" /sc daily /st 02:00 /f # Every 4 hours schtasks /create /tn "SSH Backup 4h" /tr "\"C:\Program Files\SSHBackup\ssh_backup.exe\" --headless" /sc hourly /mo 4 /st 00:00 /f # Weekly on Sunday at 3 AM schtasks /create /tn "SSH Backup Weekly" /tr "\"C:\Program Files\SSHBackup\ssh_backup.exe\" --headless" /sc weekly /d SUN /st 03:00 /f

Example Console Output

SSH Backup - Headless Mode =========================== Start Time: 1/15/2025 2:00:15 PM Host: backup.example.com:22 Username: backup_user Remote Path: /backups Folders to backup: 3 Running backup (1 ZIP, 2 Incremental Sync)... [14:00:16] Creating ZIP archive... [14:00:45] Uploading ZIP backup... [14:01:02] Processing 2 folder(s) with Incremental Sync... [14:01:03] Syncing: Documents (1/2) [14:01:04] Uploading: report.pdf [14:01:05] Progress: 45/100 files (2 uploaded, 43 skipped) [14:01:20] Completed: 2 uploaded, 43 unchanged [14:01:21] Backup completed successfully! End Time: 1/15/2025 2:05:32 PM ===========================
⚠️ Important: The scheduled task must run as the same Windows user who configured the settings, because credentials are stored per-user in the registry via DPAPI.

6. Remote API Integration

SSH Backup can fetch configuration from a central server and report back via HTTP API. A single Server URL is used for all endpoints; authentication is via an X-API-Key header.

Endpoints

PurposeMethodPath
Fetch / Test Settings GET /backup/api/settings_secure.php?username={user}
Upload Log POST /backup/api/log_upload.php

Settings Response

GET /backup/api/settings_secure.php?username=john X-API-Key: your-api-key Accept: application/json Response: { "username": "john", "role": "user", "backupDirectory": "/mnt/backups/john", "maxBackupFiles": 7, "enabled": true }

How It's Used

  1. On every startup (GUI and headless), the app calls the settings endpoint
  2. If enabled is false, GUI shows a warning; headless exits with code 4
  3. backupDirectory and maxBackupFiles override local values
  4. Values are saved to the registry (RemoteApiPath, RemoteApiMaxBackups) for offline fallback
✓ Graceful Fallback: If the API is unreachable, the application continues with locally stored settings. Your backups keep running even when the management server is offline.

7. Log Upload Feature

After each backup (success or failure), SSH Backup can POST the full log to your API for centralized monitoring, alerting, and auditing.

Enable in 4 Steps

  1. Open Settings
  2. Fill in Remote Server URL and API Key
  3. Check "Upload log file to remote server after backup completes"
  4. Click Save

API Request Format

POST /backup/api/log_upload.php X-API-Key: your-api-key Content-Type: application/json { "log": "Full log content...", "filename": "backup_log_DESKTOP-ABC123_2025-01-15_143022.log", "mode": "file" }

Filename Convention

backup_log_{MACHINE}_{DATE}_{TIME}.log Example: backup_log_DESKTOP-ABC123_2025-01-15_143022.log

What's in the Log?

[2025-01-15 14:30:15] Backup STARTED - Method: Mixed, Host: backup.example.com, Folders: 3 [2025-01-15 14:30:16] Creating ZIP archive... [2025-01-15 14:30:45] Uploading ZIP backup... [2025-01-15 14:31:02] Processing 2 folder(s) with Incremental Sync... [2025-01-15 14:31:03] Syncing: Documents (1/2) [2025-01-15 14:31:04] Uploading: report.pdf [2025-01-15 14:31:05] Progress: 45/100 files (2 uploaded, 43 skipped) [2025-01-15 14:31:20] FOLDER: Documents - Method: Incremental Sync Total: 100 files, Uploaded: 2, Changed: 2, Deleted: 0 [2025-01-15 14:31:21] Backup SUCCESS

Use Cases

📊 Centralized Monitoring

Collect logs from multiple machines into one API. Build dashboards, generate reports.

🔍 Remote Troubleshooting

Review logs via API without accessing each machine.

🔔 Alerting

API can parse logs and trigger email/SMS/Slack notifications on failure.

📈 Auditing

Store logs in a database for compliance, regulatory review, or historical analysis.

💡 Note: The backup is considered successful regardless of whether the log upload succeeds. A failed upload is logged locally but does not change the exit code.

8. Server-Side API Implementation

You need to host two endpoints on your own server. Below are starter examples in PHP and Python.

Directory Structure

/var/www/html/ └── backup/ └── api/ ├── settings_secure.php ├── log_upload.php └── common/ └── auth.php (shared API-key validation)

Shared Authentication (PHP)

<?php // common/auth.php function validateApiKey() { $apiKey = $_SERVER['HTTP_X_API_KEY'] ?? ''; $validKeys = [ 'your-api-key-here' => 'Client Name', ]; if (!isset($validKeys[$apiKey])) { http_response_code(401); echo json_encode(['error' => 'Invalid API key']); exit; } return $apiKey; } ?>

settings_secure.php (PHP)

<?php require_once 'common/auth.php'; validateApiKey(); $username = $_GET['username'] ?? ''; if (empty($username)) { http_response_code(400); echo json_encode(['error' => 'Username required']); exit; } // Fetch from database or config file $settings = getSettingsForUser($username); header('Content-Type: application/json'); echo json_encode([ 'username' => $username, 'role' => $settings['role'] ?? 'user', 'backupDirectory' => $settings['backup_directory'] ?? '/backup', 'maxBackupFiles' => $settings['max_backup_files'] ?? 5, 'enabled' => $settings['enabled'] ?? true ]); ?>

log_upload.php (PHP)

<?php require_once 'common/auth.php'; validateApiKey(); $data = json_decode(file_get_contents('php://input'), true); if (!isset($data['log']) || !isset($data['filename'])) { http_response_code(400); echo json_encode(['error' => 'Missing required fields']); exit; } // Save to database or file system file_put_contents('/var/backups/logs/' . basename($data['filename']), $data['log']); header('Content-Type: application/json'); echo json_encode([ 'success' => true, 'message' => 'Log uploaded successfully', 'filename' => $data['filename'] ]); ?>

Python Flask Example

from flask import Flask, request, jsonify app = Flask(__name__) VALID_KEYS = {'your-api-key-here': 'Client'} def check_key(): key = request.headers.get('X-API-Key', '') if key not in VALID_KEYS: return jsonify(error='Invalid API key'), 401 return None @app.route('/backup/api/settings_secure.php') def get_settings(): err = check_key() if err: return err user = request.args.get('username', '') return jsonify( username=user, role='user', backupDirectory='/backup', maxBackupFiles=5, enabled=True ) @app.route('/backup/api/log_upload.php', methods=['POST']) def upload_log(): err = check_key() if err: return err data = request.get_json() # save data['log'] with data['filename'] return jsonify(success=True, message='Log uploaded')

Testing Your Endpoints

# Test settings curl -H "X-API-Key: your-api-key" \ "http://10.254.200.3:9000/backup/api/settings_secure.php?username=test" # Test log upload curl -X POST \ -H "X-API-Key: your-api-key" \ -H "Content-Type: application/json" \ -d '{"log":"test log","filename":"test.log","mode":"file"}' \ "http://10.254.200.3:9000/backup/api/log_upload.php"

9. Configuration Reference

Registry Location

All settings are stored per-user at: HKEY_CURRENT_USER\Software\SSHBackup

KeyTypeDescription
HostStringSFTP server hostname / IP
PortStringSSH port (default "22")
UsernameStringSSH username
SSHPasswordEncryptedSSH password (DPAPI)
ZipPasswordEncryptedZIP encryption password (DPAPI)
RemotePathStringDestination directory on server
MaxBackupFilesStringZIP retention limit per folder (0 = unlimited)
RemoteServerUrlStringAPI base URL
RemoteApiKeyEncryptedAPI authentication key (DPAPI)
RemoteApiPathStringLast path received from API
RemoteApiMaxBackupsStringLast retention limit received from API
UploadLogFileString"true" / "false"
FolderSetting0, 1, …Stringpath|enabled — enabled=True → sync, False → ZIP

Exporting / Importing Settings

# Export reg export "HKCU\Software\SSHBackup" SSHBackup_Settings.reg # Import (on another machine or after reinstall) reg import SSHBackup_Settings.reg
⚠️ Caution: Exported .reg files contain DPAPI-encrypted passwords. They can only be decrypted by the same Windows user account on the same machine.

Log Files

FileLocationPurpose
backup_log.txt%LocalAppData%\SSHBackup\logs\Main backup activity log
error.logApplication directoryUnhandled exceptions

10. Troubleshooting

Application Won't Start

Symptom: Double-clicking the exe does nothing or shows an error

  • Verify .NET 10.0 Desktop Runtime (x64) is installed: dotnet --list-runtimes
  • Check error.log in the application folder
  • Try running as Administrator
  • Ensure all DLLs are present: Renci.SshNet.dll, BouncyCastle.Cryptography.dll, DotNetZip.dll

Authentication Failed

Symptom: Log shows "Authentication failed" or "Permission denied"

  • Verify credentials with PuTTY or another SSH client
  • Check firewall rules on port 22
  • Ensure the SSH server is running
  • Confirm the remote path exists and is writable by the SSH user

Headless Mode: "Missing required settings"

Exit code 2

  • Open the GUI and configure settings first
  • Ensure the Task Scheduler task runs as the same user who configured settings
  • Registry settings are per-user (HKCU)

API Settings Not Applying

Symptom: Remote settings not reflected in the app

  • Verify Server URL and API Key in Settings
  • Click Test Connection to check reachability
  • Test manually:
    curl -H "X-API-Key: KEY" \ "http://SERVER/backup/api/settings_secure.php?username=test"
  • Check that your API returns valid JSON with backupDirectory, maxBackupFiles, enabled

Log Upload Fails

Symptom: Backup succeeds but log upload shows an error

  • Common HTTP status codes:
    • 401 — invalid API key
    • 404 — endpoint URL incorrect
    • 500 — server-side error
  • Test manually:
    curl -X POST \ -H "X-API-Key: KEY" \ -H "Content-Type: application/json" \ -d '{"log":"test","filename":"test.log","mode":"file"}' \ http://SERVER/backup/api/log_upload.php
  • The backup is still successful even if the upload fails

Old Backups Not Being Deleted

Symptom: More ZIP files than the configured retention limit

  • Verify the folder uses ZIP mode (not Sync)
  • Max Backup Files = 0 means unlimited — set a positive number
  • Check remote directory permissions (SSH user needs delete rights)

Slow Backup Performance

Tips

  • Use Sync mode for large/media files (avoids re-uploading unchanged data)
  • Schedule during off-hours to avoid network congestion
  • Use wired Ethernet instead of Wi-Fi
  • Exclude temporary files and caches from backup