# Модель угроз

## Периметр

**Находки строятся против целевого периметра: сервис открыт в публичный
интернет.** Целевой контур — 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`,
  участвуют в выводе слоя, а `Accept-Language` — ещё и в выводе кода
  категориального значения (тег ограничен по длине и по форме, не тег даёт
  пустую локаль). Заголовки полуправдивы: `automation-aggregation`
  реальной гранулярности не описывает (разведка, находка 33).
- **Размер тела** — предела на одну сущность нет; наблюдалось 63 МиБ на одной
  координате и 768 МиБ пика кучи на теле 40 МиБ.

Позже к этому добавится **содержимое родного экспорта Apple** — zip-архив с
`export.xml`, который выбирает человек, но формируется он устройством и по
объёму (3,6 млн записей) глазами не проверяется.

**Новый адресат недоверенного входа — терминал оператора.** Подкоманда
`healthlog uncovered` печатает имена секций, а имя это верхнеуровневый ключ
чужого тела: длина у него ограничена разбором (64 байта, не больше 32 имён),
содержимое — ничем. Печатается оно экранированным (`%q`), иначе управляющая
последовательность из тела подделала бы строки вывода. Тот же вход попадает
структурным атрибутом в лог свёртки, где его экранирует кодировщик `slog`.

Ответы внешних систем в недоверенный вход не входят: исходящих вызовов у
сервиса нет.

## Из чего строятся пути и ключи

- **Путь в архиве** — `<storage.archive_dir>/raw/ГГГГ/ММ/ДД/<ulid>.json.gz`.
  Дата берётся из времени приёма, имя файла — из ULID, сгенерированного нами.
  **Ни один сегмент пути не берётся из тела или заголовков доставки** — это и
  есть защита от выхода за пределы каталога, и она держится ровно на этом.
- **Координатный ключ точки** — `метрика + слой + начало + конец`. Имя метрики
  приходит из тела и в путь на диске не попадает, но попадает в ключ, в лог, в
  ответ каталога и — с появлением маршрута точек — **в адрес запроса и в
  заголовок `ETag` ответа**. Любое значение из чужого JSON, попадающее в ключ, в
  лог, в отчёт или в заголовок, имеет названный предел длины. У метки ответа
  предел взят формой: в неё уезжает не имя, а хеш канонизированной формы запроса
  (128 бит). Причина не только в длине — имя законно содержит кавычку, которая
  по RFC 9110 кончает метку, и разбор обрезал бы её ровно там.
- **Имя метрики в адресе** декодируется из пути **ровно один раз**. Второе
  декодирование превращает имя `a%41b` в имя `aAb` — то есть в имя **другой**
  метрики витрины, и маршрут отвечает `200` её данными. Путь построен и прогнан
  враждебным проходом ревью.
- **Ключ сущности** — `род секции + id` из HealthKit для `record`, `id` для
  `workout`. `id` приходит из тела.
- **Ключ наблюдённого категориального значения** — `метрика + поле + значение`.
  Значение приходит из тела дословно и уезжает в первичный ключ: предел на него
  назван числом (128 байт), число различных значений одной доставки ограничено
  (64), и **граница применяется при накоплении, а не при выдаче** — иначе
  накопитель растёт вместе с телом, а тело контролирует отправитель (измерено:
  миллион различных значений в теле 60 МиБ поднимал пик процесса с 780 до
  1002 МиБ). Значение, которое разбор JSON подменил (невалидный UTF-8, одинокий
  суррогат), наблюдением не считается вовсе: в ключ обязано попасть то, что
  пришло, а не то, что получилось.
- **Файл базы и каталог архива** — из конфига, не из запроса.

## Что разграничивает доступ

Статический токен в заголовке `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; подписи
  тела нет.
