Из-за временных ограничений на территории РФ наблюдаются проблемы с оплатой. Если платёж не проходит, оставьте запрос в службу поддержки.Служба поддержки работает 24/7 — мы всегда на связи по вопросам хостинга и серверов.Открыт прием заявок на аренду выделенных серверов и размещение оборудования в дата-центре.Напоминаем: рекомендуем включить резервное копирование для дополнительной защиты данных.Доступна новая линейка VPS/VDS с NVMe-дисками и увеличенной производительностью.Технические работы на части серверов завершены. Все сервисы работают в штатном режиме.
Статья8 мин чтенияПросмотры0

Ротация секрета webhook: как сменить ключ без остановки приёма

Практический guide: принимаем K1 и K2 только в ограниченном окне, связываем подпись с key_id и timestamp, проверяем HMAC через compare_digest и отключаем старый ключ по результатам семи тестов.

Комментарии 0

Механизм проверки webhook с двумя ключами для контролируемой ротации секрета
В этой статье

Ротацию секрета webhook безопаснее проводить через ограниченное окно перекрытия: новый ключ K2 начинает действовать в момент t1, старый K1 перестаёт приниматься в t2, а проверяющий выбирает ключ по key_id и отдельно контролирует время подписи. Постоянно принимать оба секрета нельзя: тогда старый ключ формально заменён, но фактически всё ещё открывает вход.

Ниже — учебная модель HMAC-SHA256 для получателя webhook. Я проверил её 23 октября 2026 года на CPython 3.12.14 без сети и внешних пакетов. Это не готовая схема конкретного сервиса или модуля 1С-Битрикс: формат заголовков, повторные доставки и допустимое окно нужно взять из контракта отправителя.

Что подтверждает HMAC

HMAC связывает сообщение с общим секретом и позволяет проверить целостность и подлинность в пределах этого секрета. Конструкция описана в RFC 2104. Сам стандарт рекомендует периодически обновлять ключи, но не задаёт универсальную длительность перекрытия.

Подпись не шифрует тело, не отменяет TLS и не доказывает, что бизнес-событие нужно применить повторно. Сначала проверяют подпись и её время, затем — идентификатор события, состояние заказа и правила идемпотентности.

Входной контракт учебной модели

Поле

Что означает

Что отклоняем

key_id

Идентификатор настроенного секрета K1 или K2

Неизвестный идентификатор

timestamp

Время, включённое в подписываемую строку

Слишком старое или заметно будущее

signature

HMAC-SHA256 в hex

Неверная длина или несовпадение

raw_body

Исходные байты тела

Изменённые байты

Строка подписи в модели состоит из ASCII timestamp, точки и исходных байтов тела. Преобразованный JSON уже не подходит: даже без изменения смысла порядок ключей или пробелы меняют байты.

signed = str(timestamp).encode("ascii") + b"." + body

Два ключа должны иметь разные окна

K1 действует до t2, K2 — начиная с t1. Между t1 и t2 оба ключа допустимы: это окно перекрытия для выкладки получателя, переключения отправителя и проверяемого отката. После t2 подпись K1 отклоняется даже тогда, когда математически верна.

Интервал

K1

K2

Назначение

t0 ≤ t < t1

Принимать

Отклонять

До переключения

t1 ≤ t < t2

Принимать

Принимать

Контролируемое перекрытие

t2 ≤ t < t3

Отклонять

Принимать

Старый ключ выключен

Учебная временная шкала ротации: K1 действует до t2, K2 — начиная с t1, между t1 и t2 оба ключа допустимы
Учебная модель: K1 и K2 перекрываются только между t1 и t2; после t2 старый ключ больше не принимается.

В стандарте HTTP Message Signatures предусмотрены метаданные keyid, created и expires. Это полезный ориентир для разделения идентификатора ключа и времени подписи, но он не означает, что произвольный webhook реализует RFC 9421: фактический формат задаёт поставщик.

Проверяющий выбирает ключ, а не перебирает историю

def verify(*, key_id, signature_hex, timestamp, body, received_at, keys): key = keys.get(key_id) if key is None: return "unknown_key" signed_at = datetime.fromtimestamp(timestamp, tz=timezone.utc) if signed_at > received_at + timedelta(seconds=30): return "timestamp_in_future" if received_at - signed_at > timedelta(minutes=5): return "stale_timestamp" if not (key.not_before <= received_at < key.not_after): return "key_outside_window" expected = sign(key.secret, timestamp, body) if not hmac.compare_digest(expected, signature_hex): return "bad_signature" return "accepted"

Для сравнения использована hmac.compare_digest(): документация Python рекомендует её вместо оператора равенства, чтобы уменьшить риск анализа времени сравнения. Секрет и подпись в журнал не записываются; достаточно key_id, результата и идентификатора запроса.

Проверка времени сделана до HMAC. Так просроченное сообщение не получает статус «подпись верна» и не попадает в бизнес-обработку. Допуск 30 секунд в будущее и максимум 5 минут — параметры учебной модели, а не универсальные значения.

Семь воспроизводимых проверок

Следующая команда для Python 3.12 запускает полный стенд в памяти: файлы и сетевые обращения не используются; ключи учебные.

