PowerShell / Безопасность
SSL certificate security monitoring

Мониторинг SSL сертификатов через PowerShell: не допусти простой из-за просроченного сертификата

АйТи Фреш  ·  23 марта 2026  ·  ~18 минут чтения

Сертификат истёк в пятницу вечером. Сайт интернет-магазина начал показывать «Небезопасное соединение» — браузеры заблокировали переход, а конверсия за выходные упала на 70%. В другом случае корпоративный Exchange перестал принимать входящую почту от внешних серверов: STARTTLS-рукопожатие обрывалось из-за просроченного сертификата на SMTP-порту, и письма клиентов уходили в никуда три дня, пока не нашли причину. Третья история — VPN-шлюз с SSL-сертификатом для WebVPN: сотрудники на удалёнке потеряли доступ в корпоративную сеть ровно в день дедлайна квартального отчёта. Всё это объединяет одно: сертификат можно было обновить заранее, если бы кто-то его проверял.

SSL сертификаты безопасность

Ручная проверка через браузер — нажал на замочек, увидел дату — работает, пока у тебя пять сайтов и хорошая память. Когда доменов двадцать, а ещё есть внутренние серверы с LDAPS, RDP, почтовыми реле и VPN-шлюзами — ручной подход превращается в лотерею. PowerShell позволяет автоматизировать проверку срока сертификата на любом количестве узлов, запустить её по расписанию и получить алёрт до того, как браузер выдаст страшную красную страницу.

О чём эта статья. Полный путь от однострочника до промышленного скрипта: массовая проверка из CSV, цветовая индикация, email-алёрты, Task Scheduler, внутренние сертификаты (не только HTTPS), HTML-отчёт для руководства и интеграция с Zabbix/Nagios. В конце — готовый скрипт «всё в одном» на ~50 строк.

Как PowerShell читает сертификаты — через .NET и System.Net.Security

PowerShell работает поверх .NET, и именно .NET-классы выполняют всю криптографическую работу. Когда нужно получить SSL-сертификат удалённого сервера, используется класс System.Net.Sockets.TcpClient для установки TCP-соединения, затем поверх него создаётся System.Net.Security.SslStream — зашифрованный поток, который при инициализации выполняет TLS-рукопожатие.

После успешного рукопожатия у объекта SslStream появляется свойство RemoteCertificate типа System.Security.Cryptography.X509Certificates.X509Certificate2. Этот объект содержит всё: NotAfter (дата истечения), NotBefore (дата выдачи), Subject, Issuer, список SubjectAlternativeNames и отпечаток Thumbprint.

Альтернативный путь — Invoke-WebRequest с параметром -Certificate, но он неудобен: нельзя легко перехватить сертификат внутри запроса без кастомного обработчика. Метод через TcpClient + SslStream даёт прямой доступ к объекту сертификата и работает на любом TCP-порту, не только на 443.

Важно: по умолчанию SslStream проверяет цепочку доверия и отклоняет самоподписанные сертификаты. Для внутренней инфраструктуры нужно передавать кастомный RemoteCertificateValidationCallback, который возвращает $true всегда — только для чтения даты, не для доверия.

Базовый скрипт: проверка одного сайта

Начнём с минимального рабочего примера — он проверяет один домен и выводит количество дней до истечения сертификата.

# Домен для проверки и порт (443 — стандартный HTTPS)
$hostname = "example.com"
$port     = 443

# Создаём TCP-соединение с таймаутом 5 секунд
$tcp = New-Object System.Net.Sockets.TcpClient
$tcp.ConnectAsync($hostname, $port).Wait(5000) | Out-Null

# Создаём SSL-поток поверх TCP-соединения
# Callback всегда возвращает $true — нам важна дата, а не доверие
$ssl = New-Object System.Net.Security.SslStream(
    $tcp.GetStream(), $false,
    { $true }   # RemoteCertificateValidationCallback
)

# Выполняем TLS-рукопожатие: сервер передаёт свой сертификат
$ssl.AuthenticateAsClient($hostname)

# Извлекаем сертификат из установленного соединения
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(
    $ssl.RemoteCertificate
)

