security: модель угроз обновлена под расширившийся периметр
- заведён раздел «Куда уходит содержимое записи»: к Object Storage, SpeechKit и Telegram добавляются языковая модель за bifrost, канал уведомлений и почта; - целевое разграничение доступа описано четырьмя механизмами вместо белого списка: сессия OIDC, владелец записи, личный токен, признак владельца сервиса; токен — первый секрет, который живёт в базе, а не в конфиге; - исправлено неверное утверждение про логи: имя файла отправителя пишется строкой transcribe.go:107, чинит это задача no-user-filename-in-log; - бессрочное хранение и отсутствие квот записаны в «Что вне модели» как следствие решения паспорта, а не как недосмотр.
This commit is contained in:
+108
-7
@@ -6,9 +6,16 @@
|
||||
аутентификации не делает ни прокси, ни само приложение.** Находки строятся против
|
||||
этого — сегодняшнего — периметра.
|
||||
|
||||
Целевой периметр: те же порты наружу, но вход через OIDC у Authelia, и каждый
|
||||
пользователь видит только свои записи. Он **не** развёрнут; описанное ниже
|
||||
разграничение доступа относится только к Telegram.
|
||||
Целевой периметр: те же порты наружу, но вход через OIDC у Authelia, отдельный
|
||||
вход для программ по личным токенам, два уровня доступа — пользователь видит
|
||||
свои записи, владелец сервиса ещё и страницу расхода. Он **не** развёрнут;
|
||||
описанное ниже разграничение доступа относится только к Telegram.
|
||||
|
||||
**Целевой периметр шире сегодняшнего не только входом.** Содержимое записи
|
||||
начинает уходить на три новые стороны — языковой модели, в канал уведомлений и
|
||||
на почту. Записи и тексты хранятся бессрочно: решение паспорта от 2026-08-11
|
||||
сделало сервис архивом. Оба сдвига описаны ниже разделами «Куда
|
||||
уходит содержимое записи» и «Что вне модели».
|
||||
|
||||
Отсюда главное следствие, из которого читается всё остальное: **`POST /api/audio`
|
||||
доступен кому угодно из интернета**. Отправитель не назван, не ограничен по числу
|
||||
@@ -27,6 +34,40 @@
|
||||
| Содержимое аудио | Файл, скармливаемый `ffmpeg` и `ffprobe` | Отправитель по любому из каналов |
|
||||
| Текст расшифровки | Поток gRPC от SpeechKit | Yandex, а через него — содержимое записи |
|
||||
|
||||
Что добавится вместе с целевым периметром — каждый вход появляется своей
|
||||
задачей, и до неё его нет:
|
||||
|
||||
| Вход | Канал | Кто может слать | Чья задача |
|
||||
| --- | --- | --- | --- |
|
||||
| Токен доступа | Заголовок запроса к `/api/` | Любой из интернета | `api-tokens` |
|
||||
| Данные учётной записи: идентификатор, почта, группы | Ответ Authelia по OIDC | Провайдер, а через него — то, что записано в учётной записи | `oidc-login` |
|
||||
| Заголовок, темы, пересказ | Ответ языковой модели | Внешняя модель, а через неё — содержимое записи | `llm-insights-adapter` |
|
||||
| Вычитанный текст | Ответ той же модели | То же | `literary-text-level` |
|
||||
| Настройки пользователя | Эндпоинт записи своих настроек | Вошедший пользователь | `settings-screen` |
|
||||
| Хеш-сумма файла | Поле запроса приёма | Отправитель — и она же решает, отдать ли прежнюю запись | `dedup-by-content-hash` |
|
||||
|
||||
Ответ языковой модели опаснее прочего в этом списке: он приходит текстом, идёт
|
||||
в заголовок записи и оттуда на экран — то есть внешний сервис пишет то, что
|
||||
увидит человек.
|
||||
|
||||
## Куда уходит содержимое записи
|
||||
|
||||
Сегодня запись и её текст покидают наш сервер тремя путями: файл уезжает в
|
||||
Yandex Object Storage, оттуда его читает SpeechKit, а текст возвращается в
|
||||
Telegram отправителю.
|
||||
|
||||
Целевой периметр добавляет три пути, каждый — своей задачей:
|
||||
|
||||
| Куда | Что уходит | Чья задача |
|
||||
| --- | --- | --- |
|
||||
| Языковая модель за шлюзом bifrost | Текст расшифровки целиком | `llm-insights-adapter`, затем `literary-text-level` |
|
||||
| Канал уведомлений (ntfy через apprise) | Готовый текст либо причина отказа | `ntfy-delivery` |
|
||||
| Почтовый сервер | Готовый текст либо причина отказа, на адрес из учётной записи | `email-notification` |
|
||||
|
||||
Каждая из названных задач обязана оставить строку в этом разделе — там это
|
||||
записано их «Затрагивает». Отказ любой из трёх сторон задачу не роняет: текст
|
||||
остаётся в приложении.
|
||||
|
||||
## Из чего строятся пути и ключи
|
||||
|
||||
Раскладка файлов на диске, состав пути к файлу и ключа объекта, имя каталога.
|
||||
@@ -44,6 +85,17 @@
|
||||
- **Идентификатор задачи** — UUID v4. Он же единственное, что защищает
|
||||
`GET /api/status/:id`.
|
||||
|
||||
Целевой периметр добавляет сюда три вещи, и все три — от новых задач:
|
||||
|
||||
- **Хеш-сумма содержимого** (`dedup-by-content-hash`) становится ключом поиска
|
||||
прежней записи. Ищется она **в пределах одного пользователя**: глобальный
|
||||
поиск отдавал бы чужую расшифровку тому, кто угадал или добыл тот же файл, и
|
||||
заодно сообщал бы, что запись у кого-то уже есть.
|
||||
- **Файлы фрагментов** (`long-audio-chunking`) ложатся рядом с исходным в тот же
|
||||
плоский каталог — раскладка `data/files` меняется, и это необратимо.
|
||||
- **Имя отправляемого документа** (`long-text-delivery`) собирается из
|
||||
идентификатора задачи: имя, данное пользователем, в него не попадает.
|
||||
|
||||
## Что разграничивает доступ
|
||||
|
||||
- **Telegram** — белый список `[server] users_while_list`. Сверяется со строкой
|
||||
@@ -57,6 +109,23 @@
|
||||
Владения записью в модели данных нет: у задачи нет пользователя. Пока API
|
||||
анонимен, знание UUID задачи и есть право её читать.
|
||||
|
||||
Целевой периметр заводит четыре механизма вместо одного белого списка:
|
||||
|
||||
| Механизм | Что даёт | Чья задача |
|
||||
| --- | --- | --- |
|
||||
| Сессия OIDC у Authelia | Право открыть приложение и его эндпоинты | `oidc-login` |
|
||||
| Владелец у задачи и файла | Чужая запись по её идентификатору отвечает «не найдено» | `record-ownership` |
|
||||
| Личный токен | Права своего владельца программе, без браузерной сессии | `api-tokens` |
|
||||
| Признак владельца сервиса | Страницу расхода и сводку по всем пользователям | `admin-stats-screen` |
|
||||
|
||||
Белый список Telegram при этом перестаёт быть отдельным механизмом: право
|
||||
писать боту выводится из учётной записи (`telegram-account-link`).
|
||||
|
||||
Признак владельца сервиса — **второй уровень доступа**, которого в сегодняшней
|
||||
модели нет вовсе: до него всё разграничение сводилось к «свой или чужой».
|
||||
Откуда он берётся — из группы OIDC или из конфигурации — не решено
|
||||
(`admin-stats-screen`).
|
||||
|
||||
## Что чувствительнее чего
|
||||
|
||||
1. **Содержимое записей и расшифровок.** Голосовые сообщения — личная переписка;
|
||||
@@ -69,9 +138,31 @@
|
||||
Всё перечисленное лежит в `config.toml`. Файл в `.gitignore`, на сервер его
|
||||
кладёт Ansible; `gitleaks` на pre-commit смотрит только индекс коммита.
|
||||
|
||||
Тексты расшифровок и имена файлов в логи не пишутся — логируется длина текста и
|
||||
идентификаторы. Токен бота попадает в URL скачивания файла (`file.Link(token)`),
|
||||
и этот URL нигде не логируется.
|
||||
Целевой периметр добавляет к списку пять записей, и первая из них — новый вид
|
||||
секрета, которого сегодня в проекте нет вовсе:
|
||||
|
||||
1. **Токены пользователей** (`api-tokens`). Токен даёт права своего владельца
|
||||
целиком. Срока жизни у него нет. В базе лежит только отпечаток, полное
|
||||
значение показывается один раз при выпуске. Это первый секрет, который
|
||||
хранится **в базе**, а не в конфигурации.
|
||||
2. **Ключ языковой модели** и адрес шлюза bifrost (`llm-insights-adapter`).
|
||||
Утечка оплачивается деньгами.
|
||||
3. **Пароль почтового сервера** (`email-notification`).
|
||||
4. **Адрес почты пользователя** — приходит от Authelia и хранится у нас
|
||||
(`oidc-login`, `email-notification`).
|
||||
5. **Статистика потребления** (`usage-accounting`). Текста записей не содержит,
|
||||
но говорит, кто и когда пользовался сервисом и сколько; страница расхода
|
||||
открыта только владельцу.
|
||||
|
||||
Тексты расшифровок в логи не пишутся — логируется длина текста и
|
||||
идентификаторы. **Имя файла, данное отправителем, пишется**: строка
|
||||
`internal/service/transcribe.go:107` кладёт `file_name` на общем шаге заведения
|
||||
задачи, то есть для обоих входов. Это нарушение инварианта приватности из
|
||||
`CLAUDE.md`, оно объявлено критическим, и чинит его задача
|
||||
`no-user-filename-in-log` — первая строка очереди.
|
||||
|
||||
Токен бота попадает в URL скачивания файла (`file.Link(token)`), и этот URL
|
||||
нигде не логируется.
|
||||
|
||||
## Что вне модели
|
||||
|
||||
@@ -85,7 +176,17 @@
|
||||
SpeechKit не рассматривается.
|
||||
- **Стойкость к целенаправленной нагрузке.** Ограничения по числу запросов и по
|
||||
размеру файла нет, и защищаться от исчерпания диска мы сейчас не пытаемся.
|
||||
- **Исчерпание диска приглашёнными.** Записи и тексты хранятся бессрочно
|
||||
(паспорт, 2026-08-11), шестичасовая запись весит гигабайты, а квот нет и не
|
||||
будет: решено считать расход и показывать его владельцу, а не отказывать
|
||||
(цель `usage-stats`). Перебравшего останавливает разговор или отзыв доступа в
|
||||
Authelia. Рост каталога `data/files` при этом ничем не наблюдается —
|
||||
открытый вопрос `architecture.md`.
|
||||
- **Перерасход денег на внешних сервисах.** Распознавание и языковая модель
|
||||
оплачиваются по факту; потолка на пользователя нет по тому же решению.
|
||||
- **Стойкость `ffmpeg` к вредоносному входу.** Разбор чужого формата отдан
|
||||
внешней программе, своей песочницы вокруг неё нет.
|
||||
- **Удаление данных по требованию.** Ни файлы, ни расшифровки не удаляются
|
||||
вовсе; забвение не реализовано и в задачах не стоит.
|
||||
вовсе, и удаление по требованию в задачах не стоит. С 2026-08-11 это уже не
|
||||
недосмотр, а следствие решения хранить бессрочно — но требование «убери мою
|
||||
запись» им не закрыто, и задачи под него нет.
|
||||
|
||||
Reference in New Issue
Block a user