Вебхуки GitHub и GitLab — удобный способ «толкать» события (push, merge request, release) в вашу инфраструктуру: деплой, сборки, инвалидация кэша, синхронизация зеркал. Но как только вебхук становится частью продакшена, выясняется: один и тот же запрос может прийти несколько раз, может прийти с задержкой, может быть подделан, а обработчик может упасть на середине.
Ниже — практическая схема «как делать правильно»: проверка подписи (hmac verification) или токена, повторы (webhook retries), построение idempotency key и транзакционная webhook deduplication. Результат — endpoint, который безопасно держать в проде: он предсказуем, наблюдаем и не плодит побочные эффекты на дублях.
Модель угроз: почему «просто принять POST» недостаточно
Типовые проблемы, которые всплывают у любого обработчика github webhook или gitlab webhook:
- Подделка запросов. Кто угодно может отправить POST на ваш URL, если вы не проверяете подпись/токен.
- Повторы и дубли. Платформа делает ретраи при таймаутах/ошибках, а сеть иногда приносит дубликаты.
- Неатомарная обработка. Вы запустили деплой/таску и упали на середине — повтор запроса запускает всё ещё раз.
- Задержки и «старые» события. Событие может прийти позже ожидаемого и «переиграть» состояние.
Главная мысль: вебхук — это сообщение в ненадежной сети. Значит, вход должен быть аутентифицирован (подпись/токен), а обработка — идемпотентной и умеющей дедуплицировать события.
Подпись вебхука: что именно проверять в GitHub и GitLab
Проверка подписи — базовая линия защиты. У GitHub это HMAC от тела запроса, у GitLab чаще всего — «секретный токен» в заголовке. В обоих случаях проверка должна идти до JSON-парсинга и до любых побочных действий.
GitHub: заголовки подписи и нюансы HMAC
У GitHub в проде встречаются два заголовка:
X-Hub-Signature— HMAC SHA1 (исторически).X-Hub-Signature-256— HMAC SHA256 (рекомендуется).
Практика: принимайте SHA256 как основной вариант, SHA1 держите только для обратной совместимости. Сравнение подписи делайте в константное время (например, hmac.compare_digest), чтобы не подставляться под тайминговые атаки.
GitLab: X-Gitlab-Token и что он защищает
Самый распространенный вариант в GitLab — заголовок X-Gitlab-Token: сервер сравнивает переданный токен с ожидаемым. Это не HMAC от тела запроса, а просто секрет в заголовке.
Токен в заголовке криптографически слабее HMAC от body: он не связывает секрет с конкретным payload. Но при обязательном HTTPS, жесткой проверке токена и ограничениях доступа к endpoint это уже на порядок лучше «голого» URL.
Критические требования к реализации проверки
- Считывайте сырой body как байты. Нельзя «распарсить JSON и пересериализовать» — подпись считается по оригинальному телу.
- Проверяйте
Content-Type. Разрешайте только то, что поддерживаете (обычноapplication/json). - Constant-time compare. Только функции, устойчивые к тайминговым атакам.
- Секреты не логировать. Не пишите secret/токены и полные подписи в прод-логи.
Если endpoint выносится в интернет, держите его под TLS и не экономьте на инфраструктуре: корректно настроенный сертификат — обязательное условие. При необходимости можно закрыть вопрос через SSL-сертификаты.
Пример HMAC verification (Python) для GitHub
import hmac
import hashlib
def verify_github_signature(raw_body: bytes, secret: str, signature_header: str) -> bool:
if not signature_header:
return False
# GitHub присылает строку вида: "sha256=...hex..."
if not signature_header.startswith("sha256="):
return False
sent = signature_header.split("=", 1)[1].strip()
mac = hmac.new(secret.encode("utf-8"), msg=raw_body, digestmod=hashlib.sha256)
expected = mac.hexdigest()
return hmac.compare_digest(sent, expected)
Пример проверки X-Gitlab-Token (Python) для GitLab
import hmac
def verify_gitlab_token(sent_token: str, expected_token: str) -> bool:
if not sent_token:
return False
return hmac.compare_digest(sent_token, expected_token)
Если у вас есть прокси/балансировщик перед приложением, дополнительно проверьте, что он не меняет тело запроса и корректно пробрасывает заголовки. Отдельно разобрали частую ловушку с нормализацией тела и защитой от повторов в статье HMAC, защита от replay и влияние прокси на вебхуки.
Webhook retries: почему повторы — это норма, а не исключение
webhook retries появляются по нескольким причинам:
- Ваш сервис ответил 5xx.
- Ваш сервис не ответил достаточно быстро (таймаут у GitHub/GitLab или на прокси).
- Сетевой сбой между платформой и вашим endpoint.
Ключевой момент: ретрай не означает, что предыдущая попытка «ничего не сделала». Очень часто обработчик успевает выполнить действие, но ответ не дошел — и платформа повторяет доставку. Поэтому идемпотентность и дедуп нужны всегда, даже если «мы же отвечаем 200».
Как отвечать, чтобы минимизировать ретраи
- Отвечайте быстро. Тяжелые задачи уносите в очередь/фон.
- На валидные запросы отдавайте 2xx. Даже если обработка будет асинхронной (часто удобно отвечать
202). - На невалидные/неавторизованные — 401/403. Это «жесткое нет» без лишней нагрузки.
- На перегрузку — 429 или 503. Но помните: это почти гарантированно спровоцирует повторы.
Чтобы оффер не влиял на верстку и не «прилипал» к иллюстрациям, держите вокруг него хотя бы один смысловой абзац текста. Это особенно заметно на мобайле и в AMP-подобных шаблонах.

