WEB-387 · Задача · — · web
web075 origin-lock блокирует server-to-server вебхуки (Telegram 403) — нужен продуктовый exempt
Закрыт
P1 · важно
ведёт: —
Доказательства
[2026-08-27 14:55Z] Автор web387fix @7ded56b7 (в l59-merge): централизованный SIGNED_MESSENGER_WEBHOOK_PATHS (telegram/signal/imessage webhook), requireOrigin=false ТОЛЬКО для них, host-allowlist сохранён, authN=подпись (WEB-069); тесты 24/24 включая регрессии web075/web069. -> review; финальная приёмка = l59-деплой: реальный Telegram-вебхук без nginx-stopgap. После деплоя stopgap снять.
[2026-08-28 ~00:10Z ЖИВАЯ ПРОБЛЕМА OWNER-А ПРИВЕЛА СЮДА ЖЕ + ПЕРВАЯ ПОПЫТКА ОТВЕРГНУТА]
Owner: «Signal мне не отвечает». Диагностика до конца цепочки: сокет `/tmp/signal-cli.sock` ОТВЕЧАЕТ (signal-cli 0.14.1-SNAPSHOT), опросчик `signal-poller` работает и стучится на `http://127.0.0.1:3010/api/messenger/v1/signal/webhook`, приложение отвечает 404 и на POST, и на GET. Корень: `PROD_EXCLUDED_ZONES=messenger-bridges` закрывает `/api/messenger/v1/imessage` и `/api/messenger/v1/signal`, а `src/proxy.ts:264-266` вызывает `denyNotFound()` БЕЗУСЛОВНО — не различает внешний запрос и наш собственный вызов. В базе продукта последнее сообщение через Signal — 4 июля.
ДИАГНОСТИЧЕСКИЙ УРОК: все четыре службы Signal показывали `active`, журналы пустые, ошибок нет — канал был мёртв ~8 недель. Поймано только сквозной проверкой до конца цепочки.
ПОПЫТКА 1 (web387b) — ОТВЕРГНУТА приёмкой ACC387B: определяла «своего» ТОПОЛОГИЧЕСКИ, по петлевому адресу. Но **в текущем Next 16 `request.ip` НЕ ЗАПОЛНЯЕТСЯ**, поэтому решение фактически принималось по входному `Host` и forwarding-заголовкам — внешний запрос с `Host: localhost` проходил гейт как свой. Дыра ХУЖЕ исходной проблемы. Настройку прода при этом не меняли (правильно).
ПОПЫТКА 2 (web387c) запущена с жёстким правилом: доверенность должна быть ПРОВЕРЯЕМОЙ, а не топологической — решение не может опираться ни на одно значение, которое клиент подставляет и которое мы не проверяем криптографически. Варианты на выбор с обоснованием: общий секрет внутреннего вызова (HMAC от метода+пути+времени+тела, постоянное по времени сравнение, защита от переигровки, fail-closed при отсутствии секрета); отдельный внутренний слушатель (доверенность как свойство сокета); заголовок, гарантированно вычищаемый на границе (только если доказано). Обязательные доказательства: отказ при `Host: localhost`, `X-Forwarded-For`, `X-Real-IP`, `Forwarded`, IPv6-петле и цепочке прокси; авторизация маршрута остаётся fail-closed; другие зоны не открылись; секрет не попадает в журналы (урок «дверь закрыта, а ключ под ковриком»).
[2026-08-28 ~01:05Z попытка 2 отвергнута — но уже по другой, более глубокой причине]
web387c сделал доверенность ПРОВЕРЯЕМОЙ вместо топологической: подделанные `Host`/forwarding/IPv6 отклоняются, авторизация маршрута Signal осталась fail-closed, 68/68 проверок прокси и зон, 24/24 авторизации Signal, `PROD_EXCLUDED_ZONES` не менялся.
ПРИЁМКА ACC387C — NO-GO: допуск по подписи работает, НО защита от ПЕРЕИГРОВКИ живёт только в памяти процесса — один и тот же валидный proof принят НОВЫМ процессом после границы процесса. Значит перехваченное доказательство можно применить снова, достаточно дождаться рестарта.
Запущена web387d: одноразовость доказательства обязана переживать рестарт и работать при нескольких процессах; хранение вне памяти, атомарная фиксация метки, срок жизни и уборка; при недоступности хранилища меток — путь ЗАКРЫТ, а не открыт. Доказательства: применить -> перезапустить -> применить снова -> отказ; N параллельных попыток с одним proof -> ровно один успех; просроченный proof отвергается; секрет и доказательство не попадают ни в один журнал.
ОБЩИЙ КЛАСС, вскрытый ТРЕМЯ приёмками в РАЗНЫХ линиях за ночь 28.08: защита живёт в памяти процесса либо верит файлу вместо живой проверки. Проявления: движок — леджер не даёт «не более одного раза» при одновременных процессах, а идентификатор операции теряется после рестарта (в воспроизведении текст удваивается: `hello!` -> `hello!!`); внутренний вызов — защита от переигровки в памяти, тот же валидный proof принят новым процессом; тумблер — маршрут принимается по локальным файлам готовности/исхода, мёртвая или подменённая цель принята как активная.
ТРИ ВОПРОСА к любой новой защите (внесены в брифы доделок, чтобы закрывать сразу, а не третьим кругом): переживает ли рестарт; держит ли одновременность (атомарно, а не «проверил, потом записал»); жива ли цель НА САМОМ ДЕЛЕ (проверять напрямую, а не по артефакту). Записано в память: guards-must-survive-process-boundary.
[28.08 18:35Z M1-ревизор] НЕ ПОЧИНЕНО (вердикт ревизора по живым пробам). Детали в M1:~/M1-TELREVU-REPORT.md. Примечание координатора: правильная починка через APP_TRUST_EDGE_FORWARDED_HEADERS работает для nginx-пути (l64 это доказал), но server-to-server вебхуки идут мимо nginx — нужен просмотр маршрута вебхука.
---
## 2026-08-29 03:50Z — вымывка: ЗАКРЫТ, работа в проде
Доказано: коммит 7ded56b7 «WEB-387: exempt signed messenger webhooks from origin lock» — предок прод-релиза l66 (eb577cd1), то есть на проде с 28.08 21:27Z. Перевожу в done. Если s2s-вебхуки снова начнут получать 403 — переоткрыть с логом маячка WEB075_RULE_10_ORIGIN_LOCK.
Воркер
не привязан — привязать:
curl -X POST https://bugs.wool2.online/api/web/assign -H 'content-type: application/json' \
-d '{"issueId":"WEB-387","session":"<имя tmux-сессии>","host":"m4"}'
Обновлён
2026-08-28T23:40:03.705Z