Files
sless/doc/thinking/2026-04-04-02.md
T
Naeel b48c300ac5 feat(iot-console): IoT управляющий UI v0.1.53
- Добавлен HTML SPA: internal/api/ui/iot-console.html
  Ванильный JS + mqtt.js (CDN), без фреймворков.
  Страницы: вход, список устройств, credentials, эмулятор MQTT, заглушка телеметрии.
- Добавлен go:embed: internal/api/console_embed.go, GET /console
- Добавлен CORS middleware в router.go для http://iot.kube5s.ru
- Ingress emqx-ws-ingress.yaml: /console → sless-operator:9090
- Версия образа v0.1.53, задеплоен

Доступно: http://iot.kube5s.ru/console
2026-04-04 19:28:30 +03:00

14 KiB
Raw Blame History

Лог мышления — 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.goMQTTAuth возвращает только {"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": "#"}
  ]
}

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


Архитектурная дискуссия — IoT телеметрия и хранение данных

Контекст разговора

Пользователь задал вопрос: "куда пишутся данные с IoT датчиков?"

Выяснилось что сейчас данные теряются — function pod получает событие но никуда не сохраняет. Это нормально для serverless (пользователь сам решает), но для IoT платформы нужно автоматическое хранение.

Анализ сценариев использования

Реалистичные клиенты для Nubes (облачный провайдер СНГ, малый/средний бизнес):

  1. Мониторинг объектов (склады, серверные, торговые точки) — температура, влажность, протечка
  2. Умные счётчики / ЖКХ — снятие показаний без выезда
  3. Небольшое производство / агро — теплицы, мини-заводы

Общий паттерн для всех: датчик → данные в БД → алерт если порог → график

Решение по хранению данных

Вопрос: один большой Postgres или отдельный на каждого? Ответ: один Postgres инстанс, но отдельная DATABASE на каждого tenant.

Причины:

  • Вариант со одной таблицей + tenant_id — изоляция программная, ошибка в коде = утечка
  • Отдельная DATABASE — физическая изоляция, разные connection string, разные пароли
  • Клиент B не может подключиться к DATABASE клиента A даже при баге в коде платформы

Структура:

Postgres инстанс
├── sless_platform DB  — системные таблицы (tenants, invocations)
├── tenant_abc DB      — только данные клиента A
└── tenant_def DB      — только данные клиента B

Решение по доступу клиента

Вопрос: давать клиенту прямой доступ к Postgres? Ответ: нет. Только через REST API платформы.

Причины:

  • Postgres внутри кластера, снаружи не торчит (security)
  • Единый endpoint iot.kube5s.ru
  • Легко добавить rate limit, биллинг, кеш
  • Клиент не зависит от деталей реализации хранилища

API:

GET /v1/namespaces/{ns}/iot/telemetry?device=X&from=T&to=T
GET /v1/namespaces/{ns}/iot/devices/{id}/last

Решение по schema.sql

Клиент может положить schema.sql рядом с функцией. При деплое платформа выполняет его в БД tenant'а. Это даёт низкий порог входа — клиент не шарит в Python, но может написать SQL по шаблону.

Ключевое архитектурное решение — разделение операторов

Решение: sless-operator и iot-operator — ОТДЕЛЬНЫЕ компоненты. Пока в одном кластере, но сделать так чтобы могли быть в разных.

Namespace layout:

namespace: sless          — платформа sless (operator, event-dispatcher, RabbitMQ, Postgres invocations)
namespace: sless-{hash}   — tenant функции (function pods)
namespace: iot            — платформа IoT (iot-operator, EMQX, Postgres telemetry)
namespace: iot-{hash}     — tenant IoT (IoTDevice CRDs)

Связь:

  • Общий идентификатор tenant: {hash} одинаковый в обоих namespace
  • MQTT событие → RabbitMQ в sless → function pod в sless-{hash}
  • IoT operator НЕ импортирует пакеты sless-operator (loose coupling)
  • Общение только через k8s API и RabbitMQ