Идемпотентность: как гарантировать «один эффект» при нескольких доставках
idempotency key — это ключ, который описывает событие так, чтобы повторная обработка не выполняла побочные эффекты повторно. Для вебхуков идемпотентность обычно строится на комбинации:
- уникального ID доставки/события (если есть),
- типа события,
- идентификатора репозитория/проекта,
- «бизнес-ключа» (commit SHA, tag, MR IID и т. п.).
Где взять уникальный идентификатор события
У GitHub часто используют X-GitHub-Delivery как уникальный ID доставки. У GitLab в новых версиях встречается X-Gitlab-Event-UUID; если его нет, опирайтесь на сочетания полей payload (например, project_id + object_attributes.id + object_attributes.updated_at), но учитывайте, что это уже компромисс.
Если «идеального» ID нет, можно сделать свой: посчитать хеш от (тип события + важные поля payload). Но имейте в виду: при изменении формата payload такой ключ может поменяться, а значит, дедуп на длительном горизонте будет хуже.
Практический паттерн: inbox-таблица (dedup) + асинхронная обработка
Самый надежный подход для продакшена:
- На входе проверили подпись/токен.
- Сформировали idempotency key.
- Попытались записать событие в таблицу
webhook_inboxс уникальным индексом по ключу. - Если вставка прошла — вернули 202/200 и отдали задачу воркеру.
- Если уникальный индекс сработал (событие уже есть) — вернули 200 и ничего не делаем (либо обновляем метаданные доставки).
Это и есть webhook deduplication в «железобетонном» варианте: дедуп делается транзакционно на уровне базы.
Мини-схема таблицы для дедупликации
-- Псевдо-SQL, адаптируйте под вашу СУБД
CREATE TABLE webhook_inbox (
id BIGSERIAL PRIMARY KEY,
provider VARCHAR(16) NOT NULL,
event_type VARCHAR(64) NOT NULL,
idempotency_key VARCHAR(128) NOT NULL,
delivery_id VARCHAR(128) NULL,
received_at TIMESTAMP NOT NULL,
payload_sha256 CHAR(64) NOT NULL,
status VARCHAR(16) NOT NULL,
processed_at TIMESTAMP NULL,
last_error TEXT NULL
);
CREATE UNIQUE INDEX webhook_inbox_uniq ON webhook_inbox(provider, idempotency_key);
Зачем payload_sha256: удобно для диагностики (понимать, одинаковый ли payload пришел повторно), не храня при этом весь JSON бесконечно. Полный payload можно хранить отдельно (например, в объектном хранилище) или в этой же таблице, если объем небольшой и есть политика ретеншна.
Дедупликация vs идемпотентность: в чем разница на практике
Термины часто смешивают, но полезно разделять:
- Webhook deduplication — мы не запускаем обработку одного и того же события дважды (обычно на входе).
- Идемпотентность — даже если обработка запустилась дважды, побочный эффект (например, «создать релиз», «создать запись в БД», «запустить деплой») выполнится один раз или будет безопасно повторен.
В идеале делаем и то и другое: дедуп на входе защищает от лавины ретраев, а идемпотентность на бизнес-операциях страхует от гонок, ручных повторов и редких дублей.
Как проектировать idempotency key для типовых сценариев
Ниже — рабочие идеи. Универсального ключа нет, но принцип один: ключ должен отражать именно бизнес-результат, а не «попытку доставки».
Деплой по push в main
- Бизнес-ключ:
repo_id + ref + after_commit_sha. - Почему: один и тот же commit деплоить дважды обычно не нужно, повтор должен быть безопасен.
Деплой по tag/release
- Бизнес-ключ:
repo_id + tag_name(илиrelease_id). - Нюанс: теги иногда «перетирают» (force). Решите заранее, вы разрешаете повторный деплой при смене target commit или блокируете такие кейсы.
Автоматизация вокруг Merge Request / Pull Request
- Бизнес-ключ:
project_id + mr_iid + action + state_version. - Нюанс: событие «updated» может приходить часто. Иногда выгоднее обрабатывать «последнее состояние» (state-based), а не каждое изменение.
Если вебхуком вы триггерите операции по кэшу или CDN, отдельно продумайте версионирование и «последнее состояние», чтобы старое событие не откатывало изменения. По теме полезно: версионирование кэша и инвалидация статики.