# Вычисляем количество дней до истечения
$daysLeft = ($cert.NotAfter - (Get-Date)).Days

# Выводим результат
Write-Host "Домен:         $hostname"
Write-Host "Истекает:      $($cert.NotAfter.ToString('dd.MM.yyyy'))"
Write-Host "Дней осталось: $daysLeft"
Write-Host "Выдан:         $($cert.Issuer)"

# Закрываем соединения
$ssl.Close()
$tcp.Close()

Скрипт выполняется за долю секунды и возвращает точную дату истечения. Если сервер недоступен или сертификат уже просрочен (TLS-соединение всё равно устанавливается, просто NotAfter будет в прошлом), скрипт корректно обработает оба случая.

Промышленный скрипт: массовая проверка из файла

Для проверки десятков доменов читаем список из CSV-файла и используем фоновые задания (Start-Job) для параллельного выполнения. Это сокращает общее время проверки в разы.

Файл domains.csv имеет простой формат:

hostname,port,description
itfresh.ru,443,Основной сайт
mail.itfresh.ru,443,Веб-почта
crm.itfresh.ru,443,CRM-система
vpn.itfresh.ru,443,VPN-шлюз
ldap.corp.local,636,Контроллер домена LDAPS

Скрипт массовой проверки:

# Путь к файлу со списком доменов
$csvPath      = "C:\Scripts\SSL\domains.csv"
$warningDays  = 30   # жёлтая зона
$criticalDays = 10   # красная зона

# Функция проверки одного узла (запускается в отдельном job)
$checkScript = {
    param($hostname, $port, $description)

    $result = [PSCustomObject]@{
        Description = $description
        Hostname    = $hostname
        Port        = $port
        DaysLeft    = $null
        Expiry      = $null
        Issuer      = $null
        Status      = "ERROR"
        Error       = ""
    }

    try {
        $tcp = New-Object System.Net.Sockets.TcpClient
        if (-not $tcp.ConnectAsync($hostname, $port).Wait(8000)) {
            $result.Error = "Таймаут подключения"
            return $result
        }
        $ssl = New-Object System.Net.Security.SslStream(
            $tcp.GetStream(), $false, { $true }
        )
        $ssl.AuthenticateAsClient($hostname)
        $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(
            $ssl.RemoteCertificate
        )
        $daysLeft        = ($cert.NotAfter - (Get-Date)).Days
        $result.DaysLeft = $daysLeft
        $result.Expiry   = $cert.NotAfter.ToString("dd.MM.yyyy")
        $result.Issuer   = $cert.Issuer -replace "CN=","" -replace ",.*",""
        $result.Status   = if ($daysLeft -lt 0)        { "EXPIRED"  }
                           elseif ($daysLeft -lt 10)   { "CRITICAL" }
                           elseif ($daysLeft -lt 30)   { "WARNING"  }
                           else                        { "OK"       }
        $ssl.Close(); $tcp.Close()
    }
    catch { $result.Error = $_.Exception.Message }
    return $result
}

# Читаем домены и запускаем параллельные задания
$domains = Import-Csv $csvPath
$jobs = foreach ($d in $domains) {
    Start-Job -ScriptBlock $checkScript -ArgumentList $d.hostname, $d.port, $d.description
}

# Ждём завершения всех заданий (максимум 30 секунд)
$jobs | Wait-Job -Timeout 30 | Out-Null

# Собираем результаты и выводим таблицу
$results = $jobs | Receive-Job | Sort-Object DaysLeft
$jobs | Remove-Job -Force

$results | Format-Table Description, Hostname, DaysLeft, Expiry, Status -AutoSize

Цветовая индикация: зелёный / жёлтый / красный

Вывод в консоль PowerShell с цветовым выделением делает таблицу мгновенно читаемой. Три статуса покрывают все ситуации:

