Новинка Виртуальный VDS сервер в Финляндии от 390р
Выберите продукт

GitHub/GitLab webhooks: подпись, повторы и идемпотентная обработка

Разбираем, как принимать GitHub/GitLab webhooks в продакшене: проверять подпись (HMAC) или токен до парсинга JSON, учитывать retries и дубли, строить idempotency key, внедрять inbox-таблицу с уникальным индексом, защищаться от гонок воркеров и настраивать метрики и логи.
GitHub/GitLab webhooks: подпись, повторы и идемпотентная обработка

Вебхуки 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. Но помните: это почти гарантированно спровоцирует повторы.
FastFox VDS
Облачный VDS-сервер
Виртуальные серверы с быстрым запуском и гибкой конфигурацией от 390₽ / мес
Доступные локации
Россия Нидерланды Финляндия

Чтобы оффер не влиял на верстку и не «прилипал» к иллюстрациям, держите вокруг него хотя бы один смысловой абзац текста. Это особенно заметно на мобайле и в AMP-подобных шаблонах.

Проверка заголовков вебхука и вычисленной HMAC-подписи

Идемпотентность: как гарантировать «один эффект» при нескольких доставках

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) + асинхронная обработка

Самый надежный подход для продакшена:

  1. На входе проверили подпись/токен.
  2. Сформировали idempotency key.
  3. Попытались записать событие в таблицу webhook_inbox с уникальным индексом по ключу.
  4. Если вставка прошла — вернули 202/200 и отдали задачу воркеру.
  5. Если уникальный индекс сработал (событие уже есть) — вернули 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, отдельно продумайте версионирование и «последнее состояние», чтобы старое событие не откатывало изменения. По теме полезно: версионирование кэша и инвалидация статики.

Схема inbox-таблицы для дедупликации вебхуков и асинхронной обработки

Обработка гонок и параллелизма в воркерах

Даже с уникальным индексом возможны гонки уже на этапе выполнения бизнес-операций (например, два воркера одновременно взяли задачу). Практика:

  • Используйте «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-сертификат под нужный домен.

FastFox SSL
Надежные SSL-сертификаты
Мы предлагаем широкий спектр SSL-сертификатов от GlobalSign по самым низким ценам. Поможем с покупкой и установкой SSL бесплатно!

Мини-чеклист «прод-готового» webhook endpoint

  • HTTPS обязателен; секреты не в репозитории, а в переменных окружения или secret-хранилище.
  • Валидация подписи (GitHub HMAC SHA256) или токена (GitLab) до любой обработки.
  • Быстрый ответ 2xx после записи события в inbox (или очередь).
  • Idempotency key на бизнес-результат, уникальный индекс для дедупликации.
  • Воркеры с безопасным «claim» и повторной обработкой ошибок.
  • Метрики и логи с delivery id/uuid и idempotency key.
  • Ретеншн/архивация для входящих событий.

Что делать, если вебхуки уже есть, а там хаос

Если обработчик живет давно, но вы ловите дубли и падения — двигайтесь по шагам:

  1. Добавьте проверку подписи/токена (даже до большого рефакторинга).
  2. Вынесите тяжелую работу в фон: синхронно делайте только прием + запись события.
  3. Внедрите inbox с уникальным ключом и начните логировать idempotency key.
  4. Постепенно делайте идемпотентными самые болезненные операции (деплой, создание сущностей, интеграции с внешними API).

После этого GitHub/GitLab webhooks станут не «магией на удачу», а обычным надежным транспортом событий, который не страшно масштабировать и сопровождать.

Если такой endpoint крутится на отдельном сервисе, часто удобнее выделить его на отдельную VM/контейнер с предсказуемыми лимитами и логированием. Для этого обычно подходит VDS с отдельными правилами доступа и ресурсами под воркеры.

Поделиться статьей

Вам будет интересно

Как безопасно выполнить bootstrap MariaDB Galera после полного останова

Как безопасно выполнить bootstrap MariaDB Galera после полного останова

После одновременной остановки всех узлов Galera обычного запуска MariaDB недостаточно. Сначала найдите сервер с последней транзакц ...
CoreDNS в Kubernetes: исправляем CrashLoopBackOff, SERVFAIL и DNS-петлю

CoreDNS в Kubernetes: исправляем CrashLoopBackOff, SERVFAIL и DNS-петлю

Разберём, почему CoreDNS перезапускается или отвечает SERVFAIL, как обнаружить замкнутую пересылку DNS-запросов, проверить доступн ...
Apache Kafka KRaft на VDS: отдельные controller и broker

Apache Kafka KRaft на VDS: отдельные controller и broker

Соберём новый Kafka-кластер без ZooKeeper на шести Linux VDS: три узла controller образуют отказоустойчивый metadata quorum, а три ...