Блокер номер один перед открытием эндпоинта оценки наружу: анонимный запрос
означал БЕЗЛИМИТ. В сервисе квот отсутствие имени пользователя трактовалось
как unlimited во всех функциях, с комментарием «dev без Caddy, fail-open».
Единственной защитой был общий лимит 300 запросов в минуту на IP — это
анти-флуд для дешёвых запросов, а не бизнес-лимит для пайплайна на десятки
секунд.
1. Fail-open больше не по умолчанию. Вызывающая сторона передаёт пустую
личность только при явно включённом флаге разработки (по умолчанию выкл).
2. Анонимная личность — подписанная кука с HMAC-SHA256, отдельным каналом
от X-Authenticated-User. Тот заголовок ставит Caddy и валидирует внутренним
секретом; смешивать схемы нельзя, это сломало бы модель безопасности.
Ключ подписи из окружения; если не задан — эфемерный на процесс, с
предупреждением в лог.
3. Анонимная квота на паре «сессия + IP», переиспользует существующую таблицу
и тот же атомарный инкремент под WHERE used < lim (защита от гонки #747).
Честно закомментировано: смена IP или чистка куки обходит лимит — задача
поднять стоимость злоупотребления, а не сделать его невозможным.
4. Отдельный жёсткий лимит частоты на оценку, проверяется ДО квоты.
Переиспользован готовый SlidingWindowLimiter. Redis намеренно не задействован:
прод работает одним воркером, состояние теряется только при рестарте, а
основная защита — месячная квота в Postgres. Компромисс задокументирован.
5. Потолок времени ответа. Вызов Avito IMV шёл БЕЗ бюджета, в отличие от всех
соседних — единственный источник неограниченного времени. Обёрнут.
Суммарный худший случай: было ~186 с (36 с ограниченных плюс IMV без
границы ~150 с), стало 56 с.
Тесты: 2754 passed. Два существующих теста обновлены под изменившееся
поведение fail-open — это ожидаемое изменение, не регрессия.