docs: документация переведена на канон av-dev-pm

- беклог и план переехали в docs/tasks (38 задач, 11 целей), слаги
  переименованы с транслита на английские, 85 ссылок поправлены
- conventions.md разобран в docs/conventions/, local-research.md — в
  docs/research/, review-journal.md — в docs/review.md с разделом настройки
  конвейера; заведены security.md, adr/ и .pm.json
- шаг docs.py check добавлен в task gate; поведение в architecture.md помечено
  девятью маркерами долга, database.md получил настройки с числовым значением
This commit is contained in:
av
2026-08-03 17:14:53 +03:00
parent de7b15d48c
commit d79189be18
94 changed files with 1234 additions and 566 deletions
+109
View File
@@ -0,0 +1,109 @@
# Модель угроз
## Периметр
**Находки строятся против целевого периметра: сервис открыт в публичный
интернет.** Целевой контур — VPS **rivendell** (Timeweb) за **Caddy**, который
терминирует TLS; сам сервис слушает plain HTTP на localhost контейнера. Наружу
открыты два контура на разных поддоменах: **приём** (телефон, токен записи) и
**чтение вместе с MCP** (агенты и приложения, токен чтения). Отдельного контура
у MCP нет.
**Сегодняшний контур другой, и это переходное состояние, а не модель.** Сервис
живёт на рабочей машине, телефон достаёт до него только по локальной сети,
проверка токенов **выключена сознательно**, `config.docker.toml` коммитится без
секретов. Сервис предупреждает на старте обоими сообщениями (`write auth
disabled`, `read auth disabled`), но стартовать не отказывается.
Отсюда правило для проходов ревью: **выключенная сегодня проверка токенов — не
дефект, а объявленное состояние**; дефектом является путь, который остаётся
открытым и после включения токенов. Закрытие сегодняшнего контура — задача
«Управление токенами и секретами», решается перед деплоем.
Цена контуров разная и определяет ранжирование: открытый приём означает мусор
во входе, открытое чтение — **выгрузку всей истории здоровья** любому, кто нашёл
порт.
## Недоверенный вход
Отправитель контролирует целиком:
- **Тело доставки** — JSON от Health Auto Export: имена метрик, единицы,
значения, метки времени, имена источников и устройств, имена секций, `id`
тренировок и записей, содержимое маршрута.
- **Заголовки доставки** — включая `automation-id`, `automation-aggregation`,
`User-Agent`, `Accept-Language`, `Upload-Complete`; они пишутся в `delivery` и
участвуют в выводе слоя. Заголовки полуправдивы: `automation-aggregation`
реальной гранулярности не описывает (разведка, находка 33).
- **Размер тела** — предела на одну сущность нет; наблюдалось 63 МиБ на одной
координате и 768 МиБ пика кучи на теле 40 МиБ.
Позже к этому добавится **содержимое родного экспорта Apple** — zip-архив с
`export.xml`, который выбирает человек, но формируется он устройством и по
объёму (3,6 млн записей) глазами не проверяется.
Ответы внешних систем в недоверенный вход не входят: исходящих вызовов у
сервиса нет.
## Из чего строятся пути и ключи
- **Путь в архиве** — `<storage.archive_dir>/raw/ГГГГ/ММ/ДД/<ulid>.json.gz`.
Дата берётся из времени приёма, имя файла — из ULID, сгенерированного нами.
**Ни один сегмент пути не берётся из тела или заголовков доставки** — это и
есть защита от выхода за пределы каталога, и она держится ровно на этом.
- **Координатный ключ точки** — `метрика + слой + начало + конец`. Имя метрики
приходит из тела и в путь на диске не попадает, но попадает в ключ, в лог и в
ответ каталога. Любое значение из чужого JSON, попадающее в ключ, в лог или в
отчёт, имеет названный предел длины.
- **Ключ сущности** — `род секции + id` из HealthKit для `record`, `id` для
`workout`. `id` приходит из тела.
- **Файл базы и каталог архива** — из конфига, не из запроса.
## Что разграничивает доступ
Статический токен в заголовке `Authorization: Bearer …`; список допустимых
токенов — в конфиге. HAE умеет слать произвольные заголовки, этого достаточно.
Токены **раздельные**: запись (приём) и чтение. Клиент, читающий данные, писать
не может. MCP пользуется токеном чтения. Ролей, пользователей и сессий нет —
данные одного человека, разграничение только по контурам.
Конфиг с токенами лежит отдельным томом под `0600`; реальный `config.toml` не
коммитится, секреты рендерит деплой.
## Что чувствительнее чего
По убыванию:
1. **Данные о здоровье** — значения точек, тела доставок, содержимое архива.
Утечка необратима и невосполнима: это история конкретного человека за годы.
2. **Токен чтения** — открывает всю ту же историю целиком.
3. **Токен записи** — открывает загрязнение витрины; лечится пересборкой
журнала, то есть обратимо.
4. **Метаданные потока** — имена устройств, `automation-id`, объёмы и время
доставок. Выдают распорядок дня и модель телефона.
Отсюда правило логов: тела запросов и значения точек — только на `DEBUG` и с
обрезкой; токены — никогда, ни на каком уровне. Ничего из `./data` не попадает
ни в git, ни в логи выше `DEBUG`, ни в вывод агента — это проверяет `task gate`.
## Что вне модели
Перечислено явно, чтобы враждебный проход не выдумывал угрозу сам.
- **Компрометация самой машины rivendell и её оператора.** Получивший shell
получает и базу, и архив, и конфиг; шифрования на покое нет.
- **Компрометация телефона и учётной записи Apple.** Источник данных доверенный
по построению.
- **TLS, сертификаты и защита от сетевых атак** — целиком на Caddy; сервис
слушает plain HTTP и об этом знает.
- **DoS и исчерпание ресурсов как злонамеренное действие.** Пределы на размер
тела и заголовков нужны против **своего же телефона**, который шлёт 63 МиБ
честно; сценарий «злоумышленник выкачивает диск» не рассматривается — контур
приёма закрыт токеном, а токен есть только у одного устройства.
- **Многопользовательность, ролевая модель, аудит доступа.** Данные одного
человека; журнала обращений к чтению нет и не планируется.
- **Стойкость статического токена к подбору.** Токен длинный и генерируется
вне сервиса; ограничения частоты запросов нет.
- **Подмена содержимого доставки в пути.** Закрывается TLS на Caddy; подписи
тела нет.