PartyFlow

Безопасность: HMAC, токены, ротация#

Эта страница объясняет, как PartyFlow защищает integration endpoints и что вы должны делать со своей стороны. Для готовых code snippets — guides/verify-signatures.md.


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

Публичный webhook URL уязвим для трёх атак:

  1. Утечка URL — злоумышленник получает URL и шлёт произвольные сообщения.
  2. Replay — злоумышленник перехватил валидный запрос (например, через лог-агрегатор) и переотправляет его позже.
  3. Подмена — злоумышленник МitM'ит сеть и подменяет тело запроса.

HMAC-подпись закрывает (2) и (3), уменьшает импакт (1) — если URL утёк, но secret нет, злоумышленник всё равно не сможет отправить валидный запрос.


Два режима webhook'а#

При создании webhook'а выбирается режим Require signature:

Режим Когда использовать Риск
On (default) Прод, любые важные каналы. Требует кода на стороне отправителя, который умеет считать HMAC-SHA256.
Off (token-only) CI/CD, мониторинг, простые скрипты, которые не умеют HMAC. Если URL утёк — нет второго фактора защиты.

Secret генерируется и хранится в обоих режимах. Это позволяет:

  • Сначала поднять webhook в Off, отладить.
  • Потом включить On без пересоздания webhook'а и потери URL.

Две схемы подписи#

PartyFlow принимает HMAC-подпись в одной из двух схем. Они взаимоисключающие — прислать обе сразу бессмысленно. Endpoint проверяет сначала Slack-схему, потом PartyFlow-native.

Slack-совместимая#

Когда использовать: ваш сервис уже подписывает запросы в стиле Slack (например, вы форвардите запросы, которые изначально шли в Slack workspace, или используете готовую Slack-библиотеку).

Заголовки:

X-Slack-Signature:          v0=<hex(HMAC-SHA256)>
X-Slack-Request-Timestamp:  <unix-seconds>

Signing string:

v0:<timestamp>:<raw-body>

Алгоритм: HMAC-SHA256(secret, signing_string) → hex encoded, префикс v0=.

PartyFlow-native#

Когда использовать: новая интеграция, есть свобода выбора схемы. Формат близок к Stripe/Linear.

Заголовки:

X-PartyFlow-Signature:  sha256=<hex(HMAC-SHA256)>
X-PartyFlow-Timestamp:  <unix-seconds>

Signing string:

<timestamp>.<raw-body>

Алгоритм: HMAC-SHA256(secret, signing_string) → hex encoded, префикс sha256=.


Anti-replay: timestamp window#

Запросы старше 5 минут отвергаются, даже если HMAC валиден.

  • Проверяется условие |server_now - timestamp| > 300s.
  • Если timestamp — пустой или не число, запрос отвергается.
  • Проверка до сверки HMAC, чтобы не тратить CPU на устаревшие запросы.

Что это даёт:

  • Злоумышленник, перехвативший запрос (например, через утечку логов), не может переотправить его больше чем через 5 минут.
  • Требует разумной синхронизации часов у отправителя (NTP).

Если у вас |now - sender_now| > 5m, вы увидите HTTP 401 — это не проблема с secret'ом, это часы.


Outgoing webhooks: подписание на стороне PartyFlow#

Outgoing webhooks работают в обратную сторону: PartyFlow подписывает исходящие POST-запросы вашим signing secret, ваш сервис проверяет. Схема похожа на incoming PartyFlow-native, но с явной версией в signing string — это позволяет нам безопасно эволюционировать схему в будущем, не ломая существующих получателей.

Заголовки outgoing delivery#

X-PartyFlow-Signature:       sha256=<hex(HMAC-SHA256)>
X-PartyFlow-Timestamp:       <unix-seconds>
X-PartyFlow-Delivery-Id:     <UUIDv7>
X-PartyFlow-Event:           <canonical event type, e.g. MESSAGE_CREATED>
X-PartyFlow-Webhook-Version: 1

Signing string#

v1:<timestamp>:<raw-body>

Обратите внимание на отличие от incoming: префикс v1: вместо простого .-разделителя. Схемы не конфликтуют, но и не взаимозаменяемы — verification код incoming нельзя использовать 1-в-1 для outgoing (и наоборот).

Направление Signing string Версионирование
Incoming (вы → PartyFlow) <timestamp>.<body> нет явной версии
Outgoing (PartFlow → вы) v1:<timestamp>:<body> префикс v1: явно зашит, X-PartyFlow-Webhook-Version: 1 дублирует

Frozen signature и retry#

Перед первой отправкой доставки PartyFlow считает подпись один раз и фиксирует её вместе с телом. Все retry той же доставки приходят с точно теми же X-PartyFlow-Signature и X-PartyFlow-Timestamp. Это означает:

  • Вы можете кэшировать проверку подписи по X-PartyFlow-Delivery-Id.
  • Ротация signing secret на стороне admin'а не ломает уже запланированные ретраи — они доставятся со старой подписью, которая всё ещё проверяется тем же старым secret'ом в момент receipt.
  • Timestamp window (5 минут) проверяется от X-PartyFlow-Timestamp. Если retry пришёл через час после первой попытки — timestamp уже устарел. Для outgoing delivery timestamp window не применяется на стороне получателя так строго, как на incoming — лучше использовать idempotency по delivery-id.

