Безопасность: HMAC, токены, ротация
Эта страница объясняет, как PartyFlow защищает integration endpoints и что вы должны делать со своей стороны. Для готовых code snippets — guides/verify-signatures.md.
Модель угроз
Публичный webhook URL уязвим для трёх атак:
- Утечка URL — злоумышленник получает URL и шлёт произвольные сообщения.
- Replay — злоумышленник перехватил валидный запрос (например, через лог-агрегатор) и переотправляет его позже.
- Подмена — злоумышленник М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: 1Signing 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.
Рекомендации получателю
- Secret — в env / secrets manager. Никогда не коммитьте в репо. Показывается один раз при создании подписки, PartFlow не даст извлечь.
- Constant-time compare. Используйте
hmac.compare_digest(Python),crypto.timingSafeEqual(Node),hmac.Equal(Go). Timing attack делает возможным восстановление подписи побайтно. - Проверяйте подпись перед парсингом body. Если это ret атакующий, экономите CPU на парсинге JSON.
- Idempotency — обязательна. Сетевой flake может привести к тому же delivery_id дважды.
- Не верьте
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 для отправителя
- Храните secret в secrets manager (Vault, AWS Secrets Manager, Doppler, 1Password) — не в git.
- Подписывайте сырые bytes тела, а не reserialized JSON. Reserialization может изменить порядок ключей, пробелы, экранирование — подпись не сойдётся.
- Используйте constant-time compare при проверке подписей у себя (чтобы защититься от timing attack). PartyFlow на стороне приёма использует
hmac.Equal. - NTP-синхронизация. Если ваш отправитель работает в Docker-контейнере без sync — часы дрейфуют, и через несколько дней всё начнёт отваливаться в
401. - Retry with backoff. При
HTTP 429 Retry-After: 1— подождать указанное время, потом повторить. ПриHTTP 5xx— exponential backoff (1s, 2s, 4s, 8s, stop). - Не ретраить 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.
Что дальше
- guides/verify-signatures.md — code snippets на Python, Node.js, Go, Bash для подписи/проверки.
- reference/http-webhook-endpoint.md — полный справочник endpoint'а и error codes.
- concepts/rate-limits.md — как вести себя при 429, как устроен auto-disable.