Бекап через OpenSSH на Windows Servers

За допомогою OpenSSH на Windows Servers можна налаштувати бекап потрібних файлів у зручний спосіб через Powershell. Качаємо  версію OpenSSH-Win64.zip https://github.com/powershell/win32-openssh/releases., закидуємо на сервер, створюємо папку C:\Program Files\OpenSSH і туди розпаковуваємо файли.

Установка OpenSSH. Запускаємо PowerShell ISE від Адміністратора і виконуємо команди нижче.

# 1. Переходимо в папку, куди розпакували файли
cd “C:\Program Files\OpenSSH”

# 2. Запускаємо вбудований скрипт встановлення від Microsoft
.\install-sshd.ps1

PowerShel буде ругатися , що не підтверджений виробник, погоджуємося і запускаємо. В результаті побачимо створення ланцюгів в реєстрі і слуби клієнта і сервера. Далі заходимо в сервіси і дивимося чи є сервіси, має бути OpenSSH Server і Authentication Agent.

# 3. Дозволяємо службі запускатися автоматично при старті сервера
Set-Service -Name sshd -StartupType ‘Automatic’

# 4. Запускаємо службу прямо зараз
Start-Service sshd

Ми побачимо, що status=running, тоді все добре, якщо ні – шукаємо причину.

# 6 Створюємо правило для брандмауера (порт 22)
New-NetFirewallRule -Name ‘OpenSSH-Server-In-TCP’ -DisplayName ‘OpenSSH Server (Inbound)’ -Enabled True -Direction Inbound -Protocol TCP -LocalPort 22 -Action Allow

Успішним резудьтатом буде PrimaryStatus = OK. Можна відкривати ssh клієнт і пробувати логінитися з використання логіна і пароля на сервері.

Створення ключів для безпарольного доступу. Створюємо ключі  на машині де будуть зберігатися бекапи.

# 1. Створюємо папку .ssh у своємо профайлі
New-Item -ItemType Directory -Path “$env:USERPROFILE\.ssh” -Force

# 2. В створеній папці генеруємо ключі.
ssh-keygen -t ed25519 -f “$env:USERPROFILE\.ssh\id_windows_backup” -N ‘””‘

Ми отримаємо 2 ключі приватний і публічний. Той що .pub ми маємо закинути на сервер звідки будуть копіюватися бекапи.

Наступні дії робимо на серверах, звідки будемо качати бекапи.

# 1. Примусово створюємо папку для SSH, якщо її приховано. Ця папка має створитися на етапі установки OpenSSH. Якщо папка існує, то цей пункт пропускаємо.
New-Item -ItemType Directory -Path “C:\ProgramData\ssh” -Force | Out-Null

# 2. Створюємо сам файл ключів всередині прихованої папки
New-Item -ItemType File -Path “C:\ProgramData\ssh\administrators_authorized_keys” -Force | Out-Null

# 3. Відкриваємо цей файл у Notepad++ в режимі Адміністратора, оскільки по цьому шляху мають право зберігати тільки Адміністратори.
“C:\ProgramData\ssh\administrators_authorized_keys”, копіюємю сюди ключ із pub файла.

Налаштовуємо безпеку.

#Якщо сервер на кириліці, то ця команда буде виводити в UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8

# 1. Вимикаємо успадкування прав доступу
icacls “$env:ProgramData\ssh\administrators_authorized_keys” /inheritance:r

# 2. Надаємо повний доступ системі (SYSTEM)
icacls “$env:ProgramData\ssh\administrators_authorized_keys” /grant “SYSTEM:(F)”

# 3. Надаємо повний доступ групі адміністраторів (Administrators), якщо сервер рос або укр. мовою, то замість Administrators пишемо назву групи як вона зветься в системі –  Адміністратори(укр), Администраторы(рос).
icacls “$env:ProgramData\ssh\administrators_authorized_keys” /grant “BUILTIN\Administrators:(F)”

+ можна надати доступ конкретному користувачу

icacls “$env:ProgramData\ssh\administrators_authorized_keys” /grant “user:(F)”

