- пришедшего называет заголовок Remote-User от прокси, и верят ему только с адреса из перечня trusted_proxies; своего входа у сервиса не осталось — ни корня /auth, ни кук, ни срока сессии, ни секрета клиента в конфиге и в базе - учётная запись заводится первым обращением: EnsureUser в пакете хранилища, шаг схемы 202608220001 с колонкой provider_login и снятыми правилами users - cmd/oidcstub заменён на cmd/devtools с подкомандой proxy; заодно закрыт унаследованный DL3066 — пользователь образа назван числом
45 KiB
Модель угроз
Периметр
Сервис открыт наружу, но не анонимен: HTTP-порт опубликован в интернет через
обратный прокси, а приём записи, чтение её карточки и текста и файл записи
требуют, чтобы пришедшего назвала Authelia. С 2026-08-22, задачей
trusted-header-login, называет она его заголовком, который ставит обратный
прокси: своего входа у сервиса не осталось — ни адреса к провайдеру, ни
возврата, ни куки, ни выхода. Прежде сервис вёл вход сам (oidc-login
2026-08-12) и потом семь суток верил выданной куке; теперь Authelia судит
каждый запрос, и отзыв доступа действует со следующего.
Без узнавания открыты проба здоровья, метрики и — с 2026-08-15, задачей spa-skeleton —
само приложение: его разметка и её ресурсы, а вместе с ними всякий путь, не
принадлежащий ни одному корню сервиса. Причина внешняя: заголовок ставит прокси,
и человек, которого прокси не назвал, до приложения дошёл бы только мимо него —
а закрытая разметка выглядела бы поломкой сервиса, а не отказом входа. Данных
открытость не касается — всякий адрес под корнем приложения узнанного
по-прежнему требует. Находки строятся против этого — сегодняшнего — периметра.
Состав того, что отдаётся анонимно, задаёт содержимое собранного приложения,
а каталог его лежит в .gitignore и не судится ничем: всё, что окажется там у
собирающего, уезжает в бинарник и раздаётся. Под каталогом ресурсов оно ещё и
отдаётся с годовым сроком хранения и пометкой «неизменяемо» — отозвать выданное
браузеру сервису нечем.
Целевой периметр добавляет к нему отдельный вход для программ по личным токенам
и два уровня доступа — пользователь видит свои записи, владелец сервиса ещё и
страницу расхода. Разграничение по владельцу записи заведено 2026-08-14
задачей record-ownership: и чтение записи, и файл записи сужены владельцем
записи, а чужая отвечает «не найдено». Целевому периметру недостаёт теперь второго уровня
доступа — страницы расхода для владельца сервиса.
Ничьих записей у сервиса больше не бывает: колонка владельца пустого значения не принимает, и держит это схема хранилища. Прежде такие записи заводил вход Telegram — связи чата с учётной записью сервис не вёл, — и 2026-08-14 вход убран вместе с этим исключением.
Целевой периметр шире сегодняшнего не только входом. Содержимое записи начинает уходить на три новые стороны — языковой модели, в канал уведомлений и на почту. Записи и тексты хранятся бессрочно: решение паспорта от 2026-08-11 сделало сервис архивом. Оба сдвига описаны ниже разделами «Куда уходит содержимое записи» и «Что вне модели».
Третий сдвиг — панель администратора. Решением от 2026-08-11
(adr) хранилищем
становится PocketBase, и вместе с ним на том же порту появляется панель по
адресу /_/: доступ ко всем записям, всем файлам и всем пользователям разом.
Порт опубликован в интернет через обратный прокси, а сама PocketBase вход в
панель через Authelia не пускает — у неё свой пароль суперпользователя.
Закрывает панель контур, а не приложение: решением владельца от 2026-08-11
адрес /_/ закрывает Authelia на обратном прокси, пропуская только группу
администраторов. Задачи в беклоге у этого нет — работа принадлежит выкладке, а
она вне модели («Что вне модели», строка про контур).
И этот барьер обходится подменой одного знака. Маршрутизатор сравнивает
сегменты пути после раскодирования, поэтому /%5f/ попадает в ту же группу,
что и /_/, а правило прокси написано на литерал и такой формы не видит.
Проверено прогоном 2026-08-15 ревью задачи spa-skeleton: обе формы отвечают
байт в байт, и весь клиент панели грузится анониму. Вход в приложение при этом
не обходится — /%61pp/me отвечает 401. Дефект старше задачи, которая его
нашла, и сегодня не закрыт: лечение — приведение пути к канонической форме на
стороне сервиса, и глухая проверка тут не годится, потому что сломает скачивание
файлов с пробелами и не-латиницей в имени. Половину пути проверить нечем: правило
прокси живёт в pet-project-server, вне этого репозитория.
Четвёртый сдвиг был — секрет клиента в базе, — и он снят. Задача
oidc-login 2026-08-12 клала адреса провайдера, идентификатор клиента и его
секрет в настройки коллекции пользователей, и чтение файла базы становилось
равносильно чтению секрета. 2026-08-22 секрета не стало вовсе: обменивать код не
на что, и изъятие из инварианта «Секрет не покидает конфиг» снято вместе с ним.
Вместо него — новый и главный: барьер держится на том, что прокси ставит
заголовок сам. Сервис верит Remote-User, пришедшему с адреса из объявленного
перечня, а перечень этот и есть адрес прокси. Прокси, настроенный добавлять
заголовок вместо замены, оставит рядом со своим значением присланное анонимом —
и аноним войдёт под любым именем. Половину этой беды сервис закрывает сам:
запрос с двумя значениями Remote-User не узнаёт никого. Вторую половину
проверить отсюда нечем: правило живёт в files/caddyproxy/Caddyfile.template
репозитория pet-project-server, и требование к нему такое — заголовки
Remote-* прокси обязан перезаписывать, а не пропускать. Выкладку запускает
человек.
То же требование распространяется на X-Forwarded-For, и по другой причине.
С 2026-08-22 сервис называет этот заголовок хранилищу источником адреса
спрашивающего — иначе счётчик ограничителя частоты ключуется адресом пира, а
пир теперь всегда один, и бюджет становится общим на весь сервис. Прокси,
дописывающий X-Forwarded-For к присланному вместо замены, отдаёт ключ счётчика
самому спрашивающему: тот меняет значение и обходит ограничитель. Барьером
узнавания этот заголовок при этом не служит — кто пришёл, решает адрес самого
соединения.
Ширина перечня доверенных адресов — тоже цена, и она принимается сознательно.
Перечень задаёт, чьему Remote-User верить, и всякий, кто дотянулся до сервиса
с такого адреса, называет себя кем угодно. Перечень поэтому обязан покрывать
адрес прокси, а не весь частный диапазон: сеть докера целиком означает «любой
контейнер на хосте», включая чужие. Образец конфига называет узкий пример
именно поэтому.
Пятый сдвиг — логин у провайдера переиспользуем. Ключ учётной записи —
Remote-User, то есть логин человека у Authelia. Логин можно выдать заново
после ухода прежнего владельца, и тогда новый человек при первом же обращении
попадает в существующую запись и получает весь её архив — самое
чувствительное, что у сервиса есть. Сервис этого не различает и различить не
может: неизменяемого признака заголовок не приносит. Не допускать
переиспользования — работа провайдера, и это принятая цена, записанная в
access. Обратная сторона той же цены:
переименование заводит новую запись, а прежняя остаётся с архивом, который
нечем ни слить, ни убрать.
Отсюда главное следствие, из которого читается всё остальное: POST /app/audiorecords требует входа, а размер файла ограничен потолком записи, число
же запросов ограничено только частотой. Вошедший тратит наши деньги на
распознавание столько, сколько захочет: ограничитель частоты под корнем
приложения заведён 2026-08-15 и режет темп, а не общий объём. Квоты по объёму
по-прежнему нет — её заводит per-user-size-quota.
Недоверенный вход
Что приходит извне и каким каналом.
| Вход | Канал | Кто может слать |
|---|---|---|
| Имя пришедшего, имя для показа и почта | Заголовки Remote-User, Remote-Name, Remote-Email |
Обратный прокси — и всякий, кто дотянулся до сервиса с доверенного адреса. Значение принимается: пустое, пробельное, длиннее 255 знаков и с управляющими знаками не узнают никого; два значения одного заголовка не узнают никого тоже. С недоверенного адреса заголовок не действует, и это идёт в журнал предупреждением с адресом пира, но без значения |
| Аудиофайл и его имя | POST /app/audiorecords, multipart-поле audio |
Любой узнанный; неузнанному — 401 до чтения тела. Имя доходит до колонки записи обрезанным по пределу и без управляющих знаков |
| Идентификатор записи | GET /app/audiorecords/{id} и /text |
Любой узнанный; неузнанному — 401, одинаковый для заведённой и незаведённой записи |
| Ключ страницы, размер страницы, состояние отбора | GET /app/audiorecords, параметры запроса |
Любой узнанный; нечитаемый ключ и негодный размер дают 400, а не молчаливую первую страницу |
| Вид текста | GET /app/audiorecords/{id}/text, параметр view |
Любой узнанный; значение вне закрытого перечня даёт 400 |
| Содержимое аудио | Файл, скармливаемый ffmpeg и ffprobe |
Отправитель |
| Текст расшифровки | Поток gRPC от SpeechKit | Yandex, а через него — содержимое записи |
Что добавится вместе с целевым периметром — каждый вход появляется своей задачей, и до неё его нет:
| Вход | Канал | Кто может слать | Чья задача |
|---|---|---|---|
| Токен доступа | Заголовок запроса к /api/ |
Любой из интернета | api-tokens |
| Заголовок, темы, пересказ | Ответ языковой модели | Внешняя модель, а через неё — содержимое записи | llm-insights-adapter |
| Вычитанный текст | Ответ той же модели | То же | literary-text-level |
| Настройки пользователя | Эндпоинт записи своих настроек | Вошедший пользователь | settings-screen |
| Хеш-сумма файла | Поле запроса приёма | Отправитель — и она же решает, отдать ли прежнюю запись | dedup-by-content-hash |
Ответ языковой модели опаснее прочего в этом списке: он приходит текстом, идёт в заголовок записи и оттуда на экран — то есть внешний сервис пишет то, что увидит человек.
Куда уходит содержимое записи
Сегодня запись покидает наш сервер двумя путями: файл уезжает в Yandex Object Storage, оттуда его читает SpeechKit. Третий путь — ответ в Telegram — исчез 2026-08-14 вместе с убранным входом: текст теперь достаётся только своим адресом и в панели владельца.
Целевой периметр добавляет три пути, каждый — своей задачей:
| Куда | Что уходит | Чья задача |
|---|---|---|
| Языковая модель за шлюзом bifrost | Текст расшифровки целиком | llm-insights-adapter, затем literary-text-level |
| Канал уведомлений (ntfy через apprise) | Готовый текст либо причина отказа | ntfy-delivery |
| Почтовый сервер | Готовый текст либо причина отказа, на адрес из учётной записи | email-notification |
Каждая из названных задач обязана оставить строку в этом разделе — там это записано их «Затрагивает». Отказ любой из трёх сторон задачу не роняет: текст остаётся в приложении.
Из чего строятся пути и ключи
Раскладка файлов на диске, состав пути к файлу и ключа объекта, имя каталога. Отсюда возможен выход за пределы каталога хранения — запись файла туда, куда путь не предполагался.
- Путь на диске выбирает хранилище:
data/storage/<коллекция>/<запись>/<имя>. Имя задаёт сервис —<uuid><расширение>, — а умолчание PocketBase, строящее имя из имени отправителя, не применяется: имя отправителя в хранилище не попадает. Расширение берётся из имени отправителя черезfilepath.Extбез проверки списком;filepath.Extрежет по последней точке и не пропускает разделитель каталогов, но это единственное, что стоит между входом и именем файла. - Ключ объекта в Object Storage — то же имя файла, то есть UUID с расширением. Бакет один на все записи, префикса по пользователю нет. С 2026-08-14 копия там файлом записи не считается: она существует лишь потому, что провайдер читает аудио по адресу, и её ключ живёт в строке попытки распознавания.
- Вторая раскладка файла на диске появилась 2026-08-14 вместе с сохранённым
ответом провайдера:
data/storage/<recognitions>/<попытка>/<имя>.payload. Имя задаёт сервис, как и у аудио. Содержимое там — полный текст речи, а не метаданные, поэтому поле помечено защищённым, правило просмотра коллекции оставлено пустым, и ссылка на вложение подпадает под тот же запрет, что и ссылка на аудио: в журнал она не пишется. Проверено прогоном: без сессии, с чужим и со своим токеном файла ссылка отвечает «не найдено». - Ссылка на файл —
/api/files/<коллекция>/<запись>/<имя>. Поле файла помечено защищённым задачейoidc-login2026-08-12: пройти по ссылке теперь можно только с коротким токеном файла, который выдаётся по сессии, и запрос без него получает «не найдено». Сама ссылка отзыва по-прежнему не имеет — токен сужает круг и живёт недолго, но выданное не отзывается. Отсюда запрет остаётся: имя файла в хранилище в журнал не пишется — иначе строка журнала вместе с идентификатором записи собирала бы ссылку целиком и работала бы бессрочно. В журнал идёт расширение своим полем. - Идентификатор записи — 15 знаков, выдаёт хранилище. Он же единственное, что защищает карточку записи и её текст.
- Поверхность самого хранилища. Вместе с переводом наружу выходят
/api/collections/...,/api/logs,/api/backups,/api/settings,/api/cronsи панель/_/. Правила доступа коллекций оставлены пустыми, то есть доступны они только владельцу панели, — и коллекции, заведённые 2026-08-14, тоже: содержимое записи отдаёт собственный адрес сервиса, а не поверхность хранилища. Коды, снятые прогоном, — database.md, «Коллекции», норма — storage, «Наружу хранилище отдаёт только то, что заказано».
Целевой периметр добавляет сюда три вещи, и все три — от новых задач:
- Хеш-сумма содержимого (
dedup-by-content-hash) становится ключом поиска прежней записи. Ищется она в пределах одного пользователя: глобальный поиск отдавал бы чужую расшифровку тому, кто угадал или добыл тот же файл, и заодно сообщал бы, что запись у кого-то уже есть. - Файлы фрагментов (
long-audio-chunking) ложатся рядом с исходным в тот же плоский каталог — раскладка каталога данных меняется, и это необратимо. - Имя отправляемого документа (
long-text-delivery) собирается из идентификатора задачи: имя, данное пользователем, в него не попадает.
Что разграничивает доступ
- HTTP API — заголовок
Remote-User, пришедший с адреса из объявленного перечня доверенных. Адрес берётся у самого соединения, а не из пересылаемого заголовка: пересылаемым распоряжается тот, кто шлёт запрос. Значения, переживающего запрос, сервис не выдаёт вовсе — ни куки, ни токена, — и потому отзыв доступа у Authelia действует со следующего обращения. Предъявленный собственный токен хранилища побеждает заголовок: им работает владелец панели, и подмена его учётной записью пользователя отобрала бы у него панель. Протухший и негодный токен предъявленными не считаются. Область узнавания сужена до корня приложения и адреса выдачи файлового токена: собственная поверхность хранилища под неё не подпадает, иначе узнанный переписал бы себе ключ учётной записи на чужое имя. - Учётная запись — заводится первым обращением с новым логином и находится
по нему же дальше. Ключ — колонка
provider_login, уникальная; править её снаружи нельзя, все пять правил доступа коллекции пользователей закрыты шагом схемы202608220001. - Файл записи — короткий токен файла, который берёт узнанный. Поле файла
помечено защищённым, правило просмотра коллекции пускает только владельца
файла, и ссылка
/api/files/...перестала быть правом пройти по ней. Одного заголовка мало: порядок здесь «узнавание → токен файла → ссылка». Это единственное значение, переживающее запрос, и на его срок отзыв доступа до файловой ссылки не доходит. - Кто допущен — решает Authelia, а не сервис. Своей проверки группы приложение не делает: кого пускать, определяет правило провайдера на этого клиента. Правило живёт вне репозитория, в настройках выкладки, и по коду его не проверить. Клиент, настроенный слишком широко, открывает сервис всякому, у кого есть учётная запись в общей Authelia. Решение владельца от 2026-08-12.
- Собственный вход хранилища закрыт целиком. Создание записи, вход по паролю, одноразовый код, обмен кода у внешнего провайдера, восстановление доступа и продление — ни один не даёт доступа и не меняет учётной записи: хранилище заводит коллекцию пользователей открытой, и без этого закрытия узнавание обходилось бы двумя запросами.
- Метрики и здоровье —
GET /metricsиGET /healthоткрыты неузнанному: учётной записи нет ни у пробы, ни у сборщика. Заголовок их ответа не меняет и учётной записи на них не заводит. Наружу их закрывает правило обратного прокси — работа выкладки, и сервис на неё не полагается: содержимого записей эти адреса не несут. - Приложение — его разметка и ресурсы открыты неузнанному, и ограничителя частоты на них нет: правило заведено под корень приложения, а раздача стоит вне его. Содержимого записей ни разметка, ни ресурсы не несут: они одинаковы для всех и собраны до всякого запроса. По ответу нельзя узнать, узнан ли кто-то, — узнанному и неузнанному отдаётся одно и то же.
Владение записью в модели данных появилось 2026-08-14: у задачи и у её файла есть владелец. Знание идентификатора задачи правом её читать больше не является — читает её тот, кто её принёс.
Целевой периметр заводит четыре механизма вместо одного белого списка; первый из них уже стоит:
| Механизм | Что даёт | Чья задача |
|---|---|---|
| Заголовок от Authelia через прокси | Право открыть приложение и его эндпоинты — сделано 2026-08-22; прежде то же давала сессия OIDC, с 2026-08-12 | trusted-header-login |
| Владелец у задачи и файла | Чужая запись по её идентификатору отвечает «не найдено» — сделано 2026-08-14 | record-ownership |
| Личный токен | Права своего владельца программе, которой прокси заголовка не ставит | api-tokens |
| Признак владельца сервиса | Страницу расхода и сводку по всем пользователям | admin-stats-screen |
Признак владельца сервиса — второй уровень доступа, которого в сегодняшней
модели нет вовсе: до него всё разграничение сводилось к «свой или чужой».
Откуда он берётся — из группы OIDC или из конфигурации — не решено
(admin-stats-screen).
Панель администратора в эту таблицу не входит и разграничению не подчиняется.
Суперпользователь PocketBase видит все записи, все файлы и всех пользователей
мимо любого из четырёх механизмов, а пускает его свой пароль, а не Authelia.
Замер показал, что закрыть панель провайдером OIDC или вторым фактором нельзя:
обе настройки у коллекции суперпользователей отклоняются. Остаётся ограничение
по списку адресов (superuserIPs), и оно же запирает владельца, если список
задан неверно: сброса в наборе команд нет.
Что чувствительнее чего
- Содержимое записей и расшифровок. Голосовые сообщения — личная переписка;
это самое чувствительное, что здесь есть. С 2026-08-14 оно живёт не одной
колонкой, а шестью коллекциями: сама запись (заголовок и краткое описание),
texts(расшифровка и вычитанный текст),structures(реплики со временем),recognitions(сырой ответ провайдера вложением — полный текст речи),record_events(журнал событий, содержимого не несёт) иtopics(словарь тем человека). Всякая новая коллекция, куда содержимое переезжает, закрывается наравне с записью — норму держит спекаstorage. - Ключи Yandex Cloud —
speech_kit_api_keyи пара ключей Object Storage. Утечка оплачивается деньгами и доступом к бакету. Секрета клиента OIDC в этом списке больше нет: 2026-08-22 он ушёл из конфига и из базы вместе с собственным входом.
Всё перечисленное лежит в config.toml. Файл в .gitignore, на сервер его
кладёт Ansible; gitleaks на pre-commit смотрит только индекс коммита.
Целевой периметр добавляет к списку пять записей, и первая из них — новый вид секрета, которого сегодня в проекте нет вовсе:
- Токены пользователей (
api-tokens). Токен даёт права своего владельца целиком. Срока жизни у него нет. В базе лежит только отпечаток, полное значение показывается один раз при выпуске. Это первый секрет, который хранится в базе, а не в конфигурации. - Ключ языковой модели и адрес шлюза bifrost (
llm-insights-adapter). Утечка оплачивается деньгами. - Пароль почтового сервера (
email-notification). - Адрес почты пользователя — приходит от Authelia и хранится у нас
(
oidc-login,email-notification). - Статистика потребления (
usage-accounting). Текста записей не содержит, но говорит, кто и когда пользовался сервисом и сколько; страница расхода открыта только владельцу. - Пароль владельца от панели. Открывает все записи, все файлы и всех пользователей разом, то есть стоит вровень с самым чувствительным из списка выше. Второй секрет после токенов пользователей, который лежит не в конфигурации: его отпечаток хранит сама база, а задаёт пароль сам владелец по приглашению, которое сервис печатает в журнал при первом запуске. У приглашения тридцать минут жизни, и после того как владелец заведён, оно не печатается вовсе — иначе строка журнала отдавала бы панель всякому его читателю навсегда.
Тексты расшифровок в логи не пишутся — логируется длина текста и
идентификаторы. Имя файла, данное отправителем, из журнала приёма убрано
2026-08-11 задачей no-user-filename-in-log; запрет проверяют тесты приёма по HTTP на
успешном пути и на пути отказа — они ищут значение, а не имя поля.
Хвост после последней точки остаётся в журнале, и это объявленное изъятие
инварианта приватности из ../CLAUDE.md, а не незакрытый остаток.
Расширение берётся из имени отправителя дословно (filepath.Ext), поэтому имя
запись.тайное-слово отдаёт тайное-слово, а Разговор с Петровым 11.08 —
08. В журнал оно идёт собственным полем, а не в составе имени файла: по нему
прослеживается путь записи. Читает этот журнал владелец сервиса. Нормализация
расширения в хранилище — отдельная работа, задачи на неё пока нет: формат имени
файла объявлен необратимым и меняется решением человека.
Наружу хвост не выходит. Метки метрик (file_extension у
transcriber_input_file_size_bytes, source_format у
transcriber_conversion_duration_seconds) несут расширение, только приведённое к
закрытому перечню известных форматов; всё прочее заменяется значением other.
Это закрыто задачей no-user-filename-in-log 2026-08-11 вместе с самим именем.
Заодно у метки размера принятой записи пропала ведущая точка (.mp3 стало
mp3) — форма выровнялась с меткой конвертации, которая точку не носила
никогда. Ряды, собранные до выкладки, перестают пополняться: панель, отобранная
по старому значению, покажет пустоту, и это не поломка.
Требование важно тем, что GET /metrics открыт вместе с остальным: без
приведения хвост читал бы кто угодно из интернета, а множеством значений метки
распоряжался бы анонимный отправитель.
Два пути утечки токена бота — адрес Bot API в отказе транспорта и отказ сборки
клиента — закрыты задачами no-user-filename-in-log и
local-run-without-telegram-token 2026-08-13 и потеряли предмет 2026-08-14
вместе с убранным входом: ни клиента, ни токена у сервиса больше нет. Разбор
случая остался в review.md — он про класс, а не про Telegram.
Путь, который остался, закрыт задачей telegram-enabled-flag 2026-08-13, и он
шире всякого одного ключа: до неё утечь мог любой секрет конфига. Отказ разбора файла
настроек пересказывался как есть, а библиотека разбора собирает текст отказа из
разбираемого куска — toml.ParseError кладёт в сообщение само значение. Строка
секретного ключа с оборванной кавычкой — типовая поломка криво собранного
шаблона выкладки — уносила ключ в журнал контейнера целиком. Теперь такой отказ
пересобирается своими словами: путь, строка, столбец и последний ключ, без текста
библиотеки; прочие отказы декодера собраны из имён ключей и типов и потому
проходят как есть. Нашло это ревью дизайна, чинилось решением владельца в той же
работе. Правило — conventions/config.md, «Секреты»;
оракулы — internal/config/config_test.go, проверки поломанного файла настроек.
Остаточный риск назван там же: разрез опирается на то, какое семейство отказов
несёт значения в нынешней версии библиотеки.
Что вне модели
Перечислить явно.
-
Атака на сам сервер и на контур. Компрометация хоста, прокси, Docker и Ansible — не наша граница.
-
Машина разработчика и то, что он на ней поднимает. В репозитории лежит
cmd/devtools— оснастка разработчика; её подкомандаproxyвстаёт на место контура: ставит заголовокRemote-Userи переправляет запрос сервису, не проверяя ничего. С 2026-08-15 по 2026-08-22 ту же роль игралcmd/oidcstub, подставной провайдер OIDC. Двух вещей это не отменяет, и обе проверяемы: в образ оснастка не едет (ступень собирает./cmd/transcriberпоимённо), а слушает петлевой адрес. Периметра выкладки она поэтому не касается; кто поднял её у себя в чужой сети, отвечает за это сам. -
Злоупотребление со стороны пользователя из белого списка. Приглашённому доверяем полностью.
-
Достоверность расшифровки. Подмена или искажение текста на стороне SpeechKit не рассматривается.
-
Стойкость к целенаправленной нагрузке. Ограничения по числу запросов и по размеру файла нет, и защищаться от исчерпания диска мы сейчас не пытаемся.
-
Исчерпание диска приглашёнными. Записи и тексты хранятся бессрочно (паспорт, 2026-08-11). Шестичасовая запись весит единицы гигабайт — оценка, а не замер: распределения длин у сервиса нет, а самая длинная проверенная запись — 9,6 МБ (research/pocketbase-defaults.md). Потолок длины стоит открытым вопросом
architecture.md, «Долгие записи». Квот нет — это граница домена, passport.md, «Учёт денег»; расход считаютusage-accountingиadmin-stats-screen. Для модели угроз отсюда следует одно: ни числом запросов, ни размером записи вошедший не ограничен, и защищаться от исчерпания диска мы не пытаемся. Рост каталога данных при этом ничем не наблюдается — открытый вопросarchitecture.md. -
Перерасход денег на внешних сервисах. Распознавание и языковая модель оплачиваются по факту; потолка на пользователя нет по тому же решению.
-
Стойкость
ffmpegк вредоносному входу. Разбор чужого формата отдан внешней программе, своей песочницы вокруг неё нет. -
Удаление данных по требованию. Ни файлы, ни расшифровки не удаляются вовсе. С 2026-08-11 это уже не недосмотр, а следствие решения хранить бессрочно, и тем же днём заведена задача
delete-record: своя запись убирается вместе с файлом, объектом в Object Storage и всеми уровнями текста. Учёт расхода удалению не подлежит по решению человека: деньги потрачены, а строки потребления текста не содержат.Руками запись сегодня не удаляется, и прежняя строка об этом была неверна. Проверено прогоном 2026-08-14: содержимое живёт в коллекциях, перечисленных выше («Что чувствительнее чего»), связи приложений с записью обязательны и каскада не имеют, поэтому удаление самой строки записи отвергается хранилищем, а удаление её файлов проходит молча. Владелец, выполнивший прежнюю процедуру, стирает аудио и оставляет полный текст речи — расшифровку, разбивку по репликам и сырой ответ провайдера файлом на диске. Порядок, которым запись убирается на самом деле: сперва строки приложений — журнал событий, попытка распознавания вместе с её вложением, структура, тексты, — потом сама запись, потом её файлы. До
delete-recordэто единственный способ, и он ручной целиком.