Idempotency по Delivery-Id#

X-PartyFlow-Delivery-Id стабилен на все попытки одного и того же row'а в очереди. Храните обработанные ID в течение 1-2 часов (вполне хватает retry window'а) и отвечайте 2xx без повторного применения side-effects.

Рекомендации получателю#

  1. Secret — в env / secrets manager. Никогда не коммитьте в репо. Показывается один раз при создании подписки, PartFlow не даст извлечь.
  2. Constant-time compare. Используйте hmac.compare_digest (Python), crypto.timingSafeEqual (Node), hmac.Equal (Go). Timing attack делает возможным восстановление подписи побайтно.
  3. Проверяйте подпись перед парсингом body. Если это ret атакующий, экономите CPU на парсинге JSON.
  4. Idempotency — обязательна. Сетевой flake может привести к тому же delivery_id дважды.
  5. Не верьте X-PartyFlow-Event без верификации подписи. Заголовки тоже входят в тело HTTP-запроса и могут быть подделаны. Доверяйте только после успешной verify.

Полные snippets для Python/Node/Go/Bash — guides/verify-signatures.md.


Ротация ключей#

Для webhook есть две независимые ротации:

Что ротирует Что меняется URL меняется?
Rotate token URL-токен (часть URL) + signing secret. Старый URL перестаёт работать мгновенно. Да
Rotate signing secret Только signing secret. Старый secret перестаёт работать мгновенно. URL тот же. Нет

Когда ротировать token:

  • Утечка URL в публичный репо, Slack, тикет.
  • Смена владельца интеграции.

Когда ротировать signing secret:

  • Утечка secret (например, в логе).
  • Плановая ротация (например, ежеквартально).
  • Переход require_signature: false → true на существующем webhook'е, если старый secret никто не использовал.

Ротация возвращает новый plaintext-ключ один раз — сохраните в secrets manager сразу.

Outgoing webhooks: ротация signing secret#

Для outgoing подписки есть одна ротация — Rotate signing secret. URL не меняется, signing secret заменяется на новый.

Важное отличие от incoming: запросы, уже запланированные к доставке до ротации, продолжают лететь со старой подписью (headers фиксируются перед первой отправкой). На стороне получателя это значит: во время окна ротации у вас одновременно могут приходить запросы со старой и новой подписью — поддерживайте оба secret'а до завершения старых retry (до часа после события).

Рекомендация: поддерживайте два signing secret параллельно минимум в течение 2 часов после ротации, принимая запросы, которые сходятся любым из них.


Best practices для отправителя#

  1. Храните secret в secrets manager (Vault, AWS Secrets Manager, Doppler, 1Password) — не в git.
  2. Подписывайте сырые bytes тела, а не reserialized JSON. Reserialization может изменить порядок ключей, пробелы, экранирование — подпись не сойдётся.
  3. Используйте constant-time compare при проверке подписей у себя (чтобы защититься от timing attack). PartyFlow на стороне приёма использует hmac.Equal.
  4. NTP-синхронизация. Если ваш отправитель работает в Docker-контейнере без sync — часы дрейфуют, и через несколько дней всё начнёт отваливаться в 401.
  5. Retry with backoff. При HTTP 429 Retry-After: 1 — подождать указанное время, потом повторить. При HTTP 5xx — exponential backoff (1s, 2s, 4s, 8s, stop).
  6. Не ретраить 4xx (кроме 429). Это клиентские ошибки — ретрай не поможет, только засорит логи.

Bot tokens#

Токен бота имеет формат:

fri_bot_<256-bit-base64url>
  • Генерируется при создании бота и показывается один раз.
  • В БД хранится как SHA-256 hash — сам токен не извлекается даже имея доступ к БД.
  • Валидация использует timing-safe compare.
  • При рекомпрометации — Regenerate token в UI. Старый перестаёт работать мгновенно.

Передаётся в заголовке:

Authorization: Bearer fri_bot_<token>

Rate-limit на бот: 10 сообщений в секунду на канал.


Что делать при утечке#

Утекло Что делать
Incoming webhook URL Ротировать token. Старый URL перестанет работать.
Incoming signing secret Ротировать signing secret.
Outgoing signing secret (вы получатель) Попросить admin'а ротировать через UI подписки. Поддерживайте два secret'а ~2 часа до опустошения retry-очереди.
Bot token Regenerate token в UI бота.
Что из этого уже использовалось злоумышленником Проверить логи канала на подозрительные сообщения, удалить, оповестить пользователей.

Что НЕ защищает HMAC#

  • DoS — если у злоумышленника есть URL + secret, он может засыпать endpoint до rate-limit'а. Защита — auto-disable (100 подряд ошибок → HTTP 410).
  • Content injection через XSS — если ваш сервис подставляет пользовательский ввод в text/attachments без sanitization, злоумышленник может прислать вам malicious JSON, который пройдёт валидацию подписи и окажется в канале. Sanitize входные данные до формирования payload'а.
  • Data exfiltration через webhook URL — если у бэкенда есть SSRF и webhook URL записан в БД, злоумышленник может заставить бэкенд слать данные в свой webhook. Не относится к PartyFlow.

Что дальше#