- Разбор проблемы crypto.subtle (HTTP → HTTPS) - Проблема 404 после apply (Docker layer cache) - Убраны поля API/MQTT из формы входа - Ребрендинг Nubes: палитра #001C34, логотип SVG, favicon - Таблица версий v0.1.54–v0.1.58 с коммитами
21 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
Что исследовал
Пользователь спросил об угрозах межтенантного проникновения.
Изучил:
emqx.confв поде —authorization { no_match = allow }— после CONNECT всё разрешено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 поле отсутствует
Риски по приоритету
- Критично: User A может SUBSCRIBE
sless/#и читать все IoT данные всех пользователей - Средне: Нет rate limit на MQTT — один клиент может flood брокер
- Низко: Нет 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 (облачный провайдер СНГ, малый/средний бизнес):
- Мониторинг объектов (склады, серверные, торговые точки) — температура, влажность, протечка
- Умные счётчики / ЖКХ — снятие показаний без выезда
- Небольшое производство / агро — теплицы, мини-заводы
Общий паттерн для всех: датчик → данные в БД → алерт если порог → график
Решение по хранению данных
Вопрос: один большой 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
Что делает пользователь
Клиент:
- Подключает устройство → данные автоматически пишутся в его
iot_telemetry - Пишет функцию которая реагирует на события
- Функция получает
DB_DSNв env var (автоматически из Secret) - Может делать SELECT/INSERT в свою БД через обычный SQL в коде функции
- Может читать телеметрию через REST API
Plan — следующие шаги (этап IoT Postgres)
- Поднять Postgres StatefulSet в namespace
iot - В iot-operator при создании IoTDevice namespace →
CREATE USER,CREATE DATABASE,CREATE TABLE iot_telemetry,CREATE TABLE iot_devices - Credentials → k8s Secret
iot-tenant-{ns}-pg - В iot-mqtt-bridge при получении MQTT сообщения → INSERT в tenant БД
- REST API endpoint для чтения телеметрии
- При деплое function → прокинуть
DB_DSNв env var из Secret - При деплое function → если есть
schema.sql→ выполнить в tenant БД
Технические решения
- Postgres:
postgres:16-alpineStatefulSet с PVC 10Gi в namespaceiot - 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
/console→sless-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
Страницы
- Вход: API адрес + MQTT брокер + namespace + токен → localStorage
- Список устройств: таблица, создать, удалить
- Устройство (3 вкладки):
- Credentials: username, password скрыт, топик, инструкция
- Эмулятор: подключиться → JSON payload → send / авто
- Телеметрия: "скоро"
Следующие шаги после UI
- Postgres StatefulSet в namespace
iot - INSERT в iot_telemetry из mqtt-bridge
- REST API для чтения телеметрии
- Заполнить вкладку "Телеметрия" в 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, но правильнее — исправить инфраструктуру.
Действия:
emqx-ws-ingress.yaml: добавлена TLS-секция +cert-manager.io/cluster-issuer: letsencrypt-prod,ssl-redirect: "true",secretName: iot-kube5s-ru-tlsrouter.go: CORSAllow-Origin:http://→https://iot.kube5s.ruiot-console.html: дефолт MQTT брокераws://→wss://- cert-manager автоматически выпустил сертификат Let's Encrypt (READY: True за ~34 сек)
- Собрали 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 скрыт от пользователя
- ❌ Телеметрия — заглушка, бэкенд не написан
Следующий шаг
Бэкенд телеметрии:
- Postgres StatefulSet в namespace
iot - Tenant provisioning при создании IoTDevice
- INSERT в mqtt-bridge
- REST API чтения
- Вкладка Телеметрия в UI