Многие системные администраторы, которые занимается выпуском ЭЦП для сотрудников, сталкиваются с проблемой и задаются вопросом, как отслеживать срок их окончания, что бы успеть их продлить?
В этой заметки я предложу способ, как отслеживать срок окончания ЭП с помощью скрипта + самое главное — это напоминания, которые будут отправляться в мессенджер MAX.
Скрипт проверяет дату окончания ОТКРЫТОГО ключа сертификата. Но не забываем, что есть еще дата окончания закрытой части и она может отличаться.
Вводные данные:
Для организации автоматических проверок, нам необходимо:
1. Зарегистрировать бота в MAX. Как это делать, я описывать не буду, всё подробно описано в этой статье на официальном сайте мессенджера.
2. Операционная система Ubuntu 25.04 (у вас может быть другая, суть скрипта — не поменяется, возможно изменится часть команд для установки)
3. Бот в MAX, который вы создали на 1 шаге и его id, а так же ваш ID его тоже легко посмотреть через какого-нибудь бота. Кроме того, вам нужен токен бота, который вы найдете в панели управления MAX.
4. Каталог с сертификатами пользователей, который вы расположили на Ubuntu
Содержание статьи:
- Подготовительные работы на сервере
- Описание скрипта на Node
- Проверка работоспособности скрипта + отправка уведомлений в MAX
- Добавление скрипта в планировщик 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