399 lines
24 KiB
Markdown
399 lines
24 KiB
Markdown
# Thinking Log — 2026-04-10
|
||
# Agent: GitHub Copilot (Claude Sonnet 4.6)
|
||
|
||
---
|
||
|
||
## Сессия 1
|
||
|
||
### Задача
|
||
1. Задокументировать итоги работы над shared-sqs (v0.1.11–v0.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.sent` → `m.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)
|
||
3. QueueName без валидации длины/символов
|
||
4. WaitTimeSeconds без верхней границы (должно быть ≤20)
|
||
5. ReceiveMessageWaitTimeSeconds атрибут без верхней границы
|
||
6. DelaySeconds без верхней границы (AWS макс 900)
|
||
7. VisibilityTimeout без верхней границы (AWS макс 43200)
|
||
8. MaxNumberOfMessages без верхней границы (AWS макс 10)
|
||
9. Message attributes: без лимита на количество и размер
|
||
10. Data race в GetQueueUrlV1 (нет RLock)
|
||
|
||
#### MEDIUM (8)
|
||
11. Нет лимита на количество сообщений в очереди
|
||
12. Нет лимита на создание тенантов
|
||
13. FIFO group lock без таймаута
|
||
14. Duplicates map без очистки
|
||
15. BatchEntryId без валидации длины
|
||
16. DeduplicationID без валидации длины (AWS макс 128)
|
||
17. GroupID без валидации длины (AWS макс 128)
|
||
18. Redis serialization без ограничения размера
|
||
|
||
#### LOW (2)
|
||
19. `{account}` в URL не валидируется
|
||
20. 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 (реальный пример):
|
||
```json
|
||
{
|
||
"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`.
|
||
|
||
---
|
||
|
||
## Сессия 5
|
||
# Agent: GitHub Copilot (GPT-5.3-Codex)
|
||
|
||
### Задача
|
||
1. Закрыть оставшиеся security/perf хвосты
|
||
2. Прогнать compatibility тесты для выявления новых расхождений
|
||
3. Подготовить документированный статус для managed SQS roadmap
|
||
|
||
### План перед действиями
|
||
- Сначала закрыть критичные и высокие уязвимости с минимальными точечными правками
|
||
- Затем закрыть medium риски, влияющие на managed эксплуатацию
|
||
- После этого прогнать compatibility scripts against production endpoint
|
||
- По итогам разделить реальные дефекты и устаревшие ожидания тестов
|
||
|
||
### Что сделано
|
||
- Исправлены security issues в auth, admin, gosqs, tenant, persistence, router
|
||
- Добавлена SigV4 подпись верификация для header и presigned запросов
|
||
- Закрыт IDOR в UI API: доступ только к собственному tenant id
|
||
- Убраны race и persistence рассинхроны в batch/send/delete/admin операциях
|
||
- Оптимизирован long polling в receive handler
|
||
- Изменения закоммичены и запушены в ветку fix/critical-high-security-2026-04-10
|
||
|
||
### Compatibility прогоны
|
||
- tests/shared_sqs_test.sh: PASS=28 FAIL=0
|
||
- tests/quick_test.sh: PASS=12 FAIL=7
|
||
- tests/hardcore_test.sh: PASS=90 FAIL=14
|
||
|
||
### Анализ результатов
|
||
1. Основной SQS поток совместимости (AWS CLI + awscurl + tenant isolation) стабилен
|
||
2. Большинство падений quick/hardcore связано с тем, что старые UI-сценарии идут без JWT
|
||
3. Это не regression сервиса, а рассогласование тестов с текущей security моделью
|
||
4. Часть проверок ожидает strict reject, тогда как текущая логика использует clamp
|
||
|
||
### Выводы
|
||
- Для managed SQS следующий блок работ: привести compatibility suite к актуальному auth контракту
|
||
- Нужны отдельные воркфлоу:
|
||
- SQS compatibility suite (без UI auth assumptions)
|
||
- UI compatibility suite (обязательный login через /ui/api/auth)
|
||
|
||
### Следующие шаги
|
||
1. Обновить tests/quick_test.sh под JWT-aware UI сценарий
|
||
2. Обновить UI-блоки tests/hardcore_test.sh
|
||
3. Добавить стабильный multi-run тест для big payload кейсов
|
||
- Перенёс `/ui/api/auth` на subrouter вместо root router HandleFunc.
|
||
- Собрал v0.1.16, задеплоил → **всё ещё 404**
|
||
|
||
2. **Тест изнутри пода**: `kubectl exec ... wget POST /ui/api/auth` → **401 Unauthorized** (маршрут работает!).
|
||
- Значит проблема НЕ в коде, а в прохождении через ingress.
|
||
|
||
3. **Тест через port-forward**: `curl POST http://localhost:14100/ui/api/auth` → **JSON ответ** (работает!).
|
||
- Подтверждение: код верный, ingress ломает.
|
||
|
||
4. **Ключевое открытие — два IP**:
|
||
- DNS `qu.kube5s.ru` → `185.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 работал с первой попытки.
|