# 4. Перезапускаємо OpenSSH, щоб усе запрацювало

Restart-Service sshd

Вимкнення можливість входу за звичайним паролем. Вносимо зміни в “C:\ProgramData\ssh\sshd_config”, відкривши notepad в режимі Адміністратора.
PubkeyAuthentication yes
PasswordAuthentication no
PermitEmptyPasswords no

Перезапускаємо службу Restart-Service sshd.

Скрипт для бекапу в повершел. Замість login – ввести свій логін на сервері. Архівують файли в назві File1*.txt i File2*.db, вставляємо назву сервера, IP, шлях до папки, де лежать файли,які ми будемо архівувати.

# ================= КОНФІГУРАЦІЯ =================
# Чистий паралельний бекап + ZIP з паролем, БЕЗ TELEGRAM)
# =================================================
$LocalBackupRoot = “D:\Backup\
$KeepDays = 365
$SshKeyPath = “C:\key\.ssh\id_windows_backup”

# Конфігурація 7-Zip
$7ZipPath = “C:\Program Files\7-Zip\7z.exe”
$ArchivePassword = “test”

$ServersList = @(
@{ Name = “Serv1″; IP = “1.1.1.1”; Path = “C:\Users\Data1” },
@{ Name = “Serv2″; IP = “2.2.2.2”; Path = “C:\Users\Data2” }
)
# =================================================

$StartTime = Get-Date
$Timestamp = $StartTime.ToString(“dd_MM_yyyy”)
$CurrentBackupDir = Join-Path $LocalBackupRoot $Timestamp

Write-Host “=== ЗАПУЩЕНО ЦЕНТРАЛІЗОВАНИЙ БЕКАП ВЕРСІЇ: v8.0 ===” -ForegroundColor Green

$Jobs = @()
$FailedAuthServers = @()

foreach ($Server in $ServersList) {
$SrvName = $Server.Name
$SrvIP = $Server.IP
$SrvPath = $Server.Path
$FolderName = “${SrvName}_${SrvIP}”

$ServerDestFolder = New-Item -ItemType Directory -Path (Join-Path $CurrentBackupDir $FolderName) -Force
$LocalDest = $ServerDestFolder.FullName

# ТЕСТ SSH-АВТОРИЗАЦІЇ ПЕРЕД ЗАПУСКОМ SCP (Відсікання чужих/офлайн серверів за 5 сек)
Write-Host “[ТЕСТ SSH] Перевірка авторизації ключа на $SrvName ($SrvIP)…” -ForegroundColor Gray

$SshTestArgs = @(“-i”, “`”$SshKeyPath`””, “-o”, “StrictHostKeyChecking=no”, “-o”, “ConnectTimeout=5″, “-o”, “NumberOfPasswordPrompts=0″, “login@${SrvIP}”, “echo 1″)
$TestResult = Start-Process -FilePath “ssh.exe” -ArgumentList $SshTestArgs -NoNewWindow -Wait -PassThru -ErrorAction SilentlyContinue

if ($null -eq $TestResult -or $TestResult.ExitCode -ne 0) {
Write-Host “[ЗБІЙ АВТОРИЗАЦІЇ] Сервер $SrvName ($SrvIP) відхилив ключ або недоступний. Пропуск!” -ForegroundColor Red
$FailedAuthServers += $SrvName
continue
}

Write-Host “[СТАРТ] Авторизація успішна! Запуск копіювання для: $SrvName ($SrvIP)…” -ForegroundColor Cyan

# Запуск паралельного фонового потоку
$Job = Start-Job -Name “Backup_$SrvName” -ArgumentList $SshKeyPath, $SrvIP, $SrvPath, $LocalDest, $SrvName -ScriptBlock {
param($Key, $IP, $Path, $Dest, $Name)

$SshOptions = @(“-o”, “StrictHostKeyChecking=no”, “-o”, “ConnectTimeout=10″)

$RemotePathTxt = “login@${IP}:${Path}\File*.txt
& scp.exe -i “$Key” $SshOptions $RemotePathTxt $Dest 2>$null

$RemotePathDb = “login@${IP}:${Path}\File*.db
& scp.exe -i “$Key” $SshOptions $RemotePathDb $Dest 2>$null
return “Done”
}
$Jobs += $Job
}

