# Модель угроз ## Периметр **Находки строятся против целевого периметра: сервис открыт в публичный интернет.** Целевой контур — 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`. Ответы внешних систем в недоверенный вход не входят: исходящих вызовов у сервиса нет. ## Из чего строятся пути и ключи - **Путь в архиве** — `/raw/ГГГГ/ММ/ДД/.json.gz`. Дата берётся из времени приёма, имя файла — из ULID, сгенерированного нами. **Ни один сегмент пути не берётся из тела или заголовков доставки** — это и есть защита от выхода за пределы каталога, и она держится ровно на этом. - **Координатный ключ точки** — `метрика + слой + начало + конец`. Имя метрики приходит из тела и в путь на диске не попадает, но попадает в ключ, в лог и в ответ каталога. Любое значение из чужого JSON, попадающее в ключ, в лог или в отчёт, имеет названный предел длины. - **Ключ сущности** — `род секции + 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; подписи тела нет.