Многие системные администраторы, которые занимается выпуском ЭЦП для сотрудников, сталкиваются с проблемой и задаются вопросом, как отслеживать срок их окончания, что бы успеть их продлить?

В этой заметки я предложу способ, как отслеживать срок окончания ЭП с помощью скрипта + самое главное — это напоминания, которые будут отправляться в мессенджер MAX.

Важная информация:

Скрипт проверяет дату окончания ОТКРЫТОГО ключа сертификата. Но не забываем, что есть еще дата окончания закрытой части и она может отличаться.

Вводные данные:

Для организации автоматических проверок, нам необходимо:

1. Зарегистрировать бота в MAX. Как это делать, я описывать не буду, всё подробно описано в этой статье на официальном сайте мессенджера.
2. Операционная система Ubuntu 25.04 (у вас может быть другая, суть скрипта — не поменяется, возможно изменится часть команд для установки)
3. Бот в MAX, который вы создали на 1 шаге и его id, а так же ваш ID его тоже легко посмотреть через какого-нибудь бота. Кроме того, вам нужен токен бота, который вы найдете в панели управления MAX.
4. Каталог с сертификатами пользователей, который вы расположили на Ubuntu

 

Содержание статьи:

  1. Подготовительные работы на сервере
  2. Описание скрипта на Node
  3. Проверка работоспособности скрипта + отправка уведомлений в MAX
  4. Добавление скрипта в планировщик Cron

 

1. Подготовительные работы на сервере

Я использовал абсолютно чистую операционную систему Ubuntu 25.04

Шаг 1. Подключитесь к серверу

ssh user@your-server-ip

Шаг 2. Установите Node.js

# Обновляем пакеты

sudo apt update && sudo apt upgrade -y

# Устанавливаем Node.js

sudo apt install -y nodejs

# Проверяем установку

node --version 

# Должно быть v20.x.x

npm --version 

# Должно быть 10.x.x

Шаг 3. Создаём структуру проекта

# Создаём папку для проекта

mkdir -p /opt/cert-monitor
cd /opt/cert-monitor

# Инициализируем проект

npm init -y

# Устанавливаем библиотеку MAX Bot API

 npm install @maxhub/max-bot-api

# Включаем поддержку ES-модулей. Откройте package.json: nano package.json и добавьте после первой строки { «type»: «module»,
Выполните эту команду, чтобы автоматически добавить «type»: «module»

 sed -i '1s/{/{\n  "type": "module",/' package.json

Шаг 4. Создайте папку для сертификатов (если её нет)

 sudo mkdir -p /mnt/certs 

Если сертификаты лежат в другой папке или смонтированы через Samba/NFS — укажите свой путь позже в скрипте.

2. Описание скрипта на Node

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

// ========== НАСТРОЙКИ ==========
const BOT_TOKEN = ‘ВАШ ТОКЕН БОТА’; # Указываем токен бота с личного кабинета MAX
const MY_USER_ID = ВАШ_ИД_МАКС; Указываем ваш ID в Максе
const WARN_DAYS = 40; Это переменная, в которую запишем количество дней. Если срок действия сертификата будет равен или будет меньше количества этих дней — нам будет приходить уведомление в телеграмм.

const certFolders = [
‘/mnt/certs’, Папка где находятся открытые части сертификатов
];
// ================================

Шаг 1. Создайте файл со скриптом check-certs.mjs

 nano /opt/cert-monitor/check-certs.mjs 

Скопируйте и вставьте финальный рабочий скрипт:

import { Bot } from '@maxhub/max-bot-api';
import { readFileSync, readdirSync } from 'fs';
import { join, extname } from 'path';
import { X509Certificate } from 'crypto';

// ========== НАСТРОЙКИ ==========
const BOT_TOKEN = 'ВАШ ТОКЕН БОТА';
const MY_USER_ID = ВАШ_ИД_МАКС;
const WARN_DAYS = 40;

const certFolders = [
    '/mnt/certs',
];
// ================================

const bot = new Bot(BOT_TOKEN);

function derToPem(derBuffer) {
    const base64 = derBuffer.toString('base64');
    const lines = base64.match(/.{1,64}/g).join('\n');
    return `-----BEGIN CERTIFICATE-----\n${lines}\n-----END CERTIFICATE-----`;
}

