Сколько у вас в инфраструктуре версий PostgreSQL? А какие из них уже не поддерживаются? А сколько CVE висит на том nginx, который «работает — не трогай»?
Если вы ответили на все три вопроса без поиска по чатам и вики, дальше можно не читать. А я в какой‑то момент понял, что не могу, и написал для этого инструмент. Под катом история о том, как он появился, на какие грабли я наступил по дороге и почему MySQL однажды заблокировал собственный хост из‑за моего мониторинга.
Зоопарк
У меня есть домашняя лаба pet‑проекта. Есть и работа: я ведущий DevOps‑инженер, и масштаб там, думаю, понятен. И там, и там всегда был и будет зоопарк версий. Где‑то стоит PostgreSQL 13, потому что «на нём так и запускали». Где‑то Jira, которую страшно обновлять. Где‑то Debian, поставленный три point‑релиза назад. Отдельно живут NAS, гипервизоры, BMC серверов и роутеры.
Пока сервисов десяток, их версии можно держать в голове. Когда их сотни, начинаются вопросы. Что именно у нас развёрнуто? Что из этого уже EOL или станет EOL через полгода? Где мы отстали на патч, а где на целый мажор? Где висят известные уязвимости?
Я взялся навести в этом порядок. Сразу было понятно, что вручную ничего не получится: обойти сотню сервисов и записать версии в табличку можно один раз, но через месяц табличка уже врёт.
Первый заход: Ansible и гуглотаблица
Первое, что приходит в голову DevOps‑инженеру — Ansible. Так я и сделал: прошёлся плейбуком по хостам, собрал версии ОС (в основном Debian), выгрузил в CSV, а из CSV собрал гуглотаблицу для команды.
Получилось прикольно, но для одного раза. Дальше начались проблемы:
-
Отчёт нужно перегенерировать, а потом ещё и понять, что изменилось с прошлого раза. Поддерживать диффы руками больно.
-
Табличка знает версию, но не знает, хороша ли она. Сроки поддержки приходилось смотреть отдельно.
-
Ansible хорошо ходит по SSH. А Jira, PostgreSQL, Redis, NAS или BMC сервера — уже не хосты с SSH, а сервисы со своими протоколами.
Захотелось инструмента, который был бы одновременно масштабным, чтобы закрыть весь парк, и атомарным: одна команда, понятный результат, который можно положить в cron или CI.
Поиск готового
Перед тем как писать своё, я честно поискал готовое. Нашлось много хорошего, но каждый инструмент закрывает только свой кусок:
-
endoflife.date и его CLI знают календари поддержки, но не знают, что развёрнуто у вас.
-
nvchecker знает свежие версии в апстриме, но тоже не знает, что развёрнуто.
-
Renovate и Dependabot работают с зависимостями в репозитории, а не с живыми серверами.
-
What’s Up Docker следит за тегами контейнеров, а всё, что не в Docker, остаётся вне поля зрения.
-
Uptime Kuma и прочий мониторинг отвечают на вопрос «живо ли», а не «устарело ли».
Ни один из них не делает главного — не соединяет «что реально развёрнуто» x «календарь поддержки» x «известные уязвимости». Так появился enodia.
Как устроен enodia
enodia — это CLI на Go. Распространяется одним бинарником под лицензией AGPL-3.0. Вы описываете парк в enodia.yaml, enodia опрашивает каждый сервис, узнаёт реальную версию и сверяет её со сроками поддержки и базами CVE.
Девиз проекта я сформулировал так: «Знайте, что у вас запущено, и сколько ему осталось» (в оригинале Know what you are running, and how long it has left). А вся механика укладывается во вторую фразу: «один раз описали сервисы — дальше enodia сама разберётся» (You describe your services once. Enodia handles the rest).
Пара слов о названии. Энодия («та, что у обочины») — эпитет Гекаты, факелоносицы и хранительницы перекрёстков. Мне показалось, что это подходящее имя для инструмента, который высвечивает увядающее и стоит ровно там, где вы выбираете, в какую сторону обновляться.
Конфиг выглядит так (это демо-стенд из статьи, о нём ниже):
schemaVersion: 1targets: - id: billing-db product: postgresql address: 127.0.0.1:25432 credentials: pg - id: crm-mysql product: mysql address: 127.0.0.1:23306 - id: web-front product: nginx address: http://127.0.0.1:28080 - id: app01 product: debian address: 127.0.0.1:22022 credentials: app01-sshcredentials: pg: kind: password username: postgres password: "${DEMO_PASSWORD}"
Несколько решений, которые я принял в самом начале и о которых не жалею.
Проба знает свой продукт. Под каждый продукт написана своя проба на Go: для PostgreSQL она говорит на протоколе PostgreSQL, для Redis — на протоколе Redis, для Jira ходит в её REST API, для ОС подключается по SSH. Сейчас проб 123: Atlassian, GitLab, Jenkins, TeamCity, SonarQube, Keycloak, Vault, Grafana, базы данных и брокеры, ESXi и vCenter, Proxmox, NAS, BMC серверов, pfSense и RouterOS, а также 31 ОС по SSH. Для всего остального есть generic-проба: URL плюс regex или путь в JSON.
Продукт вы указываете в конфиге явно, а проба проверяет, что на том конце именно он. Если вы написали mysql, а там отвечает MariaDB, вы получите ошибку, а не тихо записанную неправильную версию. Почему это важно, станет видно ниже.
Сбор отдельно от оценки. Работа разбита на две фазы:
enodia collect -o inventory.jsonl # ходит по сервисам, пишет фактыenodia check --from inventory.jsonl # сверяет факты с календарями и CVE
collect ничего не оценивает, он только собирает наблюдения. Недоступный сервис для него не ошибка, а факт «недоступен». check работает с готовым файлом. Это удобно в закрытом контуре: собрали инвентарь внутри, вынесли JSONL и оценили там, где есть интернет и базы. Можно и наоборот: базы занести внутрь контура, о них ниже.
Три оси вместо одного статуса. Сначала хотелось выдавать один вердикт: «хорошо» или «плохо». Но возьмём Confluence в LTS-ветке. Он обновлён до последнего патча своей ветки, ветка ещё поддерживается, и при этом уже вышел новый мажор. Это три независимых факта, и любой из них может быть важен. Поэтому в enodia три оси:
-
PATCH — отстаём ли мы внутри своей ветки:
current,behind; -
LIFECYCLE — поддерживается ли ветка:
active,security(только исправления безопасности),eol; -
BRANCH — есть ли ветка новее:
latest,newer,newer_lts.
Из этих осей складывается итоговая серьёзность info / warn / fail, и от неё зависит код выхода: 0 — всё в порядке, 3 — есть предупреждения, 4 — есть провалы. Поэтому enodia check удобно ставить в CI. Если какая-то ось для вас критична, её можно ужесточить: например, --fail-on=patch:behind.
HTTPS по умолчанию и никаких паролей в конфиге. Если в адресе не указана схема, enodia идёт по HTTPS, чтобы учётные данные не уходили открытым текстом. Пароли подставляются из переменных окружения через ${VAR}. Их можно вынести в отдельный credentials-файл или получать через Vault Agent.
Пробы и грабли
На этом этапе я думал, что самое сложное позади: спросил версию — записал версию. Как бы не так.
MySQL, который банил сам себя
Сервер MySQL (и MariaDB) сразу после TCP-подключения сам присылает приветствие, и в нём есть версия. Логин для этого не нужен. Удобно: подключился, прочитал первый пакет, отключился. Так и работала проба до версии 2.2.1.
Подвох в том, что для сервера такое соединение выглядит как прерванное до аутентификации. Такие соединения с одного хоста сервер считает, и когда их набирается больше max_connect_errors, он блокирует хост целиком:
ERROR 1129 (HY000): Host '172.20.3.10' is blocked because of many connection errors;unblock with 'mariadb-admin flush-hosts'
Блок действует для всех клиентов с этого хоста. Если enodia запускается с того же сервера, где живёт приложение, то мой мониторинг версий однажды положил бы продакшен-приложение. Отличная иллюстрация к поговорке про лекарство и болезнь.
Воспроизвести это оказалось на удивление непросто. В официальных Docker-образах MySQL и MariaDB в конфиге стоят skip-name-resolve и skip-host-cache, и счётчик просто не ведётся. Для блока нужно, чтобы сервер разрешал имена клиентов и держал host cache. Именно так ведут себя «обычные» установки из пакетов. На демо-стенде я включил это руками и поставил max_connect_errors=3. На MariaDB 10.6 всё сработало как по нотам. После четырёх прогонов старой версии enodia:
$ mariadb -h db -e "SELECT 1" # обычный клиент с того же хостаERROR 1129 (HY000): Host '172.20.3.10' is blocked because of many connection errors
А на пятом прогоне enodia заблокировала саму себя:
"error":"unreachable: server sent an ERR packet (code 1129) instead of a handshake"
В логе сервера это выглядит так:
[Warning] Aborted connection 24 to db: 'unconnected' user: 'unauthenticated' host: '172.20.3.10' (This connection closed normally without authentication)
Решение кажется контринтуитивным: надо попытаться залогиниться. Сервер считает в max_connect_errors только соединения, прерванные до аутентификации. Соединение, которое дошло до неё и получило отказ, считается установленным и в счётчик не попадает. Поэтому с 2.2.1 проба после чтения версии отвечает на приветствие пользователем enodia с пустым паролем, получает честный Access denied и вежливо прощается через COM_QUIT. Если сервер на MySQL 8 предлагает сменить плагин аутентификации на caching_sha2_password, проба отвечает пустым пакетом и дочитывает отказ.
Теперь в логе другая запись, и по ней сразу видно, кто стучался:
[Warning] Access denied for user 'enodia'@'app02.demo.lan' (using password: NO)
Сколько ни запускай новую версию, блока нет.
MariaDB в костюме MySQL
Помните про проверку, что на том конце именно тот продукт? Вот зачем она нужна. MariaDB 10.x представляется так:
5.5.5-10.11.19-MariaDB-ubu2204
Префикс 5.5.5- — маскировка под MySQL для старых клиентов, которые не знали схему версий MariaDB. Если читать версию в лоб, получится MySQL 5.5.5, давно мёртвый. А начиная с 11.0 MariaDB маскироваться перестала и пишет просто 11.4.13-MariaDB-ubu2404. Префикса нет, и единственный признак — подстрока -MariaDB. До версии 2.1.1 enodia записывала такие серверы как MySQL.
Версии, которые врут
Ещё один класс проблем — версия формально есть, но она не та.
-
Debian. В
/etc/os-releaseполеVERSION_IDникогда не содержит point-релиз. Полностью обновлённый Debian 13 и Debian 13, поставленный в день релиза, одинаково показывают13. Настоящая версия, например13.6, лежит в/etc/debian_version. Проба читает оба файла. -
Ubuntu. Здесь наоборот:
VERSION_IDзамораживается на22.04на всю жизнь релиза, а point-релиз22.04.5есть только в полеVERSION. -
VMware. endoflife.date пишет последнюю версию vCenter как
8.0 U3k, а ESXi — как8.0 Update 3k. Наивный парсер отрезает всё после пробела и видит8.0. В итоге любой хост на Update 3 выглядит новее календаря. Пришлось научить разбор версий понимать вендорское «Update N» и превращать его в8.0.3.
Perforce: протокол есть, ответа нет
Коротко, потому что это тянет на отдельную статью. Документации на протокол Perforce нет, поэтому я снял дамп обмена официального клиента p4 info и разобрал формат. Свой клиент воспроизводил рукопожатие байт в байт и работал против Perforce Proxy. А живые серверы p4d это же рукопожатие молча проглатывали: ни ответа, ни ошибки, ни RST. Обязательный TLS и rate limit я исключил, а официальный p4 с той же машины подключался без проблем. В итоге проба для Perforce стала первой и единственной, которая вызывает внешний CLI: путь к p4 задаётся в options.binary. Позже нашлась ещё одна мелочь: вызов p4 шёл без таймаута и на недоступном сервере мог висеть вечно. Исправлено в 1.2.1.
Где брать сведения об обновлениях
Допустим, версию мы узнали. Теперь нужно понять, хороша ли она. И тут выяснилось, что единой базы «продукт — ветки — даты окончания поддержки» не существует.
Ближе всего к ней endoflife.date. Это открытый проект, где сообщество поддерживает календари поддержки для сотен продуктов: ветки, даты EOL, последние версии. enodia берёт календари оттуда в первую очередь.
Но части нужного мне софта в endoflife.date не было, а у части была неполная история. Поэтому я туда законтрибьютил. На момент написания статьи приняты мои PR с двумя новыми продуктами, Jira Service Management и pgAdmin, с полной историей веток Confluence (1.0–7.12), Bitbucket (1.0–8.6) и Bamboo (0.9–7.2), а также с правками дат EOL у Jira Software и Confluence. На ревью висит ещё 21 продукт: TeamCity, TestRail, Crowd, FishEye и Crucible, Bitbucket Mesh, линейка Perforce Helix, pfSense CE и Plus, FreeRADIUS, Synology DSM и SRM, Jellyfin, Vaultwarden, Bitwarden Server, Owncast, openEuler, Kitsu. Если вам тоже чего-то не хватает в endoflife.date, присоединяйтесь: от этого выиграют все, а не только пользователи enodia.
Для софта, которого в endoflife.date нет, источником служат GitHub Releases и теги. Последнюю версию оттуда узнать можно, но с календарём поддержки сложнее. И здесь тоже есть грабли. Например, SonarQube разделился на две линейки: SonarQube Server с версиями вида 2025.x и Community Build с версиями вида 25.x. Это два разных календаря. enodia понимает, какой из них брать, по году в номере версии.
CVE: NVD, БДУ и данные вендоров
Следующий вопрос — уязвимости. Первым делом вспоминается NVD, американская национальная база уязвимостей. С неё я и начал.
А что, если вы работаете в России? Тогда есть БДУ ФСТЭК, Банк данных угроз безопасности информации. Для одних это обязательный источник по требованиям регулятора, для других просто ещё одна база. Я не стал выбирать и подключил обе. В отчёте у каждой находки видно, откуда она пришла. Для дистрибутивов, которые активно ставят в РФ (Astra Linux, РЕД ОС, ALT), есть своя поддержка: enodia определяет их по SSH, а для Astra и РЕД ОС ещё и сверяет установленные пакеты с OVAL-данными вендора.
На уровне пакетов ОС уязвимости ищутся по данным самих дистрибутивов. Всего поддерживается 10 дистрибутивов:
-
Debian — по Security Tracker;
-
Ubuntu, Linux Mint, RHEL, Rocky, Alma, Oracle Linux, Astra Linux, РЕД ОС — по OVAL;
-
Alpine — по secdb.
Базы скачивает команда enodia cve update, и это единственная команда, которая ходит в сеть за CVE. check, collect и serve работают с локальными файлами, поэтому базы можно занести в закрытый контур на флешке. Все базы на демо-стенде, NVD целиком с 2002 года, БДУ и данные вендоров, заняли около 350 МБ.
Грабли БДУ, часть первая: сертификат
Первый же cve update споткнулся о TLS: bdu.fstec.ru не отдаёт промежуточный сертификат. Сервер подписан цепочкой «Russian Trusted», корня которой нет в системных хранилищах. Но даже с корнем проверка не проходит без промежуточного Sub CA. Решение: скачать корень с Госуслуг, промежуточный сертификат — по ссылке AIA из самого сертификата сервера, сложить их в один PEM и указать в конфиге:
cve: bdu: {path: ./cve/bdu.zip} update: ca_file: ./cve/russian-trusted.pem
Отключить проверку сертификата, конечно, было бы проще, но для инструмента, который занимается безопасностью, это был бы плохой пример.
Грабли БДУ, часть вторая: диапазоны без веток
Интереснее оказалось с самими данными. Диапазоны уязвимых версий в БДУ часто записаны как «до 18.5» без нижней границы и без учёта веток. Для продукта с параллельными ветками, например PostgreSQL, это значит, что последний патч каждой старой ветки формально «уязвим», даже если бага там никогда не было или его исправили в той же ветке.
Как это выглядит на практике. PostgreSQL 17.11 — последний патч своей ветки на момент проверки. Вот что получается по одним только БДУ и NVD:
ID PRODUCT PATCH LIFECYCLE BRANCH SEVERITY REASON CVESanalytics-db postgresql current active newer info - 46
46 CVE на полностью обновлённом сервере. Выглядит страшно и совершенно неверно.
Решение — данные самого вендора. У PostgreSQL, MariaDB, nginx и Atlassian есть собственные страницы безопасности, где по каждой уязвимости перечислены затронутые и исправленные версии по веткам. Если вендор перечисляет ветку, его вердикт перебивает вердикт БДУ и NVD. С данными PostgreSQL у того же сервера 0 CVE, а у PostgreSQL 13.16 вместо 61 остаётся 48 настоящих.
Мораль: больше CVE не значит точнее. Если сканер показывает сотни уязвимостей на свежем сервере, на него перестают смотреть. Ложные срабатывания убивают доверие к инструменту быстрее, чем пропуски.
Что получилось
Проверим всё на живом стенде. Я поднял в Docker восемь сервисов разной степени свежести: два PostgreSQL, MySQL, две MariaDB, nginx, Redis и Debian с SSH. Сбор:
$ time enodia collect -o inventory.jsonlreal 0m0.149s
Проверка:
$ enodia check --from inventory.jsonlID PRODUCT PATCH LIFECYCLE BRANCH SEVERITY REASON CVESbilling-db postgresql behind eol newer fail - 48analytics-db postgresql current active newer info - -crm-mysql mysql behind eol newer_lts fail - 176wiki-mariadb mariadb behind eol newer_lts fail - 37shop-mariadb mariadb current active newer_lts warn - -web-front nginx current eol newer fail - 24sessions-cache redis behind security newer warn - 20app01 debian behind security newer warn - 76
Код выхода 4: есть провалы. Вместе с поиском по CVE проверка заняла 2,2 секунды. Обратите внимание на web-front: nginx 1.24.0 — последний патч своей ветки (current), но сама ветка уже EOL. Именно такие случаи теряются, если смотреть только на «обновлено ли до последнего».
Есть и другие виды. Например, --view lifecycle показывает, сколько дней осталось до EOL, или сколько дней назад он наступил:
ID PRODUCT LIFECYCLE EOL SUPPORT-ENDS DAYS-TO-EOLbilling-db postgresql eol 2025-11-13 - -350analytics-db postgresql active 2029-11-08 - 1105crm-mysql mysql eol 2026-04-30 2025-04-30 -182web-front nginx eol 2024-04-23 - -919sessions-cache redis security 2027-04-01 2022-04-27 153app01 debian security 2028-06-30 2026-07-11 609
А --view drift — насколько мы отстали внутри своей ветки:
ID PRODUCT CURRENT LATEST CYCLE PATCH CVESbilling-db postgresql 13.16 13.23 13 behind 48crm-mysql mysql 8.0.36 8.0.46 8.0 behind 176wiki-mariadb mariadb 10.6.18 10.6.28 10.6 behind 37app01 debian 12.5 12.15 12 behind 76
Для app01 CVE считаются по пакетам: enodia забрала по SSH список установленных пакетов и сверила его с Debian Security Tracker.
Для людей, которые не любят терминал, есть HTML-отчёт: enodia export --format html. Это один самодостаточный файл со всеми видами отчёта. JavaScript в нём нет: даже окно со списком CVE открывается средствами CSS. Такой файл не страшно отправить безопаснику.
По клику на число CVE открывается список с CVSS, ссылками на CVE.org и БДУ и источником каждой находки. Для записей из БДУ показывается описание на русском:
Для мониторинга есть экспорт в Prometheus (--format prometheus): по каждой оси отдаётся отдельная метрика, и алерты в Grafana на них пишутся тривиально.
enodia_lifecycle_status{id="billing-db",status="eol"} 1enodia_lifecycle_status{id="analytics-db",status="active"} 1
Есть и режим enodia serve --interval: он собирает данные по таймеру и отдаёт HTML, JSON и /metrics. Важная деталь: запрос к странице не запускает новый сбор. Кнопка «обновить», которая при каждом клике опрашивает весь парк, — это DoS, который вы устраиваете себе сами. Встроенной авторизации нет, сервис нужно ставить за reverse proxy.
Установка
enodia — статический бинарник без зависимостей, так что платформ много:
|
ОС |
Архитектуры |
Минимальная версия |
|---|---|---|
|
Linux |
amd64, arm64 |
ядро 3.2+ (Debian 8+, Ubuntu 14.04+, RHEL/CentOS 7+) |
|
Windows |
amd64, arm64, 386 |
Windows 10 / Server 2016 |
|
macOS |
amd64 (Intel), arm64 (Apple Silicon) |
macOS 12 Monterey |
|
Android (Termux) |
arm64 |
Android 7 |
Проще всего поставить скриптом:
# Linux, macOS, Termuxcurl -fsSL https://get.enodia.sh/unix | sh
# Windows (PowerShell)irm https://get.enodia.sh/windows | iex
На Windows можно и через пакетные менеджеры, тогда обновления будет отслеживать он:
winget install EpicMorg.Enodiachoco install enodia
Ещё есть пакеты deb, rpm, apk и pacman на странице релизов, архивы под все платформы из таблицы и образ epicmorg/enodia на Docker Hub, quay.io и ghcr.io. Про Termux отдельно: для Android пришлось собирать отдельный бинарник, потому что статический linux/arm64 Android запускать отказывается.
Документация на восьми языках, включая русский, лежит на docs.enodia.sh. Там же описан конфиг для каждой пробы.
Вместо выводов
Зоопарк версий никуда не денется. Его можно только видеть. Если у вас есть ответ на вопрос «что у нас устарело и где дыры», полученный одной командой и одинаковый сегодня и через месяц, дальше остаётся планировать обновления, а не искать, что обновлять.
Чему меня научила эта история:
-
Спросить версию — уже нетривиально. Протоколы, маскировки, point-релизы в разных местах, вендорские «Update 3k».
-
Мониторинг может навредить. Безобидное «подключился и отключился» в MySQL оборачивается блокировкой хоста.
-
Данные важнее кода. Половина работы — не пробы, а источники: календари, базы CVE, их несовершенство и способы его компенсировать.
-
Open source работает в обе стороны. Я беру календари из endoflife.date и возвращаю туда то, чего в нём не хватало.
Исходники лежат на GitHub. Буду рад комментариям, отзывам, звёздам, issue и PR с новыми пробами. А если вашего софта нет ни в enodia, ни в endoflife.date, напишите в комментариях, какого именно.
ссылка на оригинал статьи https://habr.com/ru/articles/1101784/