Статус Условие Цвет Действие
OK Более 30 дней Зелёный Ничего не делать
WARNING 10–30 дней Жёлтый Запланировать обновление
CRITICAL / EXPIRED Менее 10 дней или просрочен Красный Немедленное обновление
foreach ($r in $results) {
    $color = switch ($r.Status) {
        "OK"       { "Green"   }
        "WARNING"  { "Yellow"  }
        "CRITICAL" { "Red"     }
        "EXPIRED"  { "Magenta" }
        default    { "Gray"    }
    }
    $line = "{0,-30} {1,-25} {2,8} дн.  [{3}]" -f `
            $r.Description, $r.Hostname, $r.DaysLeft, $r.Status
    Write-Host $line -ForegroundColor $color
}

Отправка email-алёрта при приближении срока

Когда скрипт находит сертификаты в состоянии WARNING или CRITICAL, он должен немедленно отправить письмо ответственному администратору. PowerShell содержит встроенный командлет Send-MailMessage.

# Параметры SMTP-сервера
$smtpParams = @{
    SmtpServer  = "smtp.itfresh.ru"
    Port        = 587
    UseSsl      = $true
    Credential  = Get-Credential   # или хранить в защищённом файле
    From        = "monitoring@itfresh.ru"
    To          = "admin@itfresh.ru"
}

# Фильтруем только проблемные сертификаты
$alerts = $results | Where-Object { $_.Status -in "WARNING","CRITICAL","EXPIRED" }

if ($alerts) {
    # Формируем тело письма в HTML
    $tableRows = $alerts | ForEach-Object {
        $bgColor = if ($_.Status -eq "WARNING") { "#fff3cd" } else { "#f8d7da" }
        "
           $($_.Description)
           $($_.Hostname)
           $($_.DaysLeft)
           $($_.Expiry)
           $($_.Status)
         "
    }
    $body = @"
<h2>SSL-сертификаты требуют внимания</h2>
<table border='1' cellpadding='6' cellspacing='0' style='border-collapse:collapse'>
  <tr><th>Описание</th><th>Домен</th><th>Дней</th><th>Истекает</th><th>Статус</th></tr>
  $($tableRows -join '')
</table>
<p>Сформировано: $(Get-Date -Format 'dd.MM.yyyy HH:mm')</p>
"@
    Send-MailMessage @smtpParams `
        -Subject "ALERT: Истекают SSL-сертификаты ($($alerts.Count) шт.)" `
        -Body $body -BodyAsHtml
}
Совет по безопасности. Не храните пароль SMTP в открытом виде в скрипте. Используйте Export-Clixml для сохранения зашифрованных учётных данных: Get-Credential | Export-Clixml "C:\Scripts\SSL\creds.xml", а при чтении: $cred = Import-Clixml "C:\Scripts\SSL\creds.xml". Шифрование привязано к учётной записи Windows и текущей машине.

Интеграция с Task Scheduler

Скрипт должен запускаться автоматически — каждый день в 8:00, до начала рабочего дня. Самый надёжный способ — создать задачу через PowerShell без ручного взаимодействия с GUI Планировщика.

# Параметры задачи
$taskName   = "SSL Certificate Monitor"
$scriptPath = "C:\Scripts\SSL\Check-SSLCerts.ps1"
$logPath    = "C:\Scripts\SSL\Logs\ssl-check.log"

# Действие: запуск PowerShell со скриптом, вывод в лог
$action = New-ScheduledTaskAction `
    -Execute "powershell.exe" `
    -Argument "-NonInteractive -ExecutionPolicy Bypass -File `"$scriptPath`" >> `"$logPath`" 2>&1"

# Триггер: каждый день в 08:00
$trigger = New-ScheduledTaskTrigger -Daily -At "08:00"

# Настройки: запускать под системной учёткой, даже если никто не залогинен
$settings = New-ScheduledTaskSettingsSet `
    -ExecutionTimeLimit (New-TimeSpan -Minutes 10) `
    -RestartCount 2 `
    -RestartInterval (New-TimeSpan -Minutes 5)

$principal = New-ScheduledTaskPrincipal `
    -UserId "SYSTEM" -RunLevel Highest

# Регистрируем задачу
Register-ScheduledTask `
    -TaskName $taskName `
    -Action $action `
    -Trigger $trigger `
    -Settings $settings `
    -Principal $principal `
    -Description "Ежедневная проверка SSL-сертификатов" `
    -Force

После регистрации задача появится в Планировщике в разделе корневого каталога. Проверить её можно командой Get-ScheduledTask -TaskName "SSL Certificate Monitor", запустить вручную — Start-ScheduledTask -TaskName "SSL Certificate Monitor".

Проверка внутренних сертификатов (не только HTTPS)

Большинство статей по мониторингу SSL ограничиваются проверкой порта 443. Но в корпоративной инфраструктуре сертификаты живут везде.

LDAPS (порт 636) — контроллеры домена Active Directory. Просроченный сертификат ломает аутентификацию LDAPS-клиентов и может вывести из строя всё, что к нему обращается.

RDP (порт 3389) — серверы с пользовательскими SSL-сертификатами для Remote Desktop. Клиенты получат предупреждение безопасности и могут отказаться подключаться.

SMTP TLS (порт 587 или 465) — почтовые реле с принудительным STARTTLS. Внешние серверы отклонят соединение.

Хранилище Windows — сертификаты, установленные локально, можно проверить без сетевого соединения:

# Проверка сертификатов в локальном хранилище компьютера
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store(
    "My", "LocalMachine"
)
$store.Open("ReadOnly")

$expiringSoon = $store.Certificates | Where-Object {
    $_.NotAfter -lt (Get-Date).AddDays(30) -and $_.NotAfter -gt (Get-Date)
}

$expiringSoon | Select-Object Subject, NotAfter,
    @{N="DaysLeft"; E={($_.NotAfter - (Get-Date)).Days}},
    Thumbprint | Format-Table -AutoSize

$store.Close()

Наш базовый скрипт с TcpClient + SslStream работает для всех TCP-сервисов — достаточно указать нужный порт. Единственное отличие для LDAPS: при вызове AuthenticateAsClient нужно передать имя хоста точно как в сертификате (обычно это FQDN контроллера домена).

Экспорт в HTML-отчёт

Красивый HTML-отчёт удобно открывать в браузере, прикладывать к задаче в Jira или отправлять руководству. PowerShell строит его из тех же данных $results.

$reportPath = "C:\Scripts\SSL\Reports\ssl-report-$(Get-Date -Format 'yyyyMMdd').html"

$rows = $results | ForEach-Object {
    $bg = switch ($_.Status) {
        "OK"       { "#d4edda" }
        "WARNING"  { "#fff3cd" }
        "CRITICAL" { "#f8d7da" }
        "EXPIRED"  { "#f5c6cb" }
        default    { "#e2e3e5" }
    }
    "<tr style='background:$bg'>
       <td>$($_.Description)</td>
       <td>$($_.Hostname):$($_.Port)</td>
       <td>$($_.Expiry)</td>
       <td style='text-align:center;font-weight:bold'>$($_.DaysLeft)</td>
       <td>$($_.Issuer)</td>
       <td><b>$($_.Status)</b></td>
     </tr>"
}

$html = @"
<!DOCTYPE html><html lang='ru'><head>
<meta charset='UTF-8'>
<title>SSL Report $(Get-Date -Format 'dd.MM.yyyy')</title>
<style>body{font-family:Arial;padding:20px} table{border-collapse:collapse;width:100%}
th{background:#1a0a00;color:#fff;padding:10px} td{padding:8px;border-bottom:1px solid #ddd}</style>
</head><body>
<h1>Отчёт по SSL-сертификатам</h1>
<p>Сформирован: $(Get-Date -Format 'dd.MM.yyyy HH:mm')</p>
<table><tr>
  <th>Описание</th><th>Сервер</th><th>Дата истечения</th>
  <th>Дней</th><th>Издатель</th><th>Статус</th>
</tr>$($rows -join '')</table></body></html>
"@

$html | Out-File $reportPath -Encoding UTF8
Write-Host "Отчёт сохранён: $reportPath" -ForegroundColor Cyan

Интеграция с мониторингом (Zabbix / Nagios)

Скрипт легко адаптируется для работы с системами мониторинга через стандартные exit-коды: 0 — OK, 1 — WARNING, 2 — CRITICAL. Nagios и Zabbix читают их автоматически.

# Nagios/Zabbix-совместимая обёртка для проверки одного домена
param(
    [string]$Hostname,
    [int]$Port      = 443,
    [int]$WarnDays  = 30,
    [int]$CritDays  = 10
)

try {
    $tcp = New-Object System.Net.Sockets.TcpClient
    if (-not $tcp.ConnectAsync($Hostname, $Port).Wait(8000)) {
        Write-Output "UNKNOWN: Таймаут подключения к ${Hostname}:${Port}"
        exit 3
    }
    $ssl = New-Object System.Net.Security.SslStream($tcp.GetStream(), $false, { $true })
    $ssl.AuthenticateAsClient($Hostname)
    $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(
        $ssl.RemoteCertificate
    )
    $days = ($cert.NotAfter - (Get-Date)).Days
    $ssl.Close(); $tcp.Close()

    if ($days -lt 0) {
        Write-Output "CRITICAL: Сертификат просрочен $($cert.NotAfter.ToString('dd.MM.yyyy')) | days=$days"
        exit 2
    } elseif ($days -lt $CritDays) {
        Write-Output "CRITICAL: Осталось $days дней до $($cert.NotAfter.ToString('dd.MM.yyyy')) | days=$days"
        exit 2
    } elseif ($days -lt $WarnDays) {
        Write-Output "WARNING: Осталось $days дней до $($cert.NotAfter.ToString('dd.MM.yyyy')) | days=$days"
        exit 1
    } else {
        Write-Output "OK: Сертификат действителен $days дней до $($cert.NotAfter.ToString('dd.MM.yyyy')) | days=$days"
        exit 0
    }
}
catch {
    Write-Output "UNKNOWN: $($_.Exception.Message)"
    exit 3
}

Для Zabbix настройте внешнюю проверку (External Check) или используйте UserParameter в Zabbix Agent: UserParameter=ssl.days[*],powershell -File C:\Scripts\SSL\Check-SSL-Single.ps1 -Hostname $1 -Port $2. Zabbix передаст имя хоста и порт как аргументы и получит числовое значение дней для построения графика и триггеров.

Скрипт «всё в одном»

Финальный скрипт объединяет все функции: загрузку CSV, параллельную проверку, цветовой вывод, email-алёрт и HTML-отчёт. Достаточно один раз настроить параметры в начале файла и добавить задачу в Task Scheduler.

#Requires -Version 5.1
# Check-SSLCerts.ps1 — мониторинг SSL-сертификатов v2.0
# АйТи Фреш | itfresh.ru

param(
    [string]$CsvPath    = "C:\Scripts\SSL\domains.csv",
    [string]$ReportDir  = "C:\Scripts\SSL\Reports",
    [string]$SmtpServer = "smtp.itfresh.ru",
    [int]   $SmtpPort   = 587,
    [string]$MailFrom   = "monitoring@itfresh.ru",
    [string]$MailTo     = "admin@itfresh.ru",
    [string]$CredFile   = "C:\Scripts\SSL\creds.xml",
    [int]   $WarnDays   = 30,
    [int]   $CritDays   = 10
)

$checkBlock = {
    param($h, $p, $d)
    $r = [PSCustomObject]@{
        Desc="$d"; Host="$h"; Port="$p"
        Days=$null; Expiry=$null; Issuer=$null; Status="ERROR"; Err=""
    }
    try {
        $tcp = New-Object System.Net.Sockets.TcpClient
        if (-not $tcp.ConnectAsync($h,$p).Wait(8000)) { $r.Err="Timeout"; return $r }
        $ssl = New-Object System.Net.Security.SslStream($tcp.GetStream(),$false,{ $true })
        $ssl.AuthenticateAsClient($h)
        $cert    = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($ssl.RemoteCertificate)
        $r.Days   = ($cert.NotAfter-(Get-Date)).Days
        $r.Expiry = $cert.NotAfter.ToString("dd.MM.yyyy")
        $r.Issuer = ($cert.Issuer -replace "CN=","" -replace ",.*","").Trim()
        $r.Status = if ($r.Days -lt 0) {"EXPIRED"} elseif ($r.Days -lt $CritDays) {"CRITICAL"} `
                    elseif ($r.Days -lt $WarnDays) {"WARNING"} else {"OK"}
        $ssl.Close(); $tcp.Close()
    } catch { $r.Err = $_.Exception.Message }
    $r
}

