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
105 lines
5.1 KiB
Markdown
105 lines
5.1 KiB
Markdown
# Лог мышления — 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`:
|
|
```json
|
|
{
|
|
"result": "allow",
|
|
"acl": [
|
|
{"permission": "allow", "action": "publish", "topic": "sless/{ns}/{deviceId}"},
|
|
{"permission": "allow", "action": "subscribe", "topic": "sless/{ns}/{deviceId}"},
|
|
{"permission": "deny", "action": "all", "topic": "#"}
|
|
]
|
|
}
|
|
```
|
|
|
|
Ждём подтверждения от пользователя перед реализацией.
|