Введение в предметную область


Всем привет! В своей рабочей деятельности я очень часто разворачиваю разные сервисы для внутренних нужд компании, и у всех них сейчас есть web-интерфейсы, для которых всегда приходится выпускать TLS сертификаты (иногда не только для взаимодействия по 443 порту, но и для отправки данных через разного рода агенты на сервер и так далее, вариантов масса, можно рассуждать долго). В какой-то момент я осознал, что за последнее время этих сервисов с выпущенными мной сертификатами накопилось немалое количество и я уже физически не помню, какой сертификат вскоре протухнет.

Сюда же добавляются пользователи ЭП, у которых также есть сертификаты безопасности, называющиеся в данном случае квалифицированным сертификатом ключа проверки электронной подписи (далее - КСКПЭП) и за их сроками тоже было бы неплохо проследить (хотя можно поспорить в ключе - чей сертификат, тот и следит за ним, но в эти споры мы вдаваться не будем, а окунёмся в решение задачи).

В связи с этим возникла мысль - как заставить компьютер самому отслеживать сроки действия всевозможных сертификатов и заранее уведомлять меня о скором истечении?

Немного нудятины в спойлере (если не терпится приступить - переходите к разделу "Реализация")

Скрытый текст

Скажу сразу, что задача довольно специфичная, не каждому это нужно, у всех свои условия работы, свои задачи. У кого-то в основном опубликованные в интернет сайты, сертификаты которых можно легко отследить специальными сервисами. Кто-то отслеживает КСКПЭП пользователей с помощью какого-нибудь JaCarta Management System, но это платный вариант и просит выделять ресурсы под сервер. Кому-то приходят уведомления на корпоративную почту от АУЦ, но тут тоже есть нюансы - не на всех сертификатах может быть указана именно та почта, на которую вы ждёте уведомление или что-то с сервисом рассылок произошло и вы не получили нужное письмо, или коллега случайно его прочитал и не сообщил вам.

Конкретно моя цель - штатными средствами Windows (не прибегая к сторонним сервисам, не прибегая к языкам, которых нет в стоковой сборке ОС) организовать отслеживание всевозможных сертификатов (поля, регламентируемые 795 приказом ФСБ - они же КСКПЭП или обычные сертификаты, которые я выпускаю сам и подписываю корпоративным root сертификатом) с разными расширениями (буду делать .crt и .cer) и отправкой уведомлений, если сертификат вот-вот протухнет. Ну и не менее важное - немного поупражняемся с PowerShell, хотя сейчас это не самое популярное решение, но те не менее лишним не будет. Попробуем сделать это!

Реализация


Итак, что мне нужно? Допустим, у меня есть несколько папок, в которые я буду складывать сертификаты. Для пользователей я буду делить их по годам (например, папки 2024, и 2025 год), и отдельная папка с сертификатами для разного рода информационных систем. Таким образом сразу организуем массив, в который можно будет добавлять пути к папкам для парсинга сертификатов (у меня это три сетевые папки):

$certFolders = @(
     "\\netdirectory\certs\users2024\",
     "\\netdirectory\certs\users2025\",
     "\\netdirectory\certs\systems"
)

Теперь я создам переменную, в которую запишу количество дней. Если срок действия сертификата будет равен или будет меньше количества этих дней - мне будет приходить уведомление. Делается это так:

$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 = @(
     "\\netdirectory\certs\users2024\",
     "\\netdirectory\certs\users2025\",
     "\\netdirectory\certs\systems"
)
$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 и вижу долгожданное уведомление от бота:

Уведомление в telegram
Уведомление в telegram

Отлично! Уведомления приходят. Корректно отрабатывает форматирование текста, корректно читает поля у КСКПЭП и TLS сертификатов, а также у расширений .cer и .crt.

Теперь я в курсе, какой сертификат из моих двухсот сертификатов заканчивается и у меня меньше шансов словить ошибку сервиса из-за невалидности сертификата. Скажу сразу - способ не ультимативный и не панацея, но он работает, а ещё мы немного освежили знания PowerShell.

Всем большое спасибо за прочтение. Конечный вариант кода я прямо отсюда скопировал себе, запустил - работает. Значит по крайней мере в итоговом варианте кода ошибок быть не должно, им можно пользоваться =)

P.S.:


Что ещё хотел сказать... Если вдруг эта статья покажется кому-то интересной и я словлю не очень много минусов за неё, то, быть может, я опубликую следующую, в которой расскажу, как написать в PowerShell скриптик, который с помощью OpenSSL (детально разберём процесс установки для совсем новичков) локально выпускает TLS сертификаты для ваших внутренних (и не очень) сервисов. В нём можно будет выпускать сертификаты с нужными полями, нужными признаками (сертификат уровня CA, то есть все политики выдачи и все политики применения, или сертификат конечного субъекта). При этом они будут подписаны вышестоящим root сертификатом, который можно будет распространить на корпоративные компьютеры, чтобы они автоматически доверяли всем сертификатам, которые им были подписаны при выпуске.
Всё это я использую в своей работе и, быть может, вам это тоже пригодится.