function loadCertificate(filePath) {
    try {
        const text = readFileSync(filePath, 'utf8');
        if (text.includes('-----BEGIN CERTIFICATE-----')) {
            return new X509Certificate(text);
        }
    } catch {}
    const buffer = readFileSync(filePath);
    const pem = derToPem(buffer);
    return new X509Certificate(pem);
}

function extractCN(subject) {
    const match = subject.match(/CN\s*=\s*([^,\n]+)/);
    return match ? match[1].trim() : 'CN не найден';
}

function formatDate(date) {
    const d = new Date(date);
    return `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}/${d.getFullYear()} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
}

function collectCertFiles(folders) {
    const files = [];
    const extensions = ['.cer', '.crt', '.pem'];
    for (const folder of folders) {
        try {
            const entries = readdirSync(folder, { withFileTypes: true });
            for (const entry of entries) {
                const fullPath = join(folder, entry.name);
                if (entry.isDirectory()) {
                    files.push(...collectCertFiles([fullPath]));
                } else if (entry.isFile() && extensions.includes(extname(entry.name).toLowerCase())) {
                    files.push(fullPath);
                }
            }
        } catch (err) {
            console.error(`❌ Не удалось прочитать папку "${folder}": ${err.message}`);
        }
    }
    return files;
}

async function sendMessage(text) {
    try {
        const msg = await bot.api.sendMessageToUser(MY_USER_ID, text, { format: 'html' });
        console.log(`  ✅ Уведомление отправлено (mid: ${msg.body.mid})`);
    } catch (err) {
        console.error(`  ❌ Ошибка отправки: ${err.message}`);
    }
}

async function main() {
    console.log('🔍 Поиск сертификатов...');
    const certFiles = collectCertFiles(certFolders);
    console.log(`📁 Найдено файлов: ${certFiles.length}\n`);

    const now = new Date();

    for (const filePath of certFiles) {
        try {
            const cert = loadCertificate(filePath);
            const cn = extractCN(cert.subject);
            const notAfter = new Date(cert.validTo);
            const daysLeft = Math.floor((notAfter - now) / (1000 * 60 * 60 * 24));

            console.log(`📄 ${filePath.split('/').pop()}`);
            console.log(`   👤 ${cn} | Истекает: ${formatDate(notAfter)} (осталось ${daysLeft} дн.)`);

            if (daysLeft < WARN_DAYS) {
                let message;
                if (daysLeft >= 0) {
                    message = `❗ Сертификат: ❗\n\nВладелец: ${cn}\nДата окончания: через ${daysLeft} дн.\nИстекает: ${formatDate(notAfter)}`;
                } else {
                    message = `❌ Сертификат ${cn} срок действия ИСТЁК!`;
                }
                console.log(`   ⚠️ Нужно уведомление!`);
                await sendMessage(message);
            } else {
                console.log(`   ✅ OK`);
            }
            console.log('');

        } catch (err) {
            console.error(`❌ Файл "${filePath.split('/').pop()}": ${err.message}\n`);
        }
    }

    console.log('🏁 Проверка завершена.');
    bot.stop();
}

main().catch(console.error);

3. Проверка работоспособности скрипта + отправка уведомлений в MAX

Проверьте работу скрипта

cd /opt/cert-monitor
node check-certs.mjs

Если всё хорошо — увидите логи в консоли и уведомление в MAX.

4. Добавление скрипта MAX в планировщик Cron

Чтобы скрипт проверял сертификаты, например, каждый день в 9:00:

# Открываем crontab

crontab -e 

Выберите редактор (nano — 1). Добавьте в конец строку:

 0 9 * * * cd /opt/cert-monitor && /usr/bin/node check-certs.mjs >> /var/log/cert-monitor.log 2>&1 

Пояснение:

0 9 * * * — каждый день в 9:00
cd /opt/cert-monitor — перейти в папку проекта
/usr/bin/node check-certs.mjs — запустить скрипт
>> /var/log/cert-monitor.log 2>&1 — записать логи в файл

Проверьте, что cron запущен:

 sudo systemctl status cron 

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *