
Введение в предметную область
Всем привет! В своей рабочей деятельности я очень часто разворачиваю разные сервисы для внутренних нужд компании, и у всех них сейчас есть web-интерфейсы, для которых всегда приходится выпускать TLS сертификаты (иногда не только для взаимодействия по 443 порту, но и для отправки данных через разного рода агенты на сервер и так далее, вариантов масса, можно рассуждать долго). В какой-то момент я осознал, что за последнее время этих сервисов с выпущенными мной сертификатами накопилось немалое количество и я уже физически не помню, какой сертификат вскоре протухнет.
Сюда же добавляются пользователи ЭП, у которых также есть сертификаты безопасности, называющиеся в данном случае квалифицированным сертификатом ключа проверки электронной подписи (далее - КСКПЭП) и за их сроками тоже было бы неплохо проследить (хотя можно поспорить в ключе - чей сертификат, тот и следит за ним, но в эти споры мы вдаваться не будем, а окунёмся в решение задачи).
В связи с этим возникла мысль - как заставить компьютер самому отслеживать сроки действия всевозможных сертификатов и заранее уведомлять меня о скором истечении?
Немного нудятины в спойлере (если не терпится приступить - переходите к разделу "Реализация")
Скрытый текст
Скажу сразу, что задача довольно специфичная, не каждому это нужно, у всех свои условия работы, свои задачи. У кого-то в основном опубликованные в интернет сайты, сертификаты которых можно легко отследить специальными сервисами. Кто-то отслеживает КСКПЭП пользователей с помощью какого-нибудь JaCarta Management System, но это платный вариант и просит выделять ресурсы под сервер. Кому-то приходят уведомления на корпоративную почту от АУЦ, но тут тоже есть нюансы - не на всех сертификатах может быть указана именно та почта, на которую вы ждёте уведомление или что-то с сервисом рассылок произошло и вы не получили нужное письмо, или коллега случайно его прочитал и не сообщил вам.
Конкретно моя цель - штатными средствами Windows (не прибегая к сторонним сервисам, не прибегая к языкам, которых нет в стоковой сборке ОС) организовать отслеживание всевозможных сертификатов (поля, регламентируемые 795 приказом ФСБ - они же КСКПЭП или обычные сертификаты, которые я выпускаю сам и подписываю корпоративным root сертификатом) с разными расширениями (буду делать .crt и .cer) и отправкой уведомлений, если сертификат вот-вот протухнет. Ну и не менее важное - немного поупражняемся с PowerShell, хотя сейчас это не самое популярное решение, но те не менее лишним не будет. Попробуем сделать это!
Реализация
Итак, что мне нужно? Допустим, у меня есть несколько папок, в которые я буду складывать сертификаты. Для пользователей я буду делить их по годам (например, папки 2024, и 2025 год), и отдельная папка с сертификатами для разного рода информационных систем. Таким образом сразу организуем массив, в который можно будет добавлять пути к папкам для парсинга сертификатов (у меня это три сетевые папки):
$certFolders = @(
"\netdirectorycertsusers2024",
"\netdirectorycertsusers2025",
"\netdirectorycertssystems"
)
Теперь я создам переменную, в которую запишу количество дней. Если срок действия сертификата будет равен или будет меньше количества этих дней - мне будет приходить уведомление. Делается это так:
$expirationThreshold = 14
На этом этапе мне приходит в голову мысль, что я хочу получать не виндовые уведомления, а уведомление через bot telegram, чтобы я был в курсе даже в случае моего отсутствия на рабочем месте и мог передать информацию коллегам, например, если я в отпуске или на больничном. Рассматривать создание бота я не буду, гайдов и интернете полно и там всё очень просто. Если вас не устраивают такие уведомления, вы можете поменять их на оповещения самой винды. Но у меня это опять две переменные, в которые вам нужно ввести свои данные (а не данные моего бота, ха-ха, думали забуду стереть?):
$botToken = "TokenBotaTelegi"
$chatId = "IdentificatorVashegoChata"
Организуем функцию отправки уведомления в телегу:
function Send-TelegramMessage {
param (
[string]$message
)
$encodedMessage = [uri]::EscapeDataString($message)
$uri = "https://api.telegram.org/bot$botToken/sendMessage?chat_id=$chatId&text=$encodedMessage&parse_mode=HTML"
Invoke-RestMethod -Uri $url -Method Post}
Тут важно уточнить - я хочу, чтобы уведомления, приходящие мне в телегу были отформатированы, а именно, чтобы некоторые части сообщения были выделены жирным, из-за этого нужно использовать метод [uri]::EscapeDataString() для кодирования сообщения перед передачей URL запроса. Если использовать System.Web.HttpUtility то скрипт тоже будет работать, но если мы поместим этот скрипт в планировщик задач (а мы его таки туда поместим) то оно внезапно перестанет работать.
Ладно, теперь перейдём к самим сертификатам. Реализуем получение всех файлов с расширением .crt и .cer в вышеуказанных папках:
foreach ($certFolder in $certFolders) {
$certFiles = Get-ChildItem -Path $certFolder -Include *.cer,*.crt -Recurse
Вспомним, чего мы хотим (тут картинка из мема) - мы хотим знать, чей сертификат и когда истекает. Что мы для этого делаем? Что нам для этого нужно? Для этого как минимум нужно у каждого сертификата в разделе субъект прочитать поля CN и NotAfter. Для этого сначала создадим объект X509Certificate2, чтобы загрузить в него данные из сертификата.
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
try {
$cert.Import($certFile.FullName)
Теперь извлечём поле CN из строки субъекта:
if ($cert.Subject -match 'CN=([^,]+)') {
$cn = $matches[1]
} else {
$cn = "Common Name (CN) не найдено."
}
Пришло время вычислить количество дней до окончания срока действия сертификата и положим результат в переменную. Сделаем это по-простому:
$daysUntilExpiration = ($cert.NotAfter - (Get-Date)).Days
Отправим уведомление через бота Telegram, если осталось менее $expirationThreshold дней до истечения срока действия сертификата. Причём в случае, если срок действия сертификата равен нулю, мы уведомим, что сертификат уже истёк, а не будем писать, что он заканчивается через, мать его, ноль дней. Также воспользуемся выделением текста жирным, не зря же мы заморачивались с [uri]::EscapeDataString(). Как-то так:
if ($daysUntilExpiration -lt $expirationThreshold) {
if ($daysUntilExpiration -ge 0) {
$message = "Сертификат '$cn' заканчивается через <b>$daysUntilExpiration</b> дней."
} else {
$message = "Сертификат '$cn' срок действия <b>истёк!</b>"
}
Send-TelegramMessage -message $message
}
}
Ну и закончим всё это безобразие тем, что предусмотрим отправку уведомления и в случае, если сертификат из файла прочитать не удалось. Зачем я это сделал? Да чтобы знать, что нужно обратить внимание на какой-то из сертификатов, ведь в ином случае проверка на нём просто не будет работать и никто мне про срок на этом сертификате не скажет. У меня их реально немало и те, что я выпускаю сам на своём root сертификате бывают очень разные, потому как требования у каждого сервиса отличаются. Конечно, есть RFC, но по факту кто во что горазд, даже с тем же CN, в который запрещено писать адрес домена, если этот домен записывается в SAN (например полем DNS.1 = test.local.it или IP.1 = 192.168.1.1)
If a subjectAltName extension of type dNSName is present, that MUST be used as the identity. Otherwise, the (most specific) Common Name field in the Subject field of the certificate MUST be used.
Но по факту в документации к некоторым продуктам написано, что поле CN всё-равно должно содержать адрес домена несмотря на то, что все адреса перечислены в SAN.
Ну да ладно, не будем уходить в дебри, а закончим наш код:
catch {
$errorMessage = "Не удалось прочитать сертификат из файла $($certFile.FullName): $_"
Write-Host $errorMessage
Send-TelegramMessage -message $errorMessage
} finally {
if ($cert) {
$cert.Dispose()
}
}
}
}
Теперь соберём все эти ошмётки кода в единый организьм, чтобы его можно было отправить в PowerShell ISE и проверить, как оно работает.
Полный код:
$certFolders = @(
"\netdirectorycertsusers2024",
"\netdirectorycertsusers2025",
"\netdirectorycertssystems"
)
$expirationThreshold = 14
$botToken = "TokenBotaTelegi"
$chatId = "IdentificatorVashegoChata"
function Send-TelegramMessage {
param (
[string]$message
)
$encodedMessage = [uri]::EscapeDataString($message)
$url = "https://api.telegram.org/bot$botToken/sendMessage?chat_id=$chatId&text=$encodedMessage&parse_mode=HTML"
Invoke-RestMethod -Uri $url -Method Post
}
foreach ($certFolder in $certFolders) {
$certFiles = Get-ChildItem -Path $certFolder -Include *.cer,*.crt -Recurse
foreach ($certFile in $certFiles) {
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
try {
$cert.Import($certFile.FullName)
if ($cert.Subject -match 'CN=([^,]+)') {
$cn = $matches[1]
} else {
$cn = "Common Name (CN) не найдено."
}
$daysUntilExpiration = ($cert.NotAfter - (Get-Date)).Days
if ($daysUntilExpiration -lt $expirationThreshold) {
if ($daysUntilExpiration -ge 0) {
$message = "Сертификат '$cn' заканчивается через <b>$daysUntilExpiration</b> дней."
} else {
$message = "Сертификат '$cn' срок действия <b>истёк!</b>"
}
Send-TelegramMessage -message $message
}
} catch {
$errorMessage = "Не удалось прочитать сертификат из файла $($certFile.FullName): $_"
Write-Host $errorMessage
Send-TelegramMessage -message $errorMessage
} finally {
if ($cert) {
$cert.Dispose()
}
}
}
}
Это код, который вы можете использовать в своих задачах, что-то в нём поменять, улучшить, доработать. Пользуйтесь и размножайтесь!
Результаты
Итак, после того, как я вставил ID своего бота, ID своего чата, сложил интересующие меня сертификаты в локальную папку, вписал её путь, добавил количество дней к дате сертификата, сохранил это всё как .ps1 скрипт.
Открываю планировщик заданий Windows, создаю новую задачу:
Скрытый текст
Не забудьте только запустить планировщик с наивысшими правами, иначе схватите ошибку при сохранении задания, в котором будете просить запустить powershell.exe.

