# Runbook: усиление webhook (DNS-rebind пиннинг + аддитивный HMAC)

**Дата:** 2026-06-17 · **Зона:** `app/` (backend) · **Связано:** edge/P2 go-live

## Часть А — DNS-rebind пиннинг на доставке

- `App\Support\WebhookUrlGuard::safeDeliveryIp($url): array` — ОДИН резолв хоста →
  `['ip' => ?string, 'blockReason' => ?string]`. Любой приватный/зарезервированный
  IP → блок; все публичны → первый IP для пиннинга; нерезолвимый → ip=null.
- `WebhookSettingsController::test()` использует `safeDeliveryIp` и подключается к
  проверенному IP через `Http::withOptions(['curl' => [CURLOPT_RESOLVE => ...]])` —
  HTTP-клиент не резолвит хост повторно (закрыт TOCTOU). `blockReason()` оставлен
  для сохранения.

## Часть Б — аддитивный HMAC для supplier-webhook

- `SupplierWebhookController::receive($request, $secret = '')` — аутентификация
  `verifySecret($secret) OR HMAC`. HMAC: `X-Webhook-Signature` (формат `<hex>` или
  `sha256=<hex>`) = `hash_hmac('sha256', raw_body, supplier_webhook_secret)`,
  сверка `hash_equals`.
- Новый маршрут `POST /api/webhook/supplier` (без `{secret}`) — только HMAC.
  Старый `POST /api/webhook/supplier/{secret}` сохранён (backward-compat).
- Секрет в URL **НЕ удалён** — это аддитивный шаг. Вывод URL-секрета по флагу —
  после миграции отправителя crm.bp-gr.ru на подпись.

## Проверка

`php artisan test --filter=Webhook` (из каталога `app`).

> NB: на прогоне 17.06 вне моих файлов наблюдались pre-existing падения
> (устаревшая ожидалка B-regex в supplier-тестах после Phase-3; CsvWebhookRaceTest).
> Требуют сверки с baseline — см. статус сессии.
