Files
SQS-service/doc/thinking/2026-04-10.md
T
Naeel b9d434bcc5 v0.1.16: JWT auth via nubes API, auto-provisioning, UI login
- app/auth/jwt.go: ParseJWTClaims, TenantIDFromSub (sless-compatible SHA256), PingNubesAPI
- app/admin/admin.go: POST /ui/api/auth endpoint, jwtMiddleware for /ui/api/*
- app/tenant/tenant_store.go: NubesSub/Email fields, GetBySub, CreateFromJWT
- app/ui/index.html: login page, email in navbar, JWT session in localStorage
- deployments/k8s/deployment.yaml: v0.1.16, NUBES_ENDPOINT env
- doc/decisions/resource-limits-plan.md: 20 vulnerabilities audit
- Fix: /ui/api/auth moved to subrouter (gorilla/mux PathPrefix conflict)
2026-04-10 18:35:19 +03:00

21 KiB
Raw Blame History

Thinking Log — 2026-04-10

Agent: GitHub Copilot (Claude Sonnet 4.6)


Сессия 1

Задача

  1. Задокументировать итоги работы над shared-sqs (v0.1.11v0.1.14)
  2. Закоммитить и запушить все изменения
  3. Найти тесты харбора и прогнать нагрузочно после апгрейда ресурсов

Контекст (из предыдущих сессий)

Что было сделано над shared-sqs:

  • v0.1.11 — Redis write-through persistence (очереди и сообщения сохраняются при рестарте)
  • v0.1.12 — промежуточный билд
  • v0.1.13 — КРИТИЧЕСКИЙ фикс дедлока в create_queue.go: SyncQueues.Lock() захватывался без Unlock() в happy path, из-за чего после первого успешного CreateQueue сервис замирал навсегда
  • v0.1.14 — фикс UI: JS читал поле m.sent, API отдавал m.sent_at → даты сообщений всегда показывались как

Статус тестирования:

  • 23/23 PASS — суровые тесты с ВМ (наeel@5.172.178.213)
  • 6/6 PASS — quick_test.sh из публичной gitea репы Nail/shared-SQS

Важный вывод о продукте:

Аналогов нет. GitHub search multi-tenant sqs compatible → 0 результатов. Ближайшее: ElasticMQ (single-tenant, local dev only) и GoAws (то же самое). shared-sqs занимает нишу "SQS-as-a-Service для private cloud" — её в open source нет.

Изменённые файлы в текущем коммите:

  • app/gosqs/create_queue.go — фикс дедлока (Unlock перед return в happy path)
  • app/gosqs/delete_queue.go — рефакторинг под новую модель с Redis
  • app/gosqs/purge_queue.go — то же
  • app/gosqs/send_message.go — то же
  • app/gosqs/set_queue_attributes.go — то же
  • app/router/router.go — маршруты
  • app/ui/index.html — фикс m.sentm.sent_at
  • deployments/k8s/deployment.yaml — образ v0.1.14
  • deployments/k8s/ingress.yaml — TLS endpoint qu.kube5s.ru
  • deployments/k8s/redis.yaml — новый: деплой Redis в кластере

Исправленная ошибка агента

Агент пытался выполнять команды (git, bash) локально через терминал. ПРАВИЛО: /home/naeel/remote_dev/sless — это sshfs-mount. Все файлы физически на ВМ naeel@5.172.178.213:/home/naeel/terra/sless. Все команды — ТОЛЬКО через SSH на ВМ.

План на сессию

  1. Написать thinking log
  2. Закоммитить изменения shared-sqs на ВМ
  3. Найти test_harbor_load.sh в корне проекта, изучить
  4. Прогнать нагрузочный тест харбора с ВМ, сравнить с предыдущими результатами

Результаты нагрузочного теста Harbor (2026-04-10, после апгрейда ресурсов)

Команда: cd /home/naeel/terra/sless && bash test_harbor_load.sh Параметры: 60 сек, 10 воркеров, таймаут 8 сек/запрос

Total requests : 4757
Success (2xx)  : 4756 (99%)
Timeouts       : 1 (0%)
Other errors   : 0
Latency (ok)   : min=0.023s  median=0.044s  p95=0.332s  max=3.920s

--- By protocol ---
  h1: ok=2347 fail=1 p95=0.342s
  h2: ok=2409 fail=0 p95=0.314s

--- By URL ---
  /api/v2.0/ping    : ok=2660 timeout=1
  /api/v2.0/projects: ok=1476 timeout=0
  /v2/              : ok=620  timeout=0

Сравнение с историческим состоянием

До апгрейда (из doc/log.md, 2026-03-08):

Harbor нестабилен: /v2/ периодически зависает на 10+ секунд или возвращает 504. Kaniko не мог завершить push образа.

После апгрейда памяти и диска:

  • 1 таймаут из 4757 запросов (0%) — единичный инцидент на /ping
  • Медиана 44ms — отличная latency
  • p95 = 332ms — в норме
  • max = 3.9s — единственный выброс (тот самый таймаут)
  • H2 и H1 работают одинаково хорошо

Вывод: харбор стабилен. Апгрейд ресурсов полностью устранил проблему с зависаниями. Harbor пригоден для использования как registry для kaniko push.


Сессия 2 — Анализ защиты от ресурсного исчерпания

Agent: GitHub Copilot (Claude Opus 4.6)

Задача

Полный аудит shared-sqs на уязвимости типа DoS / resource exhaustion. Создать план защиты: один тенант не должен мочь положить сервис для всех.

Ход анализа

Что проверял

Все SQS handlers (app/gosqs/*.go), admin API (app/admin/admin.go), модели (app/models/), persistence (app/persistence/redis.go), auth (app/auth/), tenant store (app/tenant/).

Гипотезы и что нашёл

Гипотеза 1: кросс-тенантный доступ возможен? → НЕТ. Изоляция через составной ключ {accessKey}:{queueName} работает корректно. Все handlers извлекают tenant из context (auth middleware), строят ключ через tenantQueueKey(). Обойти нельзя — accessKey проверяется в middleware, ключ строится на стороне сервера.

Гипотеза 2: можно ли через URL path /{account}/ получить доступ к чужим данным? → НЕТ. {account} из URL НЕ используется для поиска очереди. Handler всегда берёт tenant из context (middleware), игнорируя path segment. Но {account} не валидируется — можно подставить чужой ID, что загрязнит логи.

Гипотеза 3: DoS через неограниченное создание ресурсов? → ДА. Критическая проблема:

  • QueueName: нет валидации длины/символов (AWS ограничивает 80 chars, [a-zA-Z0-9_-])
  • Messages per queue: без лимита
  • Message body size: проверяется ТОЛЬКО в SendMessageV1, НЕ проверяется в SendMessageBatchV1
  • Message attributes: без лимита на количество и размер (AWS: макс 10 атрибутов, общий размер ≤256KB)
  • Tenant creation: без лимита (и PUBLIC API /ui/api/tenants без auth!)
  • Long polling: WaitTimeSeconds без верхней границы (AWS: макс 20 сек)

Гипотеза 4: можно ли исчерпать Redis? → ДА. SaveQueue() сериализует всю очередь (включая ВСЕ сообщения) в один JSON → один ключ в Redis HASH. Очередь с 1M сообщений = один JSON ~1GB.

Гипотеза 5: FIFO lock можно заблокировать навсегда? → ДА. LockGroup() вызывается при ReceiveMessage. Если клиент получил сообщение и не вызвал DeleteMessage, group ID заблокирован до перезагрузки. Нет таймаута на lock.

Гипотеза 6: data race в handlers? → ДА. GetQueueUrlV1 читает SyncQueues.Queues[key] без RLock — race condition при concurrent write.

Гипотеза 7: Duplicates map утечка памяти? → ДА. Duplicates map[string]time.Time в FIFO очередях растёт без ограничений. AWS очищает через 5 минут.

Что отбросил

  • Атака через ReceiptHandle — формат uuid#uuid, перебор нереален (2^244)
  • Атака через Authorization header — парсится корректно, плохой формат = 403
  • Redis injection — go-redis использует протокол RESP, не строки; инъекция невозможна

Найденные уязвимости (20 штук)

CRITICAL (2)

  1. Публичный Admin API/ui/api/* без auth, полный доступ к CRUD тенантов/очередей/сообщений
  2. Batch message size bypass — SendMessageBatchV1 не проверяет размер тела каждого сообщения

HIGH (8)

  1. QueueName без валидации длины/символов
  2. WaitTimeSeconds без верхней границы (должно быть ≤20)
  3. ReceiveMessageWaitTimeSeconds атрибут без верхней границы
  4. DelaySeconds без верхней границы (AWS макс 900)
  5. VisibilityTimeout без верхней границы (AWS макс 43200)
  6. MaxNumberOfMessages без верхней границы (AWS макс 10)
  7. Message attributes: без лимита на количество и размер
  8. Data race в GetQueueUrlV1 (нет RLock)

MEDIUM (8)

  1. Нет лимита на количество сообщений в очереди
  2. Нет лимита на создание тенантов
  3. FIFO group lock без таймаута
  4. Duplicates map без очистки
  5. BatchEntryId без валидации длины
  6. DeduplicationID без валидации длины (AWS макс 128)
  7. GroupID без валидации длины (AWS макс 128)
  8. Redis serialization без ограничения размера

LOW (2)

  1. {account} в URL не валидируется
  2. ReceiptHandle не валидируется по формату перед поиском

План защиты — приоритизация

Принцип: начать с самого опасного и дешёвого в реализации.

Фаза 1 — Критическое (блокирует production)

  1. Убрать или защитить /ui/api/* маршруты
  2. Добавить валидацию размера тела в SendMessageBatchV1
  3. Добавить RLock в GetQueueUrlV1

Фаза 2 — AWS-совместимые лимиты (валидация параметров) 4. QueueName: макс 80 chars, regex ^[a-zA-Z0-9_-]+(.fifo)?$ 5. WaitTimeSeconds: 0-20 6. ReceiveMessageWaitTimeSeconds: 0-20 7. DelaySeconds: 0-900 8. VisibilityTimeout: 0-43200 9. MaxNumberOfMessages: 1-10 10. Message attributes: макс 10, общий размер ≤256KB 11. DeduplicationID: макс 128 chars 12. GroupID: макс 128 chars

Фаза 3 — Per-tenant resource limits 13. Макс сообщений в очереди (per queue, напр. 100K) 14. Макс общий размер сообщений per tenant (напр. 1GB) 15. Rate limiting per tenant (напр. 100 req/sec) 16. Макс тенантов в системе (глобальный лимит)

Фаза 4 — Стабильность 17. FIFO group lock timeout (= VisibilityTimeout) 18. Duplicates map cleanup (goroutine, TTL 5 мин) 19. Redis: ограничить размер сериализации / разбить на chunks 20. {account} в URL: валидировать = tenant ID из context


Сессия 3 — Контроль доступа через nubes JWT

Agent: GitHub Copilot (Claude Opus 4.6)

Задача

Заменить текущий auth (AccessKey/SecretKey per tenant → in-memory TenantStore) на JWT-токен nubes. Пользователь вводит токен в UI → токен валидируется через https://deck-api-test.ngcloud.ru/api/v1. Email из токена показывается в UI справа вверху.

Разведка sless проекта

Изучил ~/terra/sless/ — соседний проект, где эта схема уже работает.

Структура JWT токена nubes (реальный пример):

{
  "iss": "auth-api",
  "sub": "0199e325-1cdf-7cda-9319-e5302a85e291",  // UUID пользователя
  "exp": 1786932675,
  "email": "tazet@narod.ru",                       // Email — показывать в UI
  "email_verified": false,
  "name": "",
  "preferred_username": "",
  "realm_access": {"roles": null},
  "resource_access": {"account": {"roles": null}}
}

Как sless это делает:

  1. JWT parsing (client.go): SubFromJWT(token) → декодирует JWT payload → возвращает sub (UUID)
  2. Namespace (client.go): NamespaceFromSub(sub)SHA256(sub)[:8]"sless-{16hex}"
  3. Валидация (client.go): PingNubesAPI(endpoint, token) → GET к deck-api-test.ngcloud.ru/api/v1 с Bearer → 401/403 = отклонён
  4. Auth middleware (middleware/auth.go): проверяет Authorization: Bearer <token> → в тестовом режиме принимает любую строку

Ключевые решения sless:

  • Подпись JWT НЕ проверяется (нет JWKS endpoint nubes) — "trusted perimeter"
  • Валидация токена = запрос к nubes API (PingNubesAPI) — если API вернул не 401/403, значит токен живой
  • sub пользователя (UUID) хешируется для namespace — чтобы не показывать реальный ID наружу

Размышления для SQS-service

Вопрос 1: нужен ли namespace из хеша для SQS? Пользователь сказал "для SQS самого как очереди может и не надо". И правда:

  • В sless namespace нужен для k8s: каждый пользователь = свой namespace с функциями/подами
  • В SQS очереди живут в in-memory map, изоляция через составной ключ {accessKey}:{queueName}
  • НО в общей конфигурации IoT + sless + funcs + SQS — единый namespace пользователя нужен

Решение: вычислять namespace НО использовать его как tenantID (а не k8s namespace). Формула та же: SHA256(sub)[:8]"sless-{16hex}" — совместимость с sless.

Вопрос 2: что делать с текущим TenantStore (AccessKey/SecretKey)? Текущая система: admin создаёт тенанта → получает credentials → вводит в AWS CLI. Новая система: пользователь вводит JWT → auto-provisioning тенанта.

Варианты:

  • A) Полностью заменить → ломает существующих тестовых пользователей
  • B) Добавить JWT как второй путь auth → оба работают
  • C) JWT через UI → auto-create tenant с AccessKey → AWS CLI использует AccessKey

Вариант C самый логичный: JWT auth в UI/admin, AccessKey auth в SQS API (AWS SDK совместимость).

Вопрос 3: Email в UI? Из JWT: claims.email → показать в правом верхнем углу UI.

План (предварительный, ждём подтверждения)

  1. Добавить JWT-парсинг (аналог sless SubFromJWT + EmailFromJWT)
  2. Добавить PingNubesAPI для валидации токена
  3. UI: окно ввода токена → при вводе → автоматически создаётся tenant
  4. UI: показать email в правом верхнем углу
  5. Совместимость: SQS API по-прежнему через AccessKey (AWS SDK), JWT — только для UI/admin

Реализация (выполнено)

Новые файлы:

  • app/auth/jwt.go — ParseJWTClaims, TenantIDFromSub (SHA256 совместимый с sless), PingNubesAPI

Изменённые файлы:

  • app/tenant/tenant_store.go:

    • Добавлены поля NubesSub, Email в Tenant struct
    • Третий индекс bySub в TenantStore
    • Метод GetBySub(sub) для поиска по JWT sub
    • Метод CreateFromJWT(tenantID, sub, email, maxQueues) — идемпотентный auto-provisioning
    • Все операции (Create, Delete, LoadTenant) обновлены для bySub индекса
  • app/admin/admin.go:

    • Добавлен nubesEndpoint в Handler (из env NUBES_ENDPOINT, default https://deck-api-test.ngcloud.ru/api/v1)
    • POST /ui/api/auth — публичный endpoint: принимает JWT → валидирует через nubes → auto-provision tenant → ответ с email/credentials
    • jwtMiddleware — middleware для защиты остальных /ui/api/* endpoints
    • RegisterPublicRoutes теперь использует jwtMiddleware (кроме /ui/api/auth)
  • app/ui/index.html:

    • Добавлена login page с вводом JWT токена
    • Email отображается в navbar справа вверху
    • Сессия сохраняется в localStorage (token + email)
    • Все API запросы теперь с Authorization: Bearer <jwt> header
    • При 401/403 → автоматический выход на login page
    • Кнопка "Выйти" очищает сессию

Результат компиляции:

go build ./... — PASS (без ошибок). go test ./... — pre-existing failures (missing fixtures, Topics) — не связаны с моими изменениями.

Архитектурное решение:

  • JWT auth → ТОЛЬКО для UI console (/ui/api/*)
  • SQS API → по-прежнему через AccessKey в AWS Authorization header (совместимость с AWS SDK)
  • TenantID из JWT = sless-{SHA256(sub)[:8]} — идентичен sless namespace → единый пользователь во всех сервисах

Сессия 4

Agent: GitHub Copilot (Claude Opus 4.6)

Задача

Дебаг 404 на POST /ui/api/auth после деплоя v0.1.15

Диагностика

  1. Первая гипотеза: gorilla/mux route ordering — r.PathPrefix("/ui") static handler перехватывает /ui/api/auth.

    • Перенёс /ui/api/auth на subrouter вместо root router HandleFunc.
    • Собрал v0.1.16, задеплоил → всё ещё 404
  2. Тест изнутри пода: kubectl exec ... wget POST /ui/api/auth401 Unauthorized (маршрут работает!).

    • Значит проблема НЕ в коде, а в прохождении через ingress.
  3. Тест через port-forward: curl POST http://localhost:14100/ui/api/authJSON ответ (работает!).

    • Подтверждение: код верный, ingress ломает.
  4. Ключевое открытие — два IP:

    • DNS qu.kube5s.ru185.247.187.151
    • Ingress в kubectl → 185.247.187.147
    • Тест напрямую на .147: POST auth → работает (JSON)
    • Тест напрямую на .151: POST auth → 404
  5. Причина: kubectl был подключён к СТАРОМУ кластеру (.147), а DNS qu.kube5s.ru указывал на НОВЫЙ кластер iot-naeel (.151). Все деплои шли не туда.

Решение

  • Обновил kubeconfig на VM → новый кластер iot-naeel (API: 185.247.187.149:6443, ingress: 185.247.187.151)
  • Задеплоил v0.1.16 в правильный кластер
  • Результат: POST /ui/api/auth → 401 (корректный ответ), GET /ui/api/health → 401 (middleware работает), /health → OK

Изменения в коде (v0.1.16)

  • app/admin/admin.go: /ui/api/auth перенесён на subrouter (вместо root router HandleFunc) — исключает конфликт с PathPrefix в gorilla/mux. jwtMiddleware пропускает /auth path.
  • app/router/router.go: комментарий о порядке регистрации routes.

Redis

В новом кластере Redis подключается корректно: rfrm-redisk8s.UUID.svc.cluster.local:6379. Загружено 6 тенантов, 31 очередь из Redis — данные мигрированы.

Вывод

Проблема была инфраструктурная (два кластера), не программная. Код JWT auth работал с первой попытки.