Теперь открываю свой telegram и вижу долгожданное уведомление от бота:

Отлично! Уведомления приходят. Корректно отрабатывает форматирование текста, корректно читает поля у КСКПЭП и TLS сертификатов, а также у расширений .cer и .crt.
Теперь я в курсе, какой сертификат из моих двухсот сертификатов заканчивается и у меня меньше шансов словить ошибку сервиса из-за невалидности сертификата. Скажу сразу - способ не ультимативный и не панацея, но он работает, а ещё мы немного освежили знания PowerShell.
Всем большое спасибо за прочтение. Конечный вариант кода я прямо отсюда скопировал себе, запустил - работает. Значит по крайней мере в итоговом варианте кода ошибок быть не должно, им можно пользоваться =)
P.S.:
Что ещё хотел сказать... Если вдруг эта статья покажется кому-то интересной и я словлю не очень много минусов за неё, то, быть может, я опубликую следующую, в которой расскажу, как написать в PowerShell скриптик, который с помощью OpenSSL (детально разберём процесс установки для совсем новичков) локально выпускает TLS сертификаты для ваших внутренних (и не очень) сервисов. В нём можно будет выпускать сертификаты с нужными полями, нужными признаками (сертификат уровня CA, то есть все политики выдачи и все политики применения, или сертификат конечного субъекта). При этом они будут подписаны вышестоящим root сертификатом, который можно будет распространить на корпоративные компьютеры, чтобы они автоматически доверяли всем сертификатам, которые им были подписаны при выпуске.
Всё это я использую в своей работе и, быть может, вам это тоже пригодится.
До встречи!
Автор: itshnick88