Пароль от моего брокера два года жил в одном месте — в скетче-шаблоне, из которого я копировал каждую новую плату. Восемь устройств, один логин, один пароль. Потом тем же паролем я открыл знакомому доступ к температуре теплицы: дал строку из шаблона и пообещал, что там только чтение. Проверять права никто не мог — их не существовало. Знакомый с тем же паролем мог включать насос, открывать ворота и вообще всё, что умеет мой дом; мы оба не заметили разницы, потому что её не было.
Точка невозврата наступила, когда пришлось менять пароль: тестовый макет с этой же строкой в коде уехал к племяннику вместе с учётными данными брокера, и в логах появлялись подключения из чужой Wi-Fi сети. Смена пароля обернулась экспедицией: восемь плат, три комнаты, теплица и улица — каждую снять, перепрошить, вернуть. Полтора вечера. За те же полтора вечера можно было один раз завести каждому устройству собственный логин — но я не знал, что брокер умеет это из коробки.
В этом уроке три этажа доступа. Первый — пароли: allow_anonymous false и password_file. Второй — ACL: правила, кто какой топик может читать и писать. Третий — дисциплина: логин на каждое устройство и шаблон прав, который не приходится переписывать при добавлении новой платы.
Пароли: password_file и allow_anonymous
Базовый выключатель гостевого доступа — allow_anonymous false в конфиге брокера: после него без логина и пароля не пустят никого, включая ваши платы. Установка брокера и рабочий конфиг были в уроке про Mosquitto — сюда добавляются две строки: password_file и acl_file. Пользователей создаёт mosquitto_passwd: первый запуск с флагом -c создаёт файл, все следующие — без флага, просто добавляют строку. Внутри файла лежат не пароли, а хэши: утёкший password_file не отдаёт учётные данные, но права на файл всё равно держите минимальные — владельцем mosquitto и правами 600, читать его должен только сам брокер, подбор по хэшу ещё никто не отменял.
Со стороны платы всё умещается в один вызов: client.connect(clientId, логин, пароль) — или client.setCredentials() до connect, если учётные данные у вас в отдельной константе. Неправильные учётные данные брокер отвечает кодом 4 из таблицы кодов состояния — bad user name or password. Отказ вежливый: плата переподключится по своему расписанию, а вы увидите причину в client.state() за секунду.
Первый этаж закрывает вопрос «впускаем ли вообще». Второй — «что разрешено внутри»: пароль открывает дверь, ACL решает, куда за ней можно идти.
Отдельное слово про пароль внутри платы. Он лежит в прошивке открытым текстом — дамп памяти или разбор бинаря его достанут, и здесь надо трезво считать риски. Пароль от брокера в домашней плате защищает не сейф, а ветку топиков: с per-device логином утечка одного пароля отдаёт чужаку home/pump/# и ничего больше, а отзыв — одна строка в password_file. Один пароль на весь дом в тех же руках — ключи от всего дома сразу, и менять их придётся на всех платах.
Мой компромисс: пароли уникальные и длинные, логин совпадает с clientId, а в скетче пароль живёт в отдельной константе наверху файла — чтобы при выкладке кода на GitHub не пришлось чистить полдюжины строк. Для больших проектов есть чиповые секреты и Provisioning, но в самодельном доме честнее признать уровень угрозы: сосед с анализатором трафика — это про TLS, а школьник с распечаткой вашего бинаря — пока фантастика.
# 1. Пользователи: -c только для первого, иначе файл перезапишетсяsudo mosquitto_passwd -c /etc/mosquitto/passwd pumpsudo mosquitto_passwd /etc/mosquitto/passwd dashsudo mosquitto_passwd /etc/mosquitto/passwd guest # 2. /etc/mosquitto/conf.d/security.conflistener 1883 0.0.0.0allow_anonymous falsepassword_file /etc/mosquitto/passwdacl_file /etc/mosquitto/aclfile # 3. /etc/mosquitto/aclfileuser pumptopic readwrite home/pump/#user dashtopic read home/#topic readwrite home/dash/#user guesttopic read home/+/status # Шаблон для устройств: у каждой платы свой префикс по логинуpattern readwrite home/%u/# # 4. Перезапуск и проверка чужими рукамиsudo systemctl restart mosquittomosquitto_pub -u pump -P parol -t home/lock/on -m 1 # Not authorizedmosquitto_pub -u pump -P parol -t home/pump/on -m 1 # пройдёт # Со стороны платы:// client.setCredentials("pump", "parol");// client.connect("dacha-pump-1");// if (!client.subscribe("home/pump/on")) { Serial.println("ACL: otkaz"); }ACL: кто какой топик трогает
ACL-файл — это матрица прав: строка user и под ней строки topic с правом read — читать, то есть подписываться, write — писать, то есть публиковать, readwrite — и то и другое. Правило насоса: user pump, topic readwrite home/pump/# — устройство видит свою ветку и не видит чужих. Дашборд: read на весь home/# — он читает весь дом, а пишет только в home/dash/#. Гость: read на home/+/status — статусы видит, команды щёлкать не смог бы, даже если бы выучил все топики наизусть. Топики — это API дома, как мы договаривались ещё в уроке про устройство протокола, — и вот у этого API появились права на каждый адрес.
Дальше приём, который убирает девяносто процентов ручной работы: pattern readwrite home/%u/#. Строка с pattern применяется ко всем пользователям сразу, а %u подставляет логин. Условие из документации Mosquitto: подстановка занимает уровень топика целиком — склеить %u с чем-то ещё на том же уровне не выйдет, это отдельный уровень и есть. Логин pump получает home/pump/#, логин veranda — home/veranda/#: новая плата получает права одной строкой в password_file, ACL трогать не нужно. Право write без read удобно устройствам-публикаторам: датчик пишет в свою ветку, но чужих сообщений не слышит — меньше трафика и меньше поверхности для подстроенных команд. Мой шаблон — логин совпадает с clientId, правила имён были в уроке про паспорт устройства, — тогда логи брокера читаются как журнал: кто подключился и что опубликовал.
Проверяйте права руками, а не верой: mosquitto_pub с учётными данными платы в чужой топик должен ответить Not authorized — это и есть тест ACL в одну строку. Из скетча subscribe при отказе возвращает false: брокер шлёт SUBACK с кодом 0x80, и библиотека честно об этом говорит. Проверяйте возврат, иначе плата будет жить с подпиской, которой нет, — тем же белым списком, что и валидация команд в callback, только на стороне брокера.
| Логин | Чьё это | Читает | Пишет |
|---|---|---|---|
| pump | плата насоса в теплице | home/pump/# | home/pump/# |
| dash | дашборд на телефоне | home/# | home/dash/# |
| guest | друзья на выходных | home/+/status | ничего |
| все остальные | нет такого логина | ничего | ничего |
Теперь формально — почему один пароль на всех, это не упрямство, а арифметика инцидентов. Первый счёт: смена пароля оборачивается перепрошивкой всего парка — моя экспедиция по восьми платам подтверждает. Второй: в логах брокера клиенты с одним логином неразличимы, и после инцидента вы не ответите на вопрос «кто это был». Третий и главный: права выдаются логину, значит у всех плат они одинаковые — макет племянника с тем же паролем мог щёлкать отоплением, хотя ему нужна была температура. Три счёта, один вывод: на каждую плату свой логин.
Моя граница применимости: две-три платы, все ваши руки, брокер в локалке за NAT — общий пароль терпим, учёт каждой платы себя не окупит. От пятого устройства, при любом госте и тем более при выносе брокера в интернет — per-device логин, права по pattern, пароли уникальные. Затраты — одна строка в password_file на устройство, а отзывается скомпрометированная плата удалением одной строки. И держите рядом текстовый файл паспортов: логин, плата, место установки, дата прошивки — тот же список, что вы заводите для clientId, просто с паролем в колонке больше.
Соберите стену из трёх этажей: TLS из прошлого урока прячет пароль в дороге, ACL ограничивает последствия кражи, per-device логин делает отзыв дешёвым. Каждый этаж по отдельности пробивается, вместе — уже нет. А чтобы почувствовать ACL без брокера, разберите модель ниже: монитор порта играет брокера с паролями и правами, а разбор строки работает как командный парсер из Serial — четыре слова через пробел, и видно, кто проходит, а кто упирается в матрицу прав.
- Введите pump pump-4471 home/pump/on 1 — реле щёлкнет: свой логин, свой топик. Затем dash с его паролем и той же командой — DENY: права выдаются логину, а не знанию топиков.
- Введите guest guest-1108 home/kotel/status 0: ALLOW напечатается, но реле не сдвинется — право на чтение статуса не даёт писать в топик, а исполнитель висит на чужих командах.
- Испортите пароль: pump s-navorotom home/pump/on 1 — DENY rc=4. Сверьте с таблицей кодов: ровно этот код вернёт state() плате с плохими учётными данными.
- Добавьте в switch четвёртого пользователя vidya с правом write в home/vidya/# — три строки в модели. Посчитайте, сколько строк это три устройства в настоящем acl_file, и вспомните, чем отличается -c от запуска без флага.
- allow_anonymous false плюс password_file: доступ только по логину; mosquitto_passwd с -c создаёт файл, без -c добавляет пользователя
- Код 4 — неверные учётные данные, код 5 — логин принят, но прав нет: та же таблица кодов, что и у подключения
- ACL: user и topic read/write/readwrite; pattern readwrite home/%u/# даёт каждому логину свой префикс, %u занимает уровень целиком
- На устройство — свой логин, равный clientId; subscribe при отказе возвращает false — проверяйте возврат
- Один пароль на всех: смена пароля — перепрошивка парка, в логах клиенты неразличимы, права одинаково широки у всех
- 1. mosquitto_passwd -c запущен на существующем password_file. Что произойдёт?
- 2. Плата подключается с неверным паролем. Какой код вернёт client.state()?
- 3. Что делает pattern readwrite home/%u/# в acl_file?
- 4. Гостю нужно только смотреть статусы. Какая строка ACL подходит?
- 5. Почему один пароль на все платы — плохая идея?
Частые вопросы по теме урока
- mosquitto_passwd -c запущен на существующем password_file. Что произойдёт?
- Флаг -c создаёт файл с нуля. Добавление — запуск без флага. После правок проверяйте вход старой платы, а не только нового пользователя: так вы увидите пропажу за минуту, а не из звонка соседней платы. Файл перезапишется: останется только новый пользователь
- Плата подключается с неверным паролем. Какой код вернёт client.state()?
- Код 4 — bad credentials: сам брокер отверг учётные данные. Код 5 — логин принят, но прав нет; код 2 — проблемы с clientId. Плата при этом честно переподключится по своему расписанию, а причина будет лежать в state(). 4
- Что делает pattern readwrite home/%u/# в acl_file?
- pattern применяется ко всем пользователям, %u подставляет логин целым уровнем. pump получает home/pump/#, veranda — home/veranda/#. Каждому логину даёт права на свою ветку home/<логин>/#
- Гостю нужно только смотреть статусы. Какая строка ACL подходит?
- read на home/+/status отдаёт статусы всех веток и не даёт писать. Маска + на уровне имени покрывает kotel, pump и будущие ветки, не открывая команд, — readwrite открыл бы команды, write — публикацию без чтения. topic read home/+/status
- Почему один пароль на все платы — плохая идея?
- Арифметика инцидента: отзыв пароля стоит экспедиции по всем платам, аудит невозможен, права у всех одинаково широки. Per-device логин снимает все три счёта — и паспорт на каждую плату: логин, место, дата прошивки. Смена пароля — перепрошивка парка, а в логах клиенты неразличимы