# — ЦИКЛ ОЧІКУВАННЯ ДЛЯ ПРАЦЮЮЧИХ ПОТОКІВ (БЕЗЛІМІТНИЙ ДЛЯ ВЕЛИКИХ ФАЙЛІВ) —
if ($Jobs.Count -gt 0) {
Write-Host “—————————————-” -ForegroundColor Yellow
Write-Host “Очікування завершення завантаження файлів з працюючих серверів…” -ForegroundColor Yellow
$AllDone = $false
while (-not $AllDone) {
Start-Sleep -Seconds 2
$RunningJobs = $Jobs | Where-Object { $_.State -eq “Running” }
if ($RunningJobs.Count -eq 0) { $AllDone = $true }
}
$Jobs | Remove-Job
}

$EndTime = Get-Date
$TotalTime = $EndTime – $StartTime
$ElapsedTime = [string]::Format(“{0:00}:{1:00}:{2:00}”, $TotalTime.Hours, $TotalTime.Minutes, $TotalTime.Seconds)

$ReportTimestamp = $EndTime.ToString(“yyyy_MM_dd_HH_mm_ss”)
$ReportName = “backup_report_${ReportTimestamp}.txt”
$ReportPath = Join-Path $CurrentBackupDir $ReportName

$ReportContent = @(
“==========================================”,
“ЗВІТ ПРО ЦЕНТРАЛІЗОВАНИЙ БЕКАП “,
“==========================================”,
“Дата запуску: $($StartTime.ToString(‘dd.MM.yyyy’))”,
“Час початку: $($StartTime.ToString(‘HH:mm:ss’))”,
“Час закінчення: $($EndTime.ToString(‘HH:mm:ss’))”,
“Загальний час роботи: $ElapsedTime”,
“==========================================”,
“СТАТУС ЗАВАНТАЖЕННЯ СЕРВЕРІВ:”
)

Write-Host “—————————————-” -ForegroundColor Cyan
foreach ($Server in $ServersList) {
$SrvName = $Server.Name
$SrvIP = $Server.IP
$FolderName = “${SrvName}_${SrvIP}”
$TargetFolder = Join-Path $CurrentBackupDir $FolderName

if ($FailedAuthServers -contains $SrvName) {
Write-Host “ERROR: Сервер $SrvName ($SrvIP) недоступний. Порожню папку видалено.” -ForegroundColor Red
$ReportContent += “[ПОМИЛКА] Сервер $SrvName ($SrvIP) НЕ скачано (Збій SSH-авторизації).”
if (Test-Path $TargetFolder) { Remove-Item -Path $TargetFolder -Recurse -Force -ErrorAction SilentlyContinue }
continue
}

$DownloadedFiles = @()
if (Test-Path $TargetFolder) { $DownloadedFiles = Get-ChildItem -Path $TargetFolder -File }

if ($DownloadedFiles.Count -gt 0) {
Write-Host “SUCCESS: Для сервера $SrvName ($SrvIP) скачано файлів: $($DownloadedFiles.Count). Запуск ZIP…” -ForegroundColor Green

if (Test-Path $7ZipPath) {
$ZipPath = Join-Path $CurrentBackupDir “${FolderName}.zip”
$7zArgs = @(“a”, “-tzip”, “-p$ArchivePassword”, “`”$ZipPath`””, “*”)
$Process = Start-Process -FilePath $7ZipPath -ArgumentList $7zArgs -WorkingDirectory $TargetFolder -Wait -NoNewWindow -PassThru

if ($Process.ExitCode -eq 0) {
Write-Host “Архів для $SrvName створено успішно. Видалення папки.” -ForegroundColor Gray
Remove-Item -Path $TargetFolder -Recurse -Force -ErrorAction SilentlyContinue
$ReportContent += “[УСПІШНО] Сервер $SrvName ($SrvIP) скачано та заархівовано.”
} else {
Write-Host “Помилка архівації для $SrvName! Код 7-Zip: $($Process.ExitCode)” -ForegroundColor Red
$ReportContent += “[УВАГА] Сервер $SrvName ($SrvIP) скачано, але сталася помилка ZIP-архівації.”
}
}
} else {
Write-Host “ERROR: Папка сервера $SrvName ($SrvIP) порожня. Видалення директорії…” -ForegroundColor Red
$ReportContent += “[ПОМИЛКА] Сервер $SrvName ($SrvIP) НЕ скачано (Помилка копіювання).”
if (Test-Path $TargetFolder) { Remove-Item -Path $TargetFolder -Recurse -Force -ErrorAction SilentlyContinue }
}
}
$ReportContent += “==========================================”
Set-Content -Path $ReportPath -Value $ReportContent -Force