Postgres:

  • sless имеет свой Postgres (invocations)
  • iot имеет свой Postgres (telemetry per tenant)
  • Разные StatefulSet, разные PVC

Что делает пользователь

Клиент:

  1. Подключает устройство → данные автоматически пишутся в его iot_telemetry
  2. Пишет функцию которая реагирует на события
  3. Функция получает DB_DSN в env var (автоматически из Secret)
  4. Может делать SELECT/INSERT в свою БД через обычный SQL в коде функции
  5. Может читать телеметрию через REST API

Plan — следующие шаги (этап IoT Postgres)

  1. Поднять Postgres StatefulSet в namespace iot
  2. В iot-operator при создании IoTDevice namespace → CREATE USER, CREATE DATABASE, CREATE TABLE iot_telemetry, CREATE TABLE iot_devices
  3. Credentials → k8s Secret iot-tenant-{ns}-pg
  4. В iot-mqtt-bridge при получении MQTT сообщения → INSERT в tenant БД
  5. REST API endpoint для чтения телеметрии
  6. При деплое function → прокинуть DB_DSN в env var из Secret
  7. При деплое function → если есть schema.sql → выполнить в tenant БД

Технические решения

  • Postgres: postgres:16-alpine StatefulSet с PVC 10Gi в namespace iot
  • Connection pool: pgxpool (pgx v5) per-tenant, lazy init, max 5 conn per tenant
  • Таблица telemetry: (id bigserial, device_id text, ts timestamptz default now(), payload jsonb)
  • Индекс: (device_id, ts DESC) для быстрых запросов по устройству за период
  • Retention: пока без TTL, добавить позже через pg_partman или cron job

2026-04-04 — IoT Console UI: план и реализация

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

Постановка задачи

Пользователь сформулировал: нужен UI для управления IoT устройствами. Причина: не все пользователи работают через Terraform/API напрямую. Нужно: создать устройство, получить credentials, прошить в устройство, проверить отправку данных.

Ключевое решение: эмулятор устройства в браузере

MQTT WebSocket уже работает: ws://iot.kube5s.ru:80/mqtt. Браузер через mqtt.js (CDN) может подключиться как устройство напрямую. Это значит: эмулятор — это не "симуляция", а реальная публикация MQTT сообщений.

Когда у клиента ещё нет физического устройства — он тестирует через эмулятор. Это закрывает весь цикл без необходимости устанавливать MQTT-клиент.

Архитектурные решения UI

Стек: ванильный HTML/CSS/JS + mqtt.js (CDN). Никаких фреймворков. Где хранить: встраиваем в бинарник оператора через go:embed.

  • Файл: internal/api/ui/iot-console.html
  • Маршрут: GET /console

Где доступен: http://iot.kube5s.ru/console

  • Ingress добавляем path /consolesless-operator:9090

Почему не https://sless.kube5s.ru/console:

  • UI на HTTPS + MQTT WS без TLS = mixed content, браузер блокирует
  • UI на HTTP + MQTT WS = нет mixed content, всё работает
  • HTTP → HTTPS API вызовы разрешены (это не mixed content)
  • Нужен только CORS на API стороне

CORS: заголовки Access-Control-Allow-Origin: http://iot.kube5s.ru + OPTIONS preflight

Страницы

  1. Вход: API адрес + MQTT брокер + namespace + токен → localStorage
  2. Список устройств: таблица, создать, удалить
  3. Устройство (3 вкладки):
    • Credentials: username, password скрыт, топик, инструкция
    • Эмулятор: подключиться → JSON payload → send / авто
    • Телеметрия: "скоро"

Следующие шаги после UI

  1. Postgres StatefulSet в namespace iot
  2. INSERT в iot_telemetry из mqtt-bridge
  3. REST API для чтения телеметрии
  4. Заполнить вкладку "Телеметрия" в UI