Logo Craft Homelab Docs Контакты Telegram
TLS PSK для мониторинга PostgreSQL: защищённый канал агента к Zabbix Трендовые github проекты в нашем телеграм канале. Подпишись →
9 сентября 2026 г.

Когда 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.73.7ctypes → OpenSSL
РЕД ОС 7.33.8ctypes → OpenSSL
Ubuntu 20.04 LTS3.8ctypes → OpenSSL
Альт Сервер 10.43.9ctypes → OpenSSL
RHEL 93.9ctypes → OpenSSL
Ubuntu 22.04 LTS3.10ctypes → OpenSSL
Astra Linux 1.83.11ctypes → OpenSSL
Debian 123.11ctypes → OpenSSL
Альт Сервер 113.12ctypes → OpenSSL
Ubuntu 24.04 LTS3.12ctypes → OpenSSL
RHEL 103.12ctypes → OpenSSL
Debian 133.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-ключи храните вне конфигурации как путь к отдельному файлу с ограниченными правами, и не давайте им попадать в логи.