Обработка гонок и параллелизма в воркерах
Даже с уникальным индексом возможны гонки уже на этапе выполнения бизнес-операций (например, два воркера одновременно взяли задачу). Практика:
- Используйте «claim» механизмы:
SELECT ... FOR UPDATE SKIP LOCKEDили атомарныйUPDATEпо статусу (в рамках транзакции), чтобы одну задачу забрал один воркер. - Для внешних API-операций храните «след»: внешний ID, статус, время, чтобы повтор не создавал дубликаты.
- Старайтесь делать операции идемпотентными на стороне вашей БД: upsert по уникальному бизнес-ключу, уникальные индексы, проверяемые инварианты.
Наблюдаемость: что логировать и какие метрики нужны
Продовые вебхуки без наблюдаемости быстро превращаются в «черный ящик». Минимальный набор:
- Корреляция: логируйте delivery id/uuid, event type, idempotency key.
- Времена: время приема, время постановки в очередь, время обработки.
- Счетчики: принятые, отклоненные по подписи, дубликаты, успешные, ошибки.
- Размер payload: полезно для выявления аномалий и DoS-паттернов.
Хорошее правило: по одному событию вы должны быстро ответить на вопросы «оно дошло?», «оно дубль?», «оно в очереди или обработано?», «почему упало?».
Частые ошибки и как их избежать
- Подпись проверяют после JSON-parse. Проверять нужно по сырому body.
- Секрет один на все проекты без ротации. Делайте возможность ротировать secret (например, проверка по текущему и предыдущему секрету в период миграции).
- Делают тяжелую работу синхронно. Из-за таймаутов начинаются ретраи и лавина дублей.
- Дедуп по timestamp. Ненадежно: время может совпадать, а события — разные.
- Нет политики хранения inbox. Таблица растет бесконечно. Нужна ретеншн-очистка (например, хранить 7–30 дней, дольше — только агрегаты/трассировку).
Если endpoint торчит в интернет и его трогают внешние системы, TLS и корректная цепочка сертификатов — не формальность. Быстрый способ закрыть вопрос с выпуском и продлением — купить SSL-сертификат под нужный домен.
Мини-чеклист «прод-готового» webhook endpoint
- HTTPS обязателен; секреты не в репозитории, а в переменных окружения или secret-хранилище.
- Валидация подписи (GitHub HMAC SHA256) или токена (GitLab) до любой обработки.
- Быстрый ответ 2xx после записи события в inbox (или очередь).
- Idempotency key на бизнес-результат, уникальный индекс для дедупликации.
- Воркеры с безопасным «claim» и повторной обработкой ошибок.
- Метрики и логи с delivery id/uuid и idempotency key.
- Ретеншн/архивация для входящих событий.
Что делать, если вебхуки уже есть, а там хаос
Если обработчик живет давно, но вы ловите дубли и падения — двигайтесь по шагам:
- Добавьте проверку подписи/токена (даже до большого рефакторинга).
- Вынесите тяжелую работу в фон: синхронно делайте только прием + запись события.
- Внедрите inbox с уникальным ключом и начните логировать idempotency key.
- Постепенно делайте идемпотентными самые болезненные операции (деплой, создание сущностей, интеграции с внешними API).
После этого GitHub/GitLab webhooks станут не «магией на удачу», а обычным надежным транспортом событий, который не страшно масштабировать и сопровождать.
Если такой endpoint крутится на отдельном сервисе, часто удобнее выделить его на отдельную VM/контейнер с предсказуемыми лимитами и логированием. Для этого обычно подходит VDS с отдельными правилами доступа и ресурсами под воркеры.


