Вебхуки
Сервер сам сообщает вашей системе об изменениях.
Зачем
SDK читает секреты при старте сервиса. Этого достаточно, пока значения меняются редко: перезапустили — подхватили новое.
Вебхук нужен, когда реагировать надо сразу: перечитать конфиг без перезапуска, поднять тревогу при неудачной ротации, узнать, что одноразовый токен предъявили второй раз.
Как завести
Раздел Вебхуки проекта — или запрос:
curl -s $BASE/projects/$PROJECT/webhooks \
-H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{"url":"https://ops.example.com/envmaster","eventTypes":["entry.updated","rotation.failed","enrollment.reuse-detected"]}'
Нужны адрес, список событий и, если хотите, список окружений
(environments) — пустой означает «все».
Секрет подписи возвращается в ответе один раз, при создании. В базе он
хранится зашифрованным и повторно не показывается: если потеряется, вебхук
придётся завести заново. Право — проектный admin; последние 50 доставок
со статусами смотрятся в кабинете или GET /projects/:id/webhooks/:id/deliveries.
Что приходит
POST с телом:
{
"event": "entry.updated",
"projectId": "prj_...",
"environment": "prod",
"occurredAt": "2026-08-27T10:15:00.000Z",
"metadata": { "key": "DB_PASSWORD", "kind": "secret", "version": 4 }
}
Значений секретов в теле нет и не будет. Приходит имя ключа, его тип и номер версии — этого хватает, чтобы решить, надо ли перечитывать конфиг, и не хватает, чтобы утечка в лог приёмника стала утечкой секрета.
Проверка подписи
Заголовок x-signature содержит sha256=<hex> — HMAC-SHA256 от тела
запроса на вашем секрете.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, header, secret) {
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(header ?? "");
const b = Buffer.from(expected);
// Длины сравниваются отдельно: timingSafeEqual бросает на разных длинах.
return a.length === b.length && timingSafeEqual(a, b);
}
Считайте HMAC от сырого тела, до разбора JSON. Пересобранный из объекта JSON почти наверняка отличается пробелами или порядком полей, и подпись не сойдётся.
Сравнивайте за постоянное время. Обычное === на строках выходит раньше
при первом несовпавшем байте, и по времени ответа подпись можно подобрать.
Не проверять подпись — значит принимать такой запрос от кого угодно. Адрес вебхука рано или поздно окажется в логах, в истории браузера или в чужом скриншоте.
События
| Событие | Когда приходит |
|---|---|
entry.created | в окружении появился новый ключ |
entry.updated | значение изменено, включая откат на прошлую версию |
entry.deleted | ключ удалён |
entry.rotated | ротация прошла успешно |
rotation.failed | ротация не удалась; значение осталось прежним |
secret.near-expiry | до истечения секрета осталось мало времени |
enrollment.reuse-detected | enrollment-токен предъявлен второй раз |
validation.failed | окружение не прошло проверку по схеме |
Тип identity.login в списке допустимых событий зарегистрирован, но
сейчас не отправляется — входы identity смотрите в журнале аудита
(identity.login, с флагом newIp на новом адресе). Подписываться на
него бессмысленно, доставок не будет.
Два события стоит подключить, даже если остальные не нужны.
rotation.failed означает, что автоматическая замена не удалась и сервис
работает на старом значении. Без уведомления это выясняется в тот день,
когда старое значение перестанет действовать.
enrollment.reuse-detected означает, что одноразовым токеном
воспользовались дважды. Один из двух — не вы. Сервер в этот момент уже
отказал во втором обмене, но identity сам не отзывает — это ваше
действие: отозвать identity и выпустить новую. Разбор — на странице
Сервисы и identity.
Доставка и повторы
Первая попытка — сразу. При неудаче (сетевой сбой или ответ не 2xx) сервер повторяет с растущими паузами: 1 с, 10 с, 1 мин, 5 мин, всего до пяти попыток.
Отвечайте 2xx как можно раньше, до тяжёлой работы. Приёмник, который
сначала пересобирает контейнер и только потом отвечает, соберёт его пять
раз.
Обработчик должен быть идемпотентным: повтор — это та же доставка, а не новое событие.
Куда можно слать
Только HTTPS. Внутренние, loopback- и link-local-адреса в проде отклоняются: сервер, которому можно указать любой адрес, превращается в сканер вашей сети из чужих рук.
Адрес проверяется по IP после разрешения имени, а не по тексту — иначе
запрет обходился бы доменом, указывающим на 127.0.0.1. Проверка
повторяется перед каждой попыткой доставки, а не только при сохранении:
DNS, поменявшийся после настройки, дверь не открывает.
Общие секреты
Если окружение читает ключи другого проекта по ссылке, изменение общего
значения приходит и на его вебхуки — с полем fromProjectId в метаданных,
чтобы было видно, что ключ пришёл извне.
Если ключ перекрыт своим значением, событие не приходит: для этого окружения ничего не изменилось. Подробнее — в разделе Общие секреты.
Выключить, не удаляя
У вебхука есть переключатель (PATCH /webhooks/:id, поле enabled).
Выключенный сохраняет адрес, секрет и подписки, но не получает доставок —
удобно, когда приёмник на обслуживании и заваливать его повторами незачем.