---
title: Подписанные вебхуки — откуда получатель знает, что это правда Pingvera
description: Вебхук — универсальный клей для n8n, Zapier и своих скриптов. Но как получатель отличит настоящий POST от Pingvera от подделки? Разбираем HMAC-подпись — секрет, заголовки, проверка на Go/PHP/Python.
source: https://pingvera.ru/blog/podpisannye-vebhuki.html
---
# Подписанные вебхуки: откуда получатель знает, что это правда Pingvera

Вебхук — самый универсальный клей в мониторинге: [сайт клиента упал](https://pingvera.ru/blog/sayt-klienta-upal-kak-uznat-ranshe.html) — POST прилетел в ваш n8n, оттуда в тикет-систему, оттуда в Slack-канал студии. Никакого интерфейса, никакого ручного клика — просто HTTP-запрос туда, куда вы скажете. Но у этой простоты есть обратная сторона: ваш обработчик получает POST от _кого угодно_, кто знает URL. Откуда ему знать, что это правда Pingvera, а не кто-то подделал тело запроса и прислал фальшивый «инцидент»?

Ответ — подпись. Разберём, как она устроена в Pingvera, что реально приходит в заголовках и как её проверить, не наступив на типичные грабли.

## Вебхук — это просто POST на ваш URL

Канал доставки типа `webhook` при каждом событии алертинга — открытие инцидента, закрытие, тестовое сообщение, недельный CMS-дайджест — шлёт на настроенный адрес обычный `POST` с JSON-телом и `Content-Type: application/json`. Никакой магии: это тот же формат, что принимают n8n, Zapier-аналоги или ваш собственный маленький обработчик на пяти строчках. Доставка ждёт ответ 10 секунд, любой статус ≥300 — неудача, которая ретраится с экспоненциальным backoff до 8 попыток (суммарно около 2 часов 15 минут), а затем помечается окончательно неудавшейся.

В теле — плоский JSON без вложенностей, специально так, чтобы его было легко разобрать в любом no-code-инструменте:

`{
 "event": "opened",
 "severity": "critical",
 "summary": "host offline",
 "incident": "inc_01J...",
 "org": "ООО Ромашка",
 "entity": "acme.com · Доступность",
 "target": "https://acme.com",
 "link": "https://app.pingvera.ru/#/incidents/inc_01J...",
 "started_at": "2026-07-21T10:15:00Z"
}`
 `event` — одно из `opened` / `resolved` / `test` / `digest`; `incident` пустой у `test`/`digest`; `link` ведёт прямо на карточку в дашборде — удобно вставлять кликабельной кнопкой в тикет или Telegram-сообщение, которое собирает ваш сценарий.

## Проблема: URL — не секрет

Вебхук-адрес рано или поздно светится: в логах n8n, в конфиге интеграции, в скриншоте на созвоне. Если обработчик доверяет любому телу, которое на него пришло, — он доверяет не Pingvera, а факту знания URL кем угодно. Для сценария «упал сайт → тикет клиенту» это не абстрактный риск: поддельный `opened` с чужим доменом в поле `entity` — это ложная тревога, которая уходит клиенту студии от её же имени.

Решение — не прятать URL глубже, а подписывать каждое тело секретом, которого нет ни у кого, кроме Pingvera и вас.

## Как устроена подпись

Каждый webhook-канал получает секрет **при создании** — он приходит один раз, в ответе `POST /api/v1/channels` (поле `secret`, префикс `whsec_`), и больше нигде в API не отдаётся. Как и API-токен — если потеряли, пересоздаёте канал, а не «смотрите ещё раз в настройках».

Пока секрет у канала есть, к каждому запросу добавляются два заголовка:

`X-Pingvera-Signature: sha256=<hex>
X-Pingvera-Timestamp: <unix-секунды>`
 `X-Pingvera-Timestamp` — просто время отправки, в саму подпись не входит, информационное поле. Вся защита — в `X-Pingvera-Signature`: это `HMAC-SHA256(secret, raw_body)` в hex, где `raw_body` — ровно те байты, что легли в тело запроса.

## Проверка на своей стороне

Здесь есть одна практическая ловушка, из-за которой проверка «не сходится» чаще всего: HMAC нужно считать от **сырых байтов тела**, а не от JSON, который вы пересобрали после парсинга. Порядок ключей, пробелы, экранирование юникода — всё это может отличаться после повторной сериализации, и хэш просто не совпадёт, хотя данные те же. Читайте тело как поток байт до `json.Unmarshal`/`json_decode`, считайте HMAC от него, и только потом парсите.

Вторая ловушка — сравнение через обычный `==`. Оно завершается на первом несовпадающем байте, и по времени ответа теоретически можно подобрать правильную подпись. Сравнивайте в constant-time: `hmac.Equal` в Go, `hash_equals` в PHP, `hmac.compare_digest` в Python.

Пример на Go:

`func verify(secret string, rawBody []byte, header string) bool {
 mac := hmac.New(sha256.New, []byte(secret))
 mac.Write(rawBody)
 want := "sha256=" + hex.EncodeToString(mac.Sum(nil))
 return hmac.Equal([]byte(want), []byte(header))
}`
 На Python — то же самое, только тело нужно брать до парсинга (`request.get_data()` во Flask отдаёт именно сырые байты):

`def verify(secret: str, raw_body: bytes, header: str) -> bool:
 mac = hmac.new(secret.encode(), raw_body, hashlib.sha256)
 want = "sha256=" + mac.hexdigest()
 return hmac.compare_digest(want, header)`
 На PHP аналогично: `file_get_contents('php://input')` вместо уже распарсенного `$_POST`, и `hash_equals` вместо `===`.

## Ещё один слой: защита от SSRF

Подпись защищает получателя от подделки. Но есть и обратная сторона — защита отправителя от того, чтобы кто-то через настройку webhook-URL заставил Pingvera постучаться во внутреннюю сеть. Поэтому адреса из приватных диапазонов (localhost, `10.0.0.0/8` и подобные) как цель webhook-доставки заблокированы — это защита от SSRF, а не ограничение по недосмотру.

## Каналы без секрета никуда не денутся

Webhook-каналы, созданные до появления подписи, продолжают получать те же уведомления — просто без заголовков `X-Pingvera-Signature`/`X-Pingvera-Timestamp`. Ничего не сломалось и ломать не планируется: обратная совместимость важнее, чем принудительная миграция чужих интеграций. Если хочется подписанные уведомления на существующий сценарий — пересоздайте канал, секрет придёт при создании нового.

Отдельно: Slack и Discord используют свой формат incoming-webhook (`{"text": …}` / `{"content": …}`), и секрета подписи там нет — эта схема касается только каналов типа `webhook`, куда Pingvera шлёт собственный JSON.

## Куда это реально прокидывать

Для студии, которая ведёт мониторинг десятка клиентских сайтов, вебхук с подписью — это точка входа в то, что уже есть: сценарий в n8n, который по `opened` заводит карточку в Trello и шлёт клиенту сообщение с человеческим текстом из `entity`/`summary`; Zapier-аналог, кладущий инцидент в общую таблицу учёта SLA; или просто эндпоинт на своём сервере, который решает, кому из дежурных постучаться. Подпись в этой схеме — не бюрократия, а ровно то, что позволяет доверять автоматике: обработчик может смело исполнять действие («открыть тикет», «уведомить клиента») именно потому, что заранее проверил — тело реально от Pingvera.

Сам канал заводится тем же [write-API](https://pingvera.ru/blog/api-upravleniya-monitoringom.html), которым создаются мониторы и статус-страницы, или командой `channels create` из [CLI](https://pingvera.ru/blog/cli-pingvera-monitoring-iz-terminala.html) — вебхук с подписью заводится на онбординге клиента вместе с остальным мониторингом, без единого клика в дашборде.

## Главное

Вебхук без подписи — это удобно, но требует слепого доверия к URL. Вебхук с подписью снимает это допущение: секрет выдаётся один раз при создании канала, каждое тело идёт с HMAC-SHA256 от секрета, а проверка на вашей стороне — десяток строк на любом языке, если считать её от сырых байт и сравнивать в constant-time. Старые каналы без секрета работают как раньше — подпись добавляется, а не заменяет существующее поведение.

## Частые вопросы

**Зачем вообще подписывать вебхук?**

Без подписи обработчик обязан верить любому POST на этот URL, а адрес легко угадать или подсмотреть в логах. Подпись доказывает, что тело создано тем, у кого есть секрет (Pingvera), и не менялось по пути — защита от подделанных «инцидентов» и от случайного мусора на публичном эндпоинте.

**Что подставлять в проверку — тело запроса или JSON после парсинга?**

Только сырые байты тела, как они пришли по HTTP, — до `json_decode`/`json.Unmarshal`. Если пересобрать JSON заново и посчитать HMAC от него, подпись почти всегда не сойдётся: порядок ключей и пробелы после повторной сериализации могут отличаться от оригинала.

**Что будет с каналами, которые я создал до появления подписи?**

Они продолжат работать как раньше и просто не получат заголовки `X-Pingvera-Signature`/`X-Pingvera-Timestamp` — доставка не ломается. Секрет выдаётся только новым webhook-каналам и показывается один раз при создании; чтобы получить подписанные уведомления на старый URL, пересоздайте канал.
