- 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)
21 KiB
Thinking Log — 2026-04-10
Agent: GitHub Copilot (Claude Sonnet 4.6)
Сессия 1
Задача
- Задокументировать итоги работы над shared-sqs (v0.1.11–v0.1.14)
- Закоммитить и запушить все изменения
- Найти тесты харбора и прогнать нагрузочно после апгрейда ресурсов
Контекст (из предыдущих сессий)
Что было сделано над 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— рефакторинг под новую модель с Redisapp/gosqs/purge_queue.go— то жеapp/gosqs/send_message.go— то жеapp/gosqs/set_queue_attributes.go— то жеapp/router/router.go— маршрутыapp/ui/index.html— фиксm.sent→m.sent_atdeployments/k8s/deployment.yaml— образ v0.1.14deployments/k8s/ingress.yaml— TLS endpoint qu.kube5s.rudeployments/k8s/redis.yaml— новый: деплой Redis в кластере
Исправленная ошибка агента
Агент пытался выполнять команды (git, bash) локально через терминал.
ПРАВИЛО: /home/naeel/remote_dev/sless — это sshfs-mount.
Все файлы физически на ВМ naeel@5.172.178.213:/home/naeel/terra/sless.
Все команды — ТОЛЬКО через SSH на ВМ.
План на сессию
- ✅ Написать thinking log
- Закоммитить изменения shared-sqs на ВМ
- Найти
test_harbor_load.shв корне проекта, изучить - Прогнать нагрузочный тест харбора с ВМ, сравнить с предыдущими результатами
Результаты нагрузочного теста 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)
- Публичный Admin API —
/ui/api/*без auth, полный доступ к CRUD тенантов/очередей/сообщений - Batch message size bypass — SendMessageBatchV1 не проверяет размер тела каждого сообщения
HIGH (8)
- QueueName без валидации длины/символов
- WaitTimeSeconds без верхней границы (должно быть ≤20)
- ReceiveMessageWaitTimeSeconds атрибут без верхней границы
- DelaySeconds без верхней границы (AWS макс 900)
- VisibilityTimeout без верхней границы (AWS макс 43200)
- MaxNumberOfMessages без верхней границы (AWS макс 10)
- Message attributes: без лимита на количество и размер
- Data race в GetQueueUrlV1 (нет RLock)
MEDIUM (8)
- Нет лимита на количество сообщений в очереди
- Нет лимита на создание тенантов
- FIFO group lock без таймаута
- Duplicates map без очистки
- BatchEntryId без валидации длины
- DeduplicationID без валидации длины (AWS макс 128)
- GroupID без валидации длины (AWS макс 128)
- Redis serialization без ограничения размера
LOW (2)
{account}в URL не валидируется- ReceiptHandle не валидируется по формату перед поиском
План защиты — приоритизация
Принцип: начать с самого опасного и дешёвого в реализации.
Фаза 1 — Критическое (блокирует production)
- Убрать или защитить
/ui/api/*маршруты - Добавить валидацию размера тела в SendMessageBatchV1
- Добавить 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 это делает:
- JWT parsing (
client.go):SubFromJWT(token)→ декодирует JWT payload → возвращаетsub(UUID) - Namespace (
client.go):NamespaceFromSub(sub)→SHA256(sub)[:8]→"sless-{16hex}" - Валидация (
client.go):PingNubesAPI(endpoint, token)→ GET кdeck-api-test.ngcloud.ru/api/v1с Bearer → 401/403 = отклонён - 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.
План (предварительный, ждём подтверждения)
- Добавить JWT-парсинг (аналог sless SubFromJWT + EmailFromJWT)
- Добавить PingNubesAPI для валидации токена
- UI: окно ввода токена → при вводе → автоматически создаётся tenant
- UI: показать email в правом верхнем углу
- Совместимость: 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 (из envNUBES_ENDPOINT, defaulthttps://deck-api-test.ngcloud.ru/api/v1) POST /ui/api/auth— публичный endpoint: принимает JWT → валидирует через nubes → auto-provision tenant → ответ с email/credentialsjwtMiddleware— 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
Диагностика
-
Первая гипотеза: gorilla/mux route ordering —
r.PathPrefix("/ui")static handler перехватывает/ui/api/auth.- Перенёс
/ui/api/authна subrouter вместо root router HandleFunc. - Собрал v0.1.16, задеплоил → всё ещё 404
- Перенёс
-
Тест изнутри пода:
kubectl exec ... wget POST /ui/api/auth→ 401 Unauthorized (маршрут работает!).- Значит проблема НЕ в коде, а в прохождении через ingress.
-
Тест через port-forward:
curl POST http://localhost:14100/ui/api/auth→ JSON ответ (работает!).- Подтверждение: код верный, ingress ломает.
-
Ключевое открытие — два IP:
- DNS
qu.kube5s.ru→185.247.187.151 - Ingress в kubectl →
185.247.187.147 - Тест напрямую на
.147: POST auth → работает (JSON) - Тест напрямую на
.151: POST auth → 404
- DNS
-
Причина: kubectl был подключён к СТАРОМУ кластеру (
.147), а DNSqu.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 пропускает/authpath.app/router/router.go: комментарий о порядке регистрации routes.
Redis
В новом кластере Redis подключается корректно: rfrm-redisk8s.UUID.svc.cluster.local:6379.
Загружено 6 тенантов, 31 очередь из Redis — данные мигрированы.
Вывод
Проблема была инфраструктурная (два кластера), не программная. Код JWT auth работал с первой попытки.