$domains = Import-Csv $CsvPath
$jobs    = $domains | ForEach-Object {
    Start-Job $checkBlock -Args $_.hostname, $_.port, $_.description
}
$jobs | Wait-Job -Timeout 30 | Out-Null
$results = $jobs | Receive-Job | Sort-Object Days
$jobs    | Remove-Job -Force

$colors = @{ OK="Green"; WARNING="Yellow"; CRITICAL="Red"; EXPIRED="Magenta"; ERROR="Gray" }
foreach ($r in $results) {
    $line = "{0,-28} {1,-22} {2,6} дн  [{3}]" -f $r.Desc, $r.Host, $r.Days, $r.Status
    Write-Host $line -ForegroundColor $colors[$r.Status]
}

$alerts = $results | Where-Object { $_.Status -in "WARNING","CRITICAL","EXPIRED" }
if ($alerts -and (Test-Path $CredFile)) {
    $cred  = Import-Clixml $CredFile
    $rows  = $alerts | ForEach-Object {
        "<tr><td>$($_.Desc)</td><td>$($_.Host)</td><td>$($_.Days)</td><td>$($_.Expiry)</td><td>$($_.Status)</td></tr>"
    }
    $body = "<h2>SSL Alert</h2><table border=1 cellpadding=6 style='border-collapse:collapse'>" +
            "<tr><th>Описание</th><th>Домен</th><th>Дней</th><th>Истекает</th><th>Статус</th></tr>" +
            ($rows -join '') + "</table>"
    Send-MailMessage -SmtpServer $SmtpServer -Port $SmtpPort -UseSsl -Credential $cred `
        -From $MailFrom -To $MailTo `
        -Subject "SSL ALERT: $($alerts.Count) сертификатов требуют внимания" `
        -Body $body -BodyAsHtml
}

$null = New-Item $ReportDir -ItemType Directory -Force
$rFile = Join-Path $ReportDir "ssl-$(Get-Date -Format yyyyMMdd).html"
$tRows = $results | ForEach-Object {
    $bg = @{OK="#d4edda";WARNING="#fff3cd";CRITICAL="#f8d7da";EXPIRED="#f5c6cb";ERROR="#e2e3e5"}[$_.Status]
    "<tr style='background:$bg'><td>$($_.Desc)</td><td>$($_.Host):$($_.Port)</td><td>$($_.Expiry)</td><td>$($_.Days)</td><td>$($_.Status)</td></tr>"
}
"<!DOCTYPE html><html><head><meta charset='UTF-8'><title>SSL Report</title></head><body>" +
"<h1>SSL Report $(Get-Date -Format 'dd.MM.yyyy')</h1>" +
"<table border=1 cellpadding=6 style='border-collapse:collapse'>$($tRows -join '')</table></body></html>" |
Out-File $rFile -Encoding UTF8
Write-Host "`nОтчёт сохранён: $rFile" -ForegroundColor Cyan

Итог: что мы построили

Просроченный сертификат — одна из тех аварий, которых легче всего избежать. В отличие от отказа железа или DDoS, дата истечения известна заранее — за год. Скрипт PowerShell превращает эту информацию из «кто-то помнит» в «система предупредила за 30 дней». Добавьте файл domains.csv со всеми узлами, зарегистрируйте задачу в Task Scheduler — и забудьте о внезапных простоях из-за SSL.

Нужна помощь специалистов?

ООО «АйТи Фреш» возьмёт это на себя

Не хватает времени или своих специалистов — мы настроим, оптимизируем и возьмём вашу IT-инфраструктуру на постоянное сопровождение. Работаем с юридическими лицами в Москве и регионах. Собственный дата-центр, команда из 8 серверов Dell Xeon Platinum 8280 на базе МТС.

15+лет опыта
25+клиентов
40Gсвоя сеть
24/7поддержка