# — БЛОК АВТООЧИЩЕННЯ СТАРИХ БЕКАПІВ (Старше 2 днів) —
if (Test-Path $LocalBackupRoot) {
$OldBackupFolders = Get-ChildItem -Path $LocalBackupRoot -Directory
foreach ($Folder in $OldBackupFolders) {
if ($Folder.Name -match ‘^(\d{2})_(\d{2})_(\d{4})$’) {
$FolderDate = [datetime]::ParseExact($Folder.Name, ‘dd_MM_yyyy’, $null)
if ($FolderDate -lt $StartTime.Date.AddDays(-$KeepDays)) {
Write-Host “Видалення старої копії: $($Folder.FullName)” -ForegroundColor Red
Remove-Item -Path $Folder.FullName -Recurse -Force -ErrorAction SilentlyContinue
}
}
}
}

Write-Host “========================================” -ForegroundColor Cyan
Write-Host “Централізований паралельний бекап версії v8.0 завершено!” -ForegroundColor Cyan
Write-Host “Детальний звіт збережено у файл: $ReportPath” -ForegroundColor Green

 

Той самий скрипт + відправка в Телеграм.

 

# ================= КОНФІГУРАЦІЯ =================
# ВЕРСІЯ: v8.0 (бекап + ZIP з паролем +Telegram)
# =================================================
$LocalBackupRoot = “D:\Backup\MB”
$KeepDays = 365
$SshKeyPath = “C:\key\.ssh\id_windows_backup”

# Конфігурація 7-Zip
$7ZipPath = “C:\Program Files\7-Zip\7z.exe”
$ArchivePassword = “test”

# Telegram
$TelegramToken = “Свій Токен”
$TelegramChatID = “Свій чай ID”

