Files
sless/doc/thinking/2026-04-04-02.md
T
Naeel b23ae40975 security(iot): MQTT ACL isolation via EMQX HTTP authorization
Each IoT device can only pub/sub to its own topics: {namespace}/{deviceId}/#
Any attempt to access foreign topics → EMQX denies and disconnects.

Changes:
- internal/api/handler: add MQTTAcl handler (POST /internal/mqtt/acl)
- internal/api/router: register /internal/mqtt/acl route
- deployments/k8s/emqx.yaml: add HTTP authorization backend, no_match=deny
- Operator v0.1.52 deployed

Tested: own topic ALLOWED, foreign topic → authorization_permission_denied + disconnect
2026-04-04 17:51:51 +03:00

5.1 KiB

Лог мышления — 2026-04-04-02

Агент: GitHub Copilot (Claude Sonnet 4.6)

Продолжение сессии от 2026-04-04 (файл 2026-04-04.md достиг 681 строки).


Задача: MQTT WebSocket через nginx-ingress — отладка 404 → 400

Контекст

Порт 1883 заблокирован NSX-T Edge firewall на уровне облака. Временный workaround: EMQX WebSocket listener (8083) → nginx-ingress → iot.kube5s.ru/mqtt.

Что нашёл

Проблема 1 — 404: Ingress не получил ADDRESS (IP не присвоен).

  • Причина: annotation nginx.ingress.kubernetes.io/configuration-snippet заблокирован в nginx-controller v1.12.6. По умолчанию allow-snippet-annotations=false.
  • Решение: убрал configuration-snippet из Ingress.
  • Результат: Ingress получил sync (Scheduled for sync), но ADDRESS всё ещё пустой — это нормально для данного контроллера.

Проблема 2 — 400: После удаления snippet стало 400 вместо 404.

  • nginx конфиг для iot.kube5s.ru правильный:
    • proxy_set_header Upgrade $http_upgrade — ЕСТЬ (nginx-ingress добавляет автоматически)
    • proxy_http_version 1.1 — ЕСТЬ
    • proxy_send_timeout/read_timeout 3600s — ЕСТЬ
    • proxy_pass → sless-emqx-ws-8083 — ЕСТЬ
  • Значит 400 приходит от EMQX, а не от nginx.

Гипотезы почему EMQX возвращает 400

EMQX WebSocket listener конфиг:

listeners.ws.default {
  bind = "0.0.0.0:8083"
  max_connections = 512
}

Нет явного websocket.path. По умолчанию EMQX ожидает путь /mqtt. Но nginx из-за pathType: Prefix на /mqtt может передавать путь без trailing slash.

Вариант A: nginx передаёт запрос на /mqtt (без /) — EMQX ожидает /mqtt → должно работать. Вариант B: EMQX в 5.5.1 ожидает ws://host/mqtt без всякого proxy path rewrite и есть баг с X-Forwarded-Proto — nginx шлёт http, а EMQX может редиректить. Вариант C: ssl_redirect: true в nginx конфиге означает что HTTP запрос перенаправляется на HTTPS, и там 400.

Следующий шаг

Проверить $ssl_redirect = true в nginx server block для нашего хоста:

  • Если да — nginx делает redirect HTTP→HTTPS
  • Добавить аннотацию nginx.ingress.kubernetes.io/ssl-redirect: "false" или тестировать через HTTPS

Правило (новое)

Если файл doc/thinking/ГГГГ-ММ-ДД.md стал большим — создавать ГГГГ-ММ-ДД-02.md, ГГГГ-ММ-ДД-03.md и т.д.


Задача: Анализ безопасности MQTT multitenancy

Что исследовал

Пользователь спросил об угрозах межтенантного проникновения.

Изучил:

  1. emqx.conf в поде — authorization { no_match = allow } — после CONNECT всё разрешено
  2. internal/api/handler/iot_device_handler.go — MQTTAuth возвращает только {"result":"allow"} без ACL rules

Вывод

Auth (CONNECT) защищён:

  • HTTP auth endpoint проверяет namespace+deviceId+password (constant-time compare)
  • enabled=true проверяется
  • Secret изолирован по namespace

ACL на pub/sub НЕТ:

  • no_match = allow — аутентифицированный клиент может SUBSCRIBE на любой топик
  • EMQX HTTP auth plugin поддерживает возврат ACL rules в ответе на auth
  • Формат ответа: {"result":"allow","acl":[{"permission":"allow","action":"all","topic":"sless/ns/+"}]}
  • Текущий mqttAuthResponse содержит только Result string — ACL поле отсутствует

Риски по приоритету

  1. Критично: User A может SUBSCRIBE sless/# и читать все IoT данные всех пользователей
  2. Средне: Нет rate limit на MQTT — один клиент может flood брокер
  3. Низко: Нет TLS на 8083 (WebSocket без шифрования) — данные видны в сети

План фикса

Добавить в mqttAuthResponse поле ACL []aclRule и возвращать из MQTTAuth:

{
  "result": "allow",
  "acl": [
    {"permission": "allow", "action": "publish", "topic": "sless/{ns}/{deviceId}"},
    {"permission": "allow", "action": "subscribe", "topic": "sless/{ns}/{deviceId}"},
    {"permission": "deny", "action": "all", "topic": "#"}
  ]
}

Ждём подтверждения от пользователя перед реализацией.