Files
sless/doc/thinking/2026-04-04-02.md
T
Naeel d078d3156f doc(thinking): лог сессии 2026-04-04 — TLS, UX, Nubes rebrand
- Разбор проблемы crypto.subtle (HTTP → HTTPS)
- Проблема 404 после apply (Docker layer cache)
- Убраны поля API/MQTT из формы входа
- Ребрендинг Nubes: палитра #001C34, логотип SVG, favicon
- Таблица версий v0.1.54–v0.1.58 с коммитами
2026-04-04 20:39:51 +03:00

21 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

Агент: GitHub Copilot (Claude Sonnet 4.6) — продолжение сессии 2026-04-04

Исправления и улучшения IoT Console UI (v0.1.53 → v0.1.58)

Проблема 1: crypto.subtle.digest — Cannot read properties of undefined

Симптом: Пользователь вставил токен, получил ошибку "Cannot read properties of undefined (reading 'digest')".

Анализ: crypto.subtle доступен ТОЛЬКО на HTTPS-страницах (Secure Context). Консоль раздавалась по HTTP (http://iot.kube5s.ru/console). На HTTP crypto.subtle === undefined.

Решение: Перевести консоль на HTTPS — это устранит корень проблемы и заодно уберёт необходимость в pure-JS SHA256. Попытка написать pure-JS SHA256 была правильной как fallback, но правильнее — исправить инфраструктуру.

Действия:

  1. emqx-ws-ingress.yaml: добавлена TLS-секция + cert-manager.io/cluster-issuer: letsencrypt-prod, ssl-redirect: "true", secretName: iot-kube5s-ru-tls
  2. router.go: CORS Allow-Origin: http://https://iot.kube5s.ru
  3. iot-console.html: дефолт MQTT брокера ws://wss://
  4. cert-manager автоматически выпустил сертификат Let's Encrypt (READY: True за ~34 сек)
  5. Собрали v0.1.54, задеплоили

Косяк при apply: kubectl apply взял старый Ingress из кэша (только путь /mqtt, без /console). Пришлось использовать kubectl replace вместо apply.

Итог: https://iot.kube5s.ru/console → 200, TLS v1.3, CN=iot.kube5s.ru, Let's Encrypt R13. crypto.subtle заработал.


Проблема 2: 404 после перехода на HTTPS (v0.1.54)

Симптом: После kubectl apply + rollout — curl возвращал 404.

Анализ: Запрос доходил до пода (видно в логах), но оператор отвечал 404. Значит маршрут /console не регистрировался. Проверили: файл iot-console.html существует на диске, go:embed прописан, маршрут в router.go есть. Причина: первый docker build взял Go-слои из кэша Docker — старый бинарь без /console маршрута.

Решение: Пересборка с --no-cache. После пуша нового диджеста и kubectl rollout restart — заработало.


Улучшение: убрать поля API/MQTT из формы входа (v0.1.56)

Анализ: Пользователь справедливо спросил "ЗАЧЕМ юзеру это вводить?" — адреса https://sless.kube5s.ru и wss://iot.kube5s.ru/mqtt фиксированы для данного деплоя. Пользователь не должен их трогать.

Решение: Удалены <input id="f-api"> и <input id="f-mqtt"> из формы. В doLogin() адреса берутся из хардкода, не из DOM. Форма стала: только поле токена + кнопка "Войти".

Параллельно: Добавлен блок <details class="help-block"> внизу страницы устройства — 5 шагов инструкции: Credentials → формат JSON → Эмулятор → Авто → Телеметрия (скоро).


Ребрендинг: Nubes brand design (v0.1.57)

Задача: "Оформи чтобы строго, чётко — как на terra.k8c.ru".

Исследование:

  • Скачал SVG логотипа: https://terra.k8c.ru/docs/nubes/nubes/2.0.2/30_registry/assets/logo.svg
  • Логотип залит #001C34 — это основной Nubes Navy цвет
  • Сайт nubes.ru использует тёмно-синий (#001C34) как бренд-прайм

Палитра:

Переменная Цвет Назначение
brand primary #001C34 Navbar, карточки, логотип
page bg #001120 Фон страницы
card surface #001929 Карточки .card
borders #0b2d50 Границы, разделители
accent #1a7fd4 Кнопки, табы, ссылки
text primary #e2ecf6 Основной текст
text secondary #6b8eaa Метки, подписи
text muted #2d5070 Отключённые, подсказки

Изменения в CSS:

  • Navbar: background: #001C34, логотип SVG с filter: brightness(0) invert(1) (белый)
  • Badges: прямоугольные (border-radius: 4px), UPPERCASE, компактные
  • Кнопки: font-weight: 600, letter-spacing: 0.02em
  • Таблицы: заголовки color: #2d5070 — строгие, тихие
  • .help-num: квадратные (4px), не круглые

Форма входа: логотип SVG (инвертированный) вместо , подпись IoT Console uppercase вместо названия по-русски.


Favicon (v0.1.58)

Задача: Иконка вкладки браузера — как у Nubes docs.

Исследование: curl https://terra.k8c.ru/docs/nubes/nubes/2.0.2/<link rel="icon" href="30_registry/assets/favicon.png">

URL: https://terra.k8c.ru/docs/nubes/nubes/2.0.2/30_registry/assets/favicon.png

Решение: Добавлена одна строка в <head>:

<link rel="icon" href="https://terra.k8c.ru/docs/nubes/nubes/2.0.2/30_registry/assets/favicon.png">

Итоговые версии

Версия Изменение Коммит
v0.1.54 TLS на iot.kube5s.ru, wss://, CORS https fb6f9d4
v0.1.55 Help-блок на странице устройства e547871
v0.1.56 Убраны поля API/MQTT из формы входа e547871
v0.1.57 Nubes brand rebrand — палитра, логотип 0400f97
v0.1.58 Favicon Nubes 93e87a3

Текущее состояние

  • https://iot.kube5s.ru/console — работает, TLS, Nubes-дизайн, favicon
  • MQTT: wss://iot.kube5s.ru/mqtt
  • crypto.subtle работает (HTTPS)
  • Форма входа: только токен
  • Namespace скрыт от пользователя
  • Телеметрия — заглушка, бэкенд не написан

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

Бэкенд телеметрии:

  1. Postgres StatefulSet в namespace iot
  2. Tenant provisioning при создании IoTDevice
  3. INSERT в mqtt-bridge
  4. REST API чтения
  5. Вкладка Телеметрия в UI