function Send-TelegramMessage {
param([string]$Message)

try {
Invoke-RestMethod -Uri “https://api.telegram.org/bot$TelegramToken/sendMessage” `
-Method Post `
-Body @{
chat_id = $TelegramChatID
text = $Message
parse_mode = “HTML”
} | Out-Null
}
catch {}
}
$ServersList = @(
@{ Name = “Serv1″; IP = “1.1.1.1”; Path = “C:\Users\Data1” },
@{ Name = “Serv2″; IP = “2.2.2.2”; Path = “C:\Users\Data2” }
)
# =================================================

$StartTime = Get-Date
$Timestamp = $StartTime.ToString(“dd_MM_yyyy”)
$CurrentBackupDir = Join-Path $LocalBackupRoot $Timestamp

Write-Host “=== ЗАПУЩЕНО ЦЕНТРАЛІЗОВАНИЙ БЕКАП ВЕРСІЇ: v8.0 ===” -ForegroundColor Green

$Jobs = @()
$FailedAuthServers = @()

foreach ($Server in $ServersList) {
$SrvName = $Server.Name
$SrvIP = $Server.IP
$SrvPath = $Server.Path
$FolderName = “${SrvName}_${SrvIP}”

$ServerDestFolder = New-Item -ItemType Directory -Path (Join-Path $CurrentBackupDir $FolderName) -Force
$LocalDest = $ServerDestFolder.FullName

# ТЕСТ SSH-АВТОРИЗАЦІЇ ПЕРЕД ЗАПУСКОМ SCP (Відсікання чужих/офлайн серверів за 5 сек)
Write-Host “[ТЕСТ SSH] Перевірка авторизації ключа на $SrvName ($SrvIP)…” -ForegroundColor Gray

$SshTestArgs = @(“-i”, “`”$SshKeyPath`””, “-o”, “StrictHostKeyChecking=no”, “-o”, “ConnectTimeout=5″, “-o”, “NumberOfPasswordPrompts=0″, “login@${SrvIP}”, “echo 1″)
$TestResult = Start-Process -FilePath “ssh.exe” -ArgumentList $SshTestArgs -NoNewWindow -Wait -PassThru -ErrorAction SilentlyContinue

if ($null -eq $TestResult -or $TestResult.ExitCode -ne 0) {
Write-Host “[ЗБІЙ АВТОРИЗАЦІЇ] Сервер $SrvName ($SrvIP) відхилив ключ або недоступний. Пропуск!” -ForegroundColor Red
$FailedAuthServers += $SrvName
continue
}

Write-Host “[СТАРТ] Авторизація успішна! Запуск копіювання для: $SrvName ($SrvIP)…” -ForegroundColor Cyan

# Запуск паралельного фонового потоку
$Job = Start-Job -Name “Backup_$SrvName” -ArgumentList $SshKeyPath, $SrvIP, $SrvPath, $LocalDest, $SrvName -ScriptBlock {
param($Key, $IP, $Path, $Dest, $Name)

$SshOptions = @(“-o”, “StrictHostKeyChecking=no”, “-o”, “ConnectTimeout=10″)

$RemotePathTxt = “login@${IP}:${Path}\File1*.txt
& scp.exe -i “$Key” $SshOptions $RemotePathTxt $Dest 2>$null

$RemotePathDb = “login@${IP}:${Path}File2*.db
& scp.exe -i “$Key” $SshOptions $RemotePathDb $Dest 2>$null
return “Done”
}
$Jobs += $Job
}

# — ЦИКЛ ОЧІКУВАННЯ ДЛЯ ПРАЦЮЮЧИХ ПОТОКІВ (БЕЗЛІМІТНИЙ ДЛЯ ВЕЛИКИХ ФАЙЛІВ) —
if ($Jobs.Count -gt 0) {
Write-Host “—————————————-” -ForegroundColor Yellow
Write-Host “Очікування завершення завантаження файлів з працюючих серверів…” -ForegroundColor Yellow
$AllDone = $false
while (-not $AllDone) {
Start-Sleep -Seconds 2
$RunningJobs = $Jobs | Where-Object { $_.State -eq “Running” }
if ($RunningJobs.Count -eq 0) { $AllDone = $true }
}
$Jobs | Remove-Job
}

$EndTime = Get-Date
$TotalTime = $EndTime – $StartTime
$ElapsedTime = [string]::Format(“{0:00}:{1:00}:{2:00}”, $TotalTime.Hours, $TotalTime.Minutes, $TotalTime.Seconds)

$ReportTimestamp = $EndTime.ToString(“yyyy_MM_dd_HH_mm_ss”)
$ReportName = “backup_report_${ReportTimestamp}.txt”
$ReportPath = Join-Path $CurrentBackupDir $ReportName

$ReportContent = @(
“==========================================”,
“ЗВІТ ПРО ЦЕНТРАЛІЗОВАНИЙ БЕКАП ВЕРСІЇ v8.0″,
“==========================================”,
“Дата запуску: $($StartTime.ToString(‘dd.MM.yyyy’))”,
“Час початку: $($StartTime.ToString(‘HH:mm:ss’))”,
“Час закінчення: $($EndTime.ToString(‘HH:mm:ss’))”,
“Загальний час роботи: $ElapsedTime”,
“==========================================”,
“СТАТУС ЗАВАНТАЖЕННЯ СЕРВЕРІВ:”
)

Write-Host “—————————————-” -ForegroundColor Cyan
foreach ($Server in $ServersList) {
$SrvName = $Server.Name
$SrvIP = $Server.IP
$FolderName = “${SrvName}_${SrvIP}”
$TargetFolder = Join-Path $CurrentBackupDir $FolderName

if ($FailedAuthServers -contains $SrvName) {
Write-Host “ERROR: Сервер $SrvName ($SrvIP) недоступний. Порожню папку видалено.” -ForegroundColor Red
$ReportContent += “[ПОМИЛКА] Сервер $SrvName ($SrvIP) НЕ скачано (Збій SSH-авторизації).”
if (Test-Path $TargetFolder) { Remove-Item -Path $TargetFolder -Recurse -Force -ErrorAction SilentlyContinue }
continue
}

$DownloadedFiles = @()
if (Test-Path $TargetFolder) { $DownloadedFiles = Get-ChildItem -Path $TargetFolder -File }

if ($DownloadedFiles.Count -gt 0) {
Write-Host “SUCCESS: Для сервера $SrvName ($SrvIP) скачано файлів: $($DownloadedFiles.Count). Запуск ZIP…” -ForegroundColor Green

if (Test-Path $7ZipPath) {
$ZipPath = Join-Path $CurrentBackupDir “${FolderName}.zip”
$7zArgs = @(“a”, “-tzip”, “-p$ArchivePassword”, “`”$ZipPath`””, “*”)
$Process = Start-Process -FilePath $7ZipPath -ArgumentList $7zArgs -WorkingDirectory $TargetFolder -Wait -NoNewWindow -PassThru

if ($Process.ExitCode -eq 0) {
Write-Host “Архів для $SrvName створено успішно. Видалення папки.” -ForegroundColor Gray
Remove-Item -Path $TargetFolder -Recurse -Force -ErrorAction SilentlyContinue
$ReportContent += “[УСПІШНО] Сервер $SrvName ($SrvIP) скачано та заархівовано.”
} else {
Write-Host “Помилка архівації для $SrvName! Код 7-Zip: $($Process.ExitCode)” -ForegroundColor Red
$ReportContent += “[УВАГА] Сервер $SrvName ($SrvIP) скачано, але сталася помилка ZIP-архівації.”
}
}
} else {
Write-Host “ERROR: Папка сервера $SrvName ($SrvIP) порожня. Видалення директорії…” -ForegroundColor Red
$ReportContent += “[ПОМИЛКА] Сервер $SrvName ($SrvIP) НЕ скачано (Помилка копіювання).”
if (Test-Path $TargetFolder) { Remove-Item -Path $TargetFolder -Recurse -Force -ErrorAction SilentlyContinue }
}
}
$ReportContent += “==========================================”
Set-Content -Path $ReportPath -Value $ReportContent -Force

# — БЛОК АВТООЧИЩЕННЯ СТАРИХ БЕКАПІВ (Старше 2 днів) —
if (Test-Path $LocalBackupRoot) {
$OldBackupFolders = Get-ChildItem -Path $LocalBackupRoot -Directory
foreach ($Folder in $OldBackupFolders) {
if ($Folder.Name -match ‘^(\d{2})_(\d{2})_(\d{4})$’) {
$FolderDate = [datetime]::ParseExact($Folder.Name, ‘dd_MM_yyyy’, $null)
if ($FolderDate -lt $StartTime.Date.AddDays(-$KeepDays)) {
Write-Host “Видалення старої копії: $($Folder.FullName)” -ForegroundColor Red
Remove-Item -Path $Folder.FullName -Recurse -Force -ErrorAction SilentlyContinue
}
}
}
}

Write-Host “========================================” -ForegroundColor Cyan
Write-Host “Централізований паралельний бекап версії v8.0 завершено!” -ForegroundColor Cyan
Write-Host “Детальний звіт збережено у файл: $ReportPath” -ForegroundColor Green
# ===== Telegram звіт =====
$SuccessCount = $ServersList.Count – $FailedAuthServers.Count

if ($FailedAuthServers.Count -gt 0) {
$FailedList = ($FailedAuthServers | ForEach-Object { “• $_” }) -join “`n”

$TelegramReport = @”
<b>