Трендовые github проекты в нашем телеграм канале. Подпишись → Когда Zabbix Server принимает только шифрованные соединения, а агент умеет только TCP
Мониторинг PostgreSQL через Zabbix обычно выглядит скучно: ставишь агента, он видит базу, метрики текут на сервер. Но если Zabbix Server в инфраструктуре настроен принимать от узлов только защищённые соединения — TLS PSK или сертификаты, — часть агентов мониторинга внезапно перестаёт работать, хотя со сбором данных у них всё в порядке. Разберём, как локализовать такую проблему и какое решение оказывается устойчивее, чем разовый костыль.
Симптом: метрики есть локально, но не доходят до сервера
Агент подключается к PostgreSQL, собирает метрики, отдаёт список доступных элементов данных по локальному запросу — и всё это работает штатно. А в Zabbix значения не появляются. Первая реакция — искать ошибку в конфигурации: имя узла, порт, права доступа, ключ элемента данных. Но если журналы агента показывают, что данные сформированы, а на сервере их нет, проблема сидит на этапе передачи, а не сбора.
Быстрый способ отделить одну причину от другой — попробовать отправить ту же метрику штатной утилитой zabbix_sender, явно указав параметры TLS:
zabbix_sender -c /etc/zabbix/zabbix_agentd.conf \
-z <FQDN_or_IP_zabbix_server> \
-p 10051 \
-s <HOSTNAME_IN_ZABBIX> \
-k 'pgsql.ping[]' \
-o 1 \
--tls-connect psk \
--tls-psk-identity '<PSK_NAME>' \
--tls-psk-file /etc/zabbix/zabbix_agentd.psk
Если это отрабатывает с processed: 1; failed: 0, значит сервер доступен, узел зарегистрирован, PSK корректен, а проблема — исключительно в транспортном слое конкретного агента мониторинга. Дальше искать её нужно уже в исходном коде агента, а не в настройках Zabbix.
Именно так выглядела ситуация с Mamonsu — открытым агентом для метрик PostgreSQL: соединение с Zabbix Server у него создавалось как обычный TCP-сокет, без какой-либо поддержки TLS. Поддержка шифрования появилась в Zabbix ещё в версии 3.0 — разрыв между возможностями сервера и агента существовал давно, просто не проявлялся, пока сервер не начинал требовать шифрование обязательно.
Почему разрешать plaintext для узла — плохая идея
Технически проще всего снять ограничение на сервере: разрешить для конкретного узла незашифрованные соединения, и агент сразу заработает. Это решает проблему за пару минут, но меняет модель безопасности всей системы мониторинга ради одного клиента, который не умеет в неё вписаться.
Телеметрия мониторинга — тоже чувствительные данные: имена узлов, состояние сервисов и баз, загрузка, ошибки, версии компонентов. Она описывает инфраструктуру достаточно подробно, чтобы не считать канал её передачи автоматически неважным. Если в остальной инфраструктуре уже принято требование шифровать каналы связи, разумнее адаптировать агента под эту модель, чем ослаблять сервер под возможности одного агента.
Альтернатива без правки исходного кода — обернуть агент внешним транспортом: собирать метрики штатно, а отправлять их через zabbix_sender как прослойку. Рабочий вариант, но он добавляет ещё один слой с собственной конфигурацией, обработкой ошибок и жизненным циклом — притом что внутри самого агента уже есть очередь метрик, формирование пакета протокола и повторная отправка. Не хватает буквально одного компонента — защищённого транспорта, и логичнее добавить его в сам агент, если у него открытый исходный код.
Меняем транспорт, не трогая протокол
Ключевой принцип такой доработки — минимальное вмешательство. Протокол отправки (очередь метрик, формирование JSON-пакета, повторная отправка, разбор ответа сервера) уже работает и не нуждается в изменениях. Меняется только объект соединения, который отдаёт sendall(), recv(), close():
def _connect(self):
if self.tls_connect == TLS_PSK:
return tls.connect_psk(
self.host, self.port,
self.tls_psk_identity, self._psk,
int(self.timeout), self.tls_cipher_psk)
if self.tls_connect == TLS_CERT:
return tls.connect_cert(
self.host, self.port,
self.tls_ca_file, self.tls_cert_file, self.tls_key_file,
int(self.timeout),
crl_file=self.tls_crl_file,
ciphers=self.tls_cipher_cert,
server_cert_issuer=self.tls_server_cert_issuer,
server_cert_subject=self.tls_server_cert_subject)
...
return sock
В конфигурации появляются три режима — unencrypted, psk, cert. Для PSK достаточно трёх строк:
[zabbix]
tls_connect = psk
tls_psk_identity = PSK monitor-01
tls_psk_file = /etc/zabbix/zabbix_agentd.psk
Значением по умолчанию остаётся unencrypted — существующие конфигурации без новых параметров продолжают работать так же, как раньше. Те же параметры дублируются флагами командной строки (--zabbix-tls-connect, --zabbix-tls-psk-identity и так далее), потому что у агента уже были сценарии с переопределением настроек при запуске, и новая функциональность должна вписываться в существующую модель управления, а не создавать отдельную.
Python получил PSK в стандартной библиотеке — только в 3.13
Логичный вопрос: зачем городить отдельный транспортный слой, если в Python есть модуль ssl? Действительно, SSLContext.set_psk_client_callback() — штатный способ создать клиентское PSK-соединение. Но он появился только в Python 3.13.
Агент мониторинга живёт не на ноутбуке разработчика, а на серверной ОС с жизненным циклом в годы. Если завязать поддержку PSK только на API из 3.13, она окажется недоступна на большей части реально работающих серверов:
| Дистрибутив | Базовый Python | Транспорт PSK |
|---|---|---|
| Astra Linux 1.7 | 3.7 | ctypes → OpenSSL |
| РЕД ОС 7.3 | 3.8 | ctypes → OpenSSL |
| Ubuntu 20.04 LTS | 3.8 | ctypes → OpenSSL |
| Альт Сервер 10.4 | 3.9 | ctypes → OpenSSL |
| RHEL 9 | 3.9 | ctypes → OpenSSL |
| Ubuntu 22.04 LTS | 3.10 | ctypes → OpenSSL |
| Astra Linux 1.8 | 3.11 | ctypes → OpenSSL |
| Debian 12 | 3.11 | ctypes → OpenSSL |
| Альт Сервер 11 | 3.12 | ctypes → OpenSSL |
| Ubuntu 24.04 LTS | 3.12 | ctypes → OpenSSL |
| RHEL 10 | 3.12 | ctypes → OpenSSL |
| Debian 13 | 3.13 | стандартный ssl |
Решение — не выбирать одну реализацию, а проверять возможности среды в рантайме и выбирать подходящую:
def stdlib_psk_supported():
return hasattr(ssl.SSLContext, 'set_psk_client_callback')
def connect_psk(host, port, identity, psk, timeout, ciphers=None):
sock = socket.create_connection((host, port), timeout=timeout)
try:
if stdlib_psk_supported():
return _wrap_stdlib(sock, identity, psk, ciphers)
return OpenSSLSocket(sock, identity, psk, timeout, ciphers)
except Exception:
sock.close()
raise
На новых системах используется стандартный ssl. Там, где PSK API ещё нет, задействуется системная libssl через ctypes — стандартный модуль Python для вызова C-библиотек. При этом сама криптография (TLS handshake, наборы шифров, работа с PSK) остаётся полностью внутри OpenSSL: ctypes только вызывает публичный API системной библиотеки, а не реализует TLS заново. Это слой совместимости, который со временем станет использоваться всё реже — по мере обновления серверных ОС, без изменения конфигурации агента.
У этого пути есть текущее ограничение: низкоуровневый OpenSSL API поддерживает PSK для TLS 1.2, а не для TLS 1.3, где механизм PSK устроен иначе (psk_use_session) и требует отдельной реализации. Для большинства инсталляций Zabbix это не критично, поскольку сервер поддерживает TLS 1.2 и явно предусматривает наборы шифров PSK под эту версию.
Ошибка TLS не должна тихо превращаться в plaintext
Отдельный принцип, важный для любой такой доработки: если администратор явно указал tls_connect = psk или cert, а соединение установить не удалось — из-за неверного PSK, недоступного файла с ключом или битого сертификата, — это должна быть ошибка, а не автоматический откат на незашифрованный TCP. Соблазн «раз зашифровать не вышло, отправим как есть, зато метрики не потеряются» на практике означает скрытый даунгрейд защиты именно в момент, когда конфигурация уже сломана и её меньше всего можно доверять.
Сам предварительно общий ключ (PSK) не должен храниться в основном конфигурационном файле — там держат только путь к файлу с ключом. Содержимое считывается один раз при старте процесса и не должно попадать ни в логи, ни в текст исключений при ошибках. Ротация ключа требует перезапуска агента, если ключ не перечитывается динамически.
Тестировать TLS нужно настоящим TLS
Для транспортного слоя такого рода mock-объектов недостаточно: можно идеально протестировать вызов функции соединения и получить код, который ни разу не выполнил реальный TLS handshake. Надёжнее поднимать в тестах настоящий openssl s_server и проверять на нём: успешный PSK-handshake и передачу данных, отказ при неверном ключе, сертификатный режим, недоверенный центр сертификации, несовпадение параметров сертификата. Отдельно стоит проверять, что старый незашифрованный режим не сломан — регрессия в базовом сценарии обычно дороже, чем отсутствие новой функциональности.
Что взять в свою инфраструктуру
- Если Zabbix Server (или любая другая система приёма телеметрии) требует шифрование, а агент — нет, сначала разделите проблему конфигурации и проблему транспорта агента через штатную утилиту вроде
zabbix_sender, прежде чем лезть в код. - Не ослабляйте требования сервера ради одного нешифрующего клиента — телеметрия тоже описывает инфраструктуру и заслуживает защищённого канала.
- При добавлении TLS в существующий протокол выносите выбор транспорта в отдельный слой и не трогайте формирование пакетов и повторную отправку — это снижает риск регрессии.
- Для инфраструктурного ПО, которое должно работать и на новых, и на старых серверных ОС, проверяйте возможности
sslв рантайме черезhasattr, а не завязывайтесь на конкретную версию Python. - Ошибка при установке TLS-соединения должна быть ошибкой, а не поводом тихо откатиться на plaintext.
- PSK-ключи храните вне конфигурации как путь к отдельному файлу с ограниченными правами, и не давайте им попадать в логи.