До встречи!

Комментарии (20)


  1. samponet
    12.02.2025 18:41

    В статье рассматривается уведомления от файлов сертификатов, расположенных в общей папке, в то же время мне видится логичным использовать скрипты на самих машинах, потому что вот прям обязать получивших сертификаты ими сразу делиться с некой "службой централизованного хранения" в виде техпода или кого еще видится малореализуемой. Другими словами: наверняка можно сделать всё тоже самое, но для локального компьютера с пользовательскими же сертификатами? В этом случае каким бы образом сертификат не попал на компьютер, уведомления кому надо придут. Да, уведомления тогда надо вывести на какой-то сервер для защиты бота, но это, кмк, не сильно сложно.


    1. itshnick88 Автор
      12.02.2025 18:41

      Ну тут видите, опять же... у всех по-разному. В моей практике, во всех организациях, в которых я находился - сертификаты пользователей всегда проходили через безопасников. Потому что и в ЭДО их нужно передать, и в какой-нибудь ЕИС загрузить, и куча-куча разных примеров, почему сертификат всегда будет у ИБшника. Так что у меня это как раз наоборот - не бывает такого, что пользователь сам себе сделал ключики, сам установил их себе, во все ИС и так далее. Но, опять же, допускаю, что где-то и так, как вы говорите.

      И тут конечно проще складывать сертификаты в общее хранилище, чем на каждый компьютер распространять скрипт... иначе ещё нужно будет продумать, откуда он будет читать сертификаты. Единственное, что мне видится, это из хранилища сертификатов в реестре и тогда нужно, чтобы пользак обязательно его туда положил, и периодически чистил оттуда старые, иначе уведомлений будет масса, скажем, если у вас 100 компьютеров пользователей, и у каждого в хранилище будут ещё и старые сертификаты - придётся просить их вычищать всё или самому бегать...

      Ну и самый главный вопрос - нужно будет передать данные от компьютера пользователя на сервер, на котором будет скрипт с уведомлениями от бота (чтобы не слить ID бота на пользовательские компы) - тут можем получить узкое место в виде самой сети и небольшой минус в виде нужды в сервере - хотя это минусы маленькие и некритичные, но...

      В итоге возвращаемся к тому, что у каждого своя специфика, поэтому я и выложил код, который можно уже под свои нужды подрихтавать =)


      1. Shaman_RSHU
        12.02.2025 18:41

        Вполне обычная ситуация. Как вы знаете, сейчас всем подряд не выпускают КСКПЭП на юриков. Поэтому у нас примерно 30 процентов сотрудников самостоятельно выпускали себе КСКПЭП. Просто их потом присоединяли к организации в ЛК Госуслуг со своими сертификатами и необходимыми правами.


  1. viiprogrammer
    12.02.2025 18:41

    В целом, похожий опыт можно получить с Uptime Kuma, но, конечно, не для КСКПЭП — по крайней мере, не из коробки. У него есть обширный API, так что, думаю, даже это можно реализовать на его базе. Всё основное там уже есть, включая уведомления в множество сервисов.


  1. Ada_VV
    12.02.2025 18:41

    Всё прекрасно, но есть одна неточность. Вы постоянно пишете про срок действия сертификата, а имеете ввиду срок действия ключа. Это две разные вещи. И они задаются при создании ключа, или при сертификации ключа.

    Чаще всего срок действия ключа устанавливают равным 15 месяцам, реже - 3 года, ещё реже другие сроки. А срок действия сертификата этого же ключа обычно от 10 до 15 лет. И пользователь может пользоваться своим ключом в течение срока действия КЛЮЧА. Дольше уже нет. Срок действия сертификата используется не для этого.


    1. itshnick88 Автор
      12.02.2025 18:41

      Спасибо за уточнение! Крепко подумаю над ним, дополнительно почитаю информацию

      Или, буду признателен, если у вас есть ссылка на литературу по данному вопросу


      1. Ada_VV
        12.02.2025 18:41

        Это частая неточность в терминологии. И даже люди, которые считают, что могут писать разъясняющие статьи, сильно путаются. Ради интереса решила поискать ответ на такой вопрос: "что такое срок действия ключа и сертификата". Первая же ссылка ведёт на такую путаницу в терминах, что разобраться действительно невозможно. Путают сертификат, ключ и подпись по тексту. Самое грамотное объяснение нашлось на официальном сайте Центробанка в разделе АУЦ. Там и термины есть, и пояснение, что это за звери такие, и даже зачем они нужны и для чего используются.

        Ну, или надо читать учебники по СКЗИ. Но статьи в интернете - с большой осторожностью.


        1. Shaman_RSHU
          12.02.2025 18:41

          Да на первых же страницах эксплуатационной документации к СКЗИ (например, КриптоПро CSP) всё прекрасно раскрыто


  1. kvazimoda24
    12.02.2025 18:41

    Вы рассказываете о некотором количестве серверов под своим контролем. Вы все важные показатели мониторите подобными самописными скриптами? Не задумывались о централизованной системе мониторинга?


    1. itshnick88 Автор
      12.02.2025 18:41

      Но скрипт ведь мониторит не только TLS сертификаты для серверов. Так-то строго говоря можно и под SIEM написать плагин. Замес статьи-то про дефолтные возможности винды, без дополнительных серверов и сервисов


      1. Shaman_RSHU
        12.02.2025 18:41

        Как вариант, если раскиданы по инфраструктуре Zabbix-агенты переделать скрипт, чтобы он "сканировал" на предмет нахождения всех сертификатов на хосте. А потом это всё свести в красивый дашборд.


    1. ildarz
      12.02.2025 18:41

      А передавать данные в эту систему мониторинга будет тот же самый скрипт, поскольку "из коробки" всё многообразие сертификатов в организации никакая система мониторить не сможет.


      1. kvazimoda24
        12.02.2025 18:41

        Я больше про ту часть, где отправляют уведомление в Телеграм


  1. inkelyad
    12.02.2025 18:41

    (вспоминая довольно многочисленные статьи про мониторинг этих самых сроков)

    Мне одному кажется, что правильный способ - это при генерации закидывать дату в какой-нибудь календарь, с которым уже и интегрируются все нужные оповещалки через то, чего душа просит? Да даже и про мониторинге - смысл напрямую в транспорт доставки оповещений лезть?


    1. itshnick88 Автор
      12.02.2025 18:41

      Я путём календаря с самого начала хотел пойти... потом понял, что это дополнительное место ошибки, когда можно в вихре рабочего потока случайно записать сертификат не на ту дату и никогда больше не узнать о его окончании. Решил отдать это на откуп скрипта, пусть сам считывает поле даты, а я в это не лезу)

      Но вообще да, вариантов масса, я же в статье говорил. У каждого свои желания, возможности. Мне подходит мой вариант, но все вольны сделать так, как больше подходит им :)


  1. Busla
    12.02.2025 18:41

    есть RFC, но по факту кто во что горазд, даже с тем же CN, в который запрещено писать адрес домена, если этот домен записывается в SAN

    Смотрите, был стандарт: fqdn писать в CN. Потом появился упомянутый вами RFC. В итоге старый софт опирается на CN. Новый софт, следующий RFC, должен брать fqdn из SAN. Чтобы обеспечить совместимость сервера со старым и новым ПО fqdn из CN нужно продублировать в SAN. То, что софт должен читать только одно из двух полей, совершенно не говорит, что нельзя записывать в серт оба поля.


    1. itshnick88 Автор
      12.02.2025 18:41

      Есть новый софт от Kaspersky, для которого я выпускал очередные ключи и сделал это как полагается, через SAN, не указывая FQDN в поле CN (мы ведь уже достаточно давно его не должны там указывать, а на дворе был 2024 год). Продукт отказался съедать сертификат, пока он не увидит адрес домена в CN.
      Зачем/почему?


  1. Busla
    12.02.2025 18:41

    Как поведёт себя ваш скрипт, если в пути окажется $ (например, скрытая шара) или год будет в квадратных скобках?


    1. itshnick88 Автор
      12.02.2025 18:41

      На счёт скрытой шары немного не понял. Если у вашего ПК (на котором работает скрипт) есть сетевая доступность до ресурса, будь он скрытым (скрытым где? В проводнике? Это лишь атрибут проводника), вы всё равно имеете к нему доступ и можете попасть напрямую, указав полный путь в адресной строке, например. Но не уверен, что вы именно это имели ввиду в вопросе.

      На счёт года в скобках... - год чего? Дни до истечения сертификата отправляются в телегу в днях, скобки там отлично работают.
      Если скобки, вы имеете ввиду, будут в дате окончания сертификата, то они там не будут, потому что поле NotAfter не допускает наличия скобок в сертификате, а мы читаем именно это поле. Но, если бы была такая проблема, я бы сначала привёл два формата даты к какому-то единому, а потом уже сравнивал их между собой и вычислял результат. В общем, как в математике :)


      1. Shaman_RSHU
        12.02.2025 18:41

        Если в Windows в конце имени расшаренной папки указать $, то она не будет отображаться в сетевом окружении и попасть туда можно только зная её имя (ну не совсем :) Это имелось в виду.