python3 - <<'PY' from dataclasses import dataclass from datetime import datetime, timedelta, timezone import hashlib import hmac @dataclass(frozen=True) class Key: secret: bytes not_before: datetime not_after: datetime def sign(secret, timestamp, body): signed = str(timestamp).encode("ascii") + b"." + body return hmac.new(secret, signed, hashlib.sha256).hexdigest() def verify(*, key_id, signature_hex, timestamp, body, received_at, keys): key = keys.get(key_id) if key is None: return "unknown_key" signed_at = datetime.fromtimestamp(timestamp, tz=timezone.utc) if signed_at > received_at + timedelta(seconds=30): return "timestamp_in_future" if received_at - signed_at > timedelta(minutes=5): return "stale_timestamp" if not (key.not_before <= received_at < key.not_after): return "key_outside_window" expected = sign(key.secret, timestamp, body) if not hmac.compare_digest(expected, signature_hex): return "bad_signature" return "accepted" t0 = datetime(2026, 10, 23, 7, 0, tzinfo=timezone.utc) t1 = t0 + timedelta(minutes=10) t2 = t0 + timedelta(minutes=20) t3 = t0 + timedelta(minutes=30) keys = { "K1": Key(b"1" * 32, t0, t2), "K2": Key(b"2" * 32, t1, t3), } body = b'{"event_id":"lab-1","order_id":"lab-order"}' before = t0 + timedelta(minutes=5) during = t0 + timedelta(minutes=15) after = t0 + timedelta(minutes=21) def check(name, key_id, signed_at, received_at, expected, *, changed=False): timestamp = int(signed_at.timestamp()) signing_key = keys["K2"] if key_id == "K2" else keys["K1"] signature = sign(signing_key.secret, timestamp, body) result = verify( key_id=key_id, signature_hex=signature, timestamp=timestamp, body=body + b" " if changed else body, received_at=received_at, keys=keys, ) if result != expected: raise AssertionError((name, expected, result)) print(f"{name}: {result}") check("old_before_overlap", "K1", before, before, "accepted") check("old_during_overlap", "K1", during, during, "accepted") check("new_during_overlap", "K2", during, during, "accepted") check("old_after_cutoff", "K1", after, after, "key_outside_window") check("tampered_body", "K1", during, during, "bad_signature", changed=True) check("stale_message", "K1", during - timedelta(minutes=6), during, "stale_timestamp") check("unknown_key", "K3", during, during, "unknown_key") print("all 7 checks passed") PY

old_before_overlap: accepted old_during_overlap: accepted new_during_overlap: accepted old_after_cutoff: key_outside_window tampered_body: bad_signature stale_message: stale_timestamp unknown_key: unknown_key all 7 checks passed

Сценарий

Ожидание

Наблюдение

K1 до перекрытия

Принять

accepted

K1 в перекрытии

Принять

accepted

K2 в перекрытии

Принять

accepted

K1 после t2

Отклонить

key_outside_window

Изменённое тело

Отклонить

bad_signature

Сообщение старше 5 минут

Отклонить

stale_timestamp

Неизвестный key_id

Отклонить

unknown_key

Скрипт завершился с кодом 0 и сравнил все семь строк с ожидаемым списком. Он подтверждает только логику выбора ключа, окна и HMAC на синтетических данных. Сеть, прокси, хранилище секретов, конкретный поставщик и параллельная обработка здесь не проверялись.

Порядок ротации без героизма

  1. Зафиксировать формат подписи, продолжительность повторных доставок, допустимое расхождение часов и способ указать key_id у конкретного отправителя.

  2. Создать K2 в штатном хранилище секретов. Не копировать его в тикет, код, образ контейнера или журнал.

  3. Развернуть получатель с K1 и K2, но разрешить K2 только с t1, а K1 — только до t2.

  4. В t1 переключить отправителя на K2 и проверить метрики accepted по key_id, bad_signature, unknown_key и задержку timestamp без сохранения подписи.

  5. До t2 выполнить контролируемый откат отправителя на K1 и обратно, если контракт и среда позволяют безопасную проверку.

  6. В t2 прекратить принимать K1. После согласованного срока диагностики удалить старый секрет из активной конфигурации и проверить, что возврат старой версии не включит его снова.

Как выбрать окно перекрытия

  • Окно должно покрывать штатную выкладку и документированный способ отката.

  • Если поставщик повторяет старые доставки дольше окна, заранее решите, как они будут классифицированы после t2; молчаливое продление K1 отменяет ротацию.

  • Если key_id нет, используйте только явно заданный небольшой набор текущих ключей и удаляйте K1 по времени. Не перебирайте архив секретов.

  • Проверяйте синхронизацию часов и записывайте signed_at и received_at без тела и секретов.

  • При подозрении на компрометацию обычное окно может быть неприемлемо: отключение ключа и разбор последствий важнее бесшовности.

Подпись и повтор события — разные проверки

Статья «Подпись webhook не сходится: проверьте исходные байты JSON» подробно разбирает, почему HMAC считают по исходному телу. Здесь другой результат: управляемый переход между двумя секретами.

После принятия подписи остаётся бизнес-повтор. В guide «Повторный webhook не должен создать второй заказ» показаны UNIQUE-ограничение, транзакция и повтор после сбоя. Верная подпись не заменяет эти меры.

Критерий готовности

Ротация готова, когда команда может показать четыре факта: K2 принимается после t1, K1 принимается только до t2, изменённое или просроченное сообщение отклоняется, а возврат старой версии приложения не включает K1 после отсечки. Один удачный webhook в окне перекрытия доказывает слишком мало.

Обсуждение 0

Делись опытом и задавай вопросы. Комментарии без ссылок появляются после проверки редактором.

Пока никто не написал. Начни обсуждение.