Протокол MQTT держит большую часть самодельной телеметрии: датчики температуры и влажности, счётчики воды, реле, контроллеры автополива, тревожные герконовые сенсоры. На стороне сервера этой инфраструктуры чаще всего стоит брокер Mosquitto: проект фонда Eclipse, написан на C, ставится на что угодно, от скромной виртуалки до платы Raspberry Pi Zero. Развернуть его можно за полчаса, но установка по умолчанию таит две ловушки: слушает всё подряд без шифрования и допускает анонимов. Дальше показана настройка, после которой брокер принимает клиентов только поверх TLS, проверяет сертификаты и режет права доступа к топикам. Одна служба, один файл настроек, предсказуемый расход памяти: всё, что нужно телеметрии, и ничего, что отвлекает от данных.

Изменения в ветке 2.x, о которых забывают при обновлении

Свежий релиз нумеруется 2.1.2, ветка 2.0 живёт с 2020 года, и переход на неё стал неожиданностью для многих владельцев брокеров. До версии 2.0 служба без настроек слушала порт 1883 на всех интерфейсах и принимала анонимные подключения. Теперь поведение другое: если в конфигурации нет ни одной секции listener, брокер слушает только локальный адрес 127.0.0.1, а внешние клиенты получают отказ. Это не сбой, а новая политика безопасности, из-за которой после обновления 1.x на 2.x половина парков устройств разом теряет связь.

Основной файл живёт в /etc/mosquitto/mosquitto.conf и подключает каталог conf.d, служба управляется юнитом mosquitto.service, а в Docker работает образ eclipse-mosquitto. Конфигурацию проверяют запуском mosquitto -c /etc/mosquitto/mosquitto.conf -v: брокер напишет, какие строки он прочитал и где споткнулся. Статус службы показывает systemctl status mosquitto, и смотреть его полезно первым делом, когда клиенты перестают подключаться. Контейнерный вариант подчиняется той же логике: без сопоставления портов образ недоступен снаружи, а сопоставленный порт 1883 открывает миру нешифрованный вход, чего как раз и хочется избежать. Правильная схема выносит наружу только 8883 и, при необходимости, 9001 для вебсокетов.

Выпуск корневого сертификата и подпись клиентских запросов

Для брокера с публичным доменом сертификаты выписываются через Let's Encrypt: certbot certonly --standalone -d example.com, службу на пару минут останавливают, затем в конфиг попадают fullchain.pem и privkey.pem, а хук обновления перечитывает их автоматически. Для закрытой сети, где стики и платы живут без доменных имён, проще собственный центр сертификации. Вся процедура сводится к пяти шагам:

  1. Сгенерировать корневой ключ и самоподписанный сертификат центра командой openssl req -x509 -newkey rsa:4096 -sha256 -nodes -days 3650;
  2. Выпустить ключ и запрос CSR для брокера, в поле CN указать имя хоста или домен;
  3. Подписать запрос брокера корнем командой openssl x509 -req -CA ca.crt -CAkey ca.key -days 1825;
  4. Повторить выпуск для каждого клиента: стика, платы с датчиками, сервиса панели;
  5. Разложить файлы в /etc/mosquitto/certs, ключу брокера выставить права 0600 и владельца mosquitto.

Корневой ключ хранят вдали от сервера: он выдаёт доверие всему парку, и потерять его дешевле, чем выпустить заново. Срок жизни клиентских сертификатов обычно делают меньше корневого, а отозванные записи попадают в crl-файл, если включена проверка отзыва. Современные библиотеки проверяют не только поле CN, но и список SAN, поэтому при выпуске сертификата брокера имя хоста добавляют и туда, иначе часть клиентов отвергнет соединение с диагнозом о несовпадении имени. Формат добавления известен из документации openssl: расширение subjectAltName перечисляет DNS-имена и IP-адреса, и в закрытой сети список легко держать коротким.

Конфигурация защищённого прослушивателя с TLS

Ядро настройки умещается в десяток строк:

listener 8883
cafile   /etc/mosquitto/certs/ca.crt
certfile /etc/mosquitto/certs/server.crt
keyfile  /etc/mosquitto/certs/server.key
tls_version tlsv1.2
require_certificate true
use_identity_as_username true
per_listener_settings true
allow_anonymous false
password_file /etc/mosquitto/passwd
acl_file      /etc/mosquitto/acl

Порт 8883 занят MQTT поверх TLS, а старый 1883 остаётся для локальных задач и убирается целиком, если дом используется только по шифрованному каналу. Параметр require_certificate обязывает клиента предъявить сертификат, подписанный корнем из cafile, а use_identity_as_username подставляет поле CN вместо имени пользователя, поэтому пароль не нужен: аутентификация держится на криптографии. Ограничение tls_version tlsv1.2 отсекает устаревшие версии протокола, свежие сборки умеют и TLS 1.3. Для панелей в браузере добавляют второй слушатель: listener 9001 и protocol websockets с тем же набором сертификатов, а per_listener_settings true даёт каждому слушателю собственные правила доступа. Цифры портов это соглашение, а не закон: 1883 держат для открытого MQTT без шифрования, 8883 для MQTT поверх TLS, 8884 для шифрованного подключения с обязательными клиентскими сертификатами, 9001 для вебсокетов. Брокер примет и любой свободный порт, если в локальной сети приняты собственные правила. Отдельный слушатель может слушать не все интерфейсы, а конкретный адрес: в строке listener указывают bind-адрес перед портом, и сегмент домашней сети получает вход только с одной ноги сервера.

Пароли, права доступа и ACL по топикам

Клиентские сертификаты избыточны для бытового парка, где двадцать дешёвых модулей. Тогда опора на пароли: allow_anonymous false и password_file. Первый пользователь создаётся командой mosquitto_passwd -c /etc/mosquitto/passwd greenhouse, следующие добавляются той же командой без флага -c, удаление делает флаг -D. Свежие версии хранят хэш argon2id по умолчанию, ветка 2.0 использовала sha512-pbkdf2, а формат sha512 остался для совместимости с брокерами 1.6 и старше. Права на топики описываются в файле ACL:

user greenhouse
topic write greenhouse/#

user dashboard
topic read greenhouse/#
topic read $SYS/#

pattern write home/%u/events

Контроллер полива пишет только в свой префикс, панель читает телеметрию и системные метрики, а pattern-правило выдаёт каждому пользователю личную ветку, подставляя его имя вместо %u. Есть и подстановка %c по идентификатору клиента. Правила читаются сверху вниз, первое совпадение решает судьбу сообщения, поэтому запрещающее правило deny ставят выше разрешающих. Такой файл безжалостно честен: датчик отопления не запишет ни одного байта в ветку камеры. Альтернатива статичному файлу это плагин Dynamic Security: пользователи, группы и права создаются командами mosquitto_ctrl прямо на работающем брокере, без перезагрузки службы и ручного пересчёта хэшей. Для парка, где устройства появляются и списываются ежемесячно, этот режим заметно экономит нервы, хотя классический ACL проще переносится между серверами.

Проверка канала и поведение клиентов

Работоспособность проверяется с другой машины парой команд. Подписка на все ветки парка: mosquitto_sub -h example.com -p 8883 --cafile ca.crt -t "greenhouse/#" -v. Публикация показания: mosquitto_pub -h example.com -p 8883 --cafile ca.crt -t "greenhouse/temperature" -m "22.7" -q 2 -r. Вывод обеих команд мгновенно показывает, жив ли канал, правильно ли выбран сертификат и приходит ли сообщение с нужным уровнем доставки. Ошибка вида certificate verify failed означает неверный корень на клиенте, а handshake failure чаще говорит о несовпадении версий TLS между клиентом и брокером.

Уровни QoS работают как обещано в спецификации: ноль означает максимум одну попытку без подтверждения, единица гарантирует минимум одну доставку, двойка даёт ровно одну даже на ненадёжном радио. Для команд реле и показаний счётчиков берут QoS 1 или 2, для плотного потока телеметрии раз в секунду хватает нуля. Флаг -r делает сообщение удерживаемым, и новый клиент сразу получает последнее значение без ожидания. Keepalive держат в пределах 30-60 секунд, чтобы пропажа датчика замечалась быстро. Предварительное сообщение LWT публикуется брокером вместо клиента, если тот пропал без прощания, и панель мгновенно узнаёт о выключившемся устройстве. В MQTT 5 параметр session expiry позволяет пережить краткие разрывы связи без потери очереди сообщений. Сессии в третьей версии протокола управляются флагом clean session: значение true заставляет брокера забыть подписки и очереди клиента при отключении, значение false сохраняет их до следующего подключения. Пятая версия тоньше: интервал жизни сессии задаётся в секундах, и кратковременная потеря радио у платы не оборачивается потерей сообщений. Число сообщений, отправляемых одновременно на уровнях QoS 1 и 2, ограничивает max_inflight_messages, по умолчанию двадцать, и для медленных клиентов это значение уменьшают.

Мониторинг и ресурсы брокера

Брокер сам публикует метрики в системные топики $SYS: число подключённых клиентов, счётчики принятых и отправленных сообщений, объём памяти, время работы. Период обновления задаётся параметром sys_interval, по умолчанию десять секунд. Подписка на $SYS/# даёт готовую базу для графика нагрузки без внешних экспортеров, хотя продвинутая схема тянет метрики в Prometheus через отдельный экспортер. Строки из системных топиков выглядят так:

$SYS/broker/clients/connected 42
$SYS/broker/messages/received 118322
$SYS/broker/load/messages/received/1min 18.42
$SYS/broker/uptime 1542 seconds

По ним видно и пиковую нагрузку, и накопленную статистику, а одна минутная серия метрик load отражает усреднённый трафик за окно наблюдения.

Лимиты настраиваются под нагрузку: max_connections со значением -1 снимает ограничение, max_queued_messages по умолчанию 1000 на клиента с подпиской, а memory_limit не даёт одному клиенту съесть всю память. Для тысяч подключений в systemd-юните поднимают лимит файловых дескрипторов: дроп-ин файл с LimitNOFILE=65536 в каталоге /etc/systemd/system/mosquitto.service.d. На одном ядре ARM-платы брокер спокойно держит сотни клиентов, на обычном VPS после поднятия лимитов счёт одновременных соединений идёт на десятки тысяч. Журнал уходит в /var/log/mosquitto/mosquitto.log через log_dest file, уровни error, warning, notice, information включаются по вкусу, а ротацию делает logrotate. Внешний доступ открывают точечно: правило ufw allow 8883/tcp пускает шифрованный вход, а 1883 остаётся закрытым от внешних интерфейсов даже в локальных сетях с доверенными устройствами. Строки журнала с повторяющимися неудачными попытками входа вылавливаются средством вроде fail2ban, которое после серии ошибок закрывает адрес на стороне межсетевого экрана. Такая связка снимает половину вопросов на поверку, когда в логах мелькают посторонние адреса с рандомными именами пользователей.

Копии, обновления и спокойная эксплуатация

Включённый параметр persistence true и путь persistence_location /var/lib/mosquitto/ дают файл базы mosquitto.db, где лежат удерживаемые сообщения и очереди. Копия снимается на остановленной службе, файл один, поэтому бэкап не сложнее tar-архива. Частоту записи на диск регулирует autosave_interval, в секундах, и при частых retained-сообщениях значение уменьшают. В контейнерном образе рабочие каталоги принадлежат внутреннему пользователю с UID 1883, поэтому каталоги на хосте перед первым запуском отдают ему командой chown -R 1883:1883, иначе брокер упадёт с ошибкой доступа к файлу базы. Обновления проходят незаметно: apt upgrade или docker compose pull, после чего брокер поднимается за секунду. Контейнерная схема выглядит так:

services:
  mosquitto:
    image: eclipse-mosquitto:2.1.2
    ports:
      - "8883:8883"
      - "9001:9001"
    volumes:
      - /opt/mosquitto/config:/mosquitto/config
      - /opt/mosquitto/data:/mosquitto/data
      - /opt/mosquitto/log:/mosquitto/log
    restart: unless-stopped

Перед обновлением сохраняют и конфигурацию, и базу, потому что миграция очередей при смене ветки бывает капризной. Сертификаты Let's Encrypt обновляются сами, если в настройках certbot есть хук перечитывания, и брокер даже не заметит ротацию.

После такой настройки Mosquitto превращается в тихую и предсказуемую часть инфраструктуры: он не выйдет в открытый интернет без шифрования, не пустит чужую железку в чужие ветки и честно покажет метриками, что происходит внутри. Останется только раз в год проверять срок корневого сертификата, и брокер будет молча собирать телеметрию, пока вся остальная техника суетится вокруг.