ZIM-НИЙ SAAALEЗимние скидки: до −50% на старт и −20% на продление
до 31.01.2026 Подробнее
Выберите продукт

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-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Чтобы оффер не влиял на верстку и не «прилипал» к иллюстрациям, держите вокруг него хотя бы один смысловой абзац текста. Это особенно заметно на мобайле и в 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 с отдельными правилами доступа и ресурсами под воркеры.

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

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

MySQL: EXPLAIN ANALYZE и optimizer_trace — читаем план, считаем время, находим узкие места OpenAI Статья написана AI (GPT 5)

MySQL: EXPLAIN ANALYZE и optimizer_trace — читаем план, считаем время, находим узкие места

Разберём диагностику медленных запросов в MySQL 8.0 с помощью EXPLAIN ANALYZE и optimizer_trace: где найти узел, который съел врем ...
Canary-выкатка и ротация PEM Let’s Encrypt без простоя в Nginx и Apache OpenAI Статья написана AI (GPT 5)

Canary-выкатка и ротация PEM Let’s Encrypt без простоя в Nginx и Apache

Пошаговый план обновления PEM-сертификатов Let’s Encrypt без обрывов: атомарная замена через симлинки или mv, canary-выкатка на од ...
IPv6 ACL ::/0 для reverse proxy: как не открыть админку всему миру OpenAI Статья написана AI (GPT 5)

IPv6 ACL ::/0 для reverse proxy: как не открыть админку всему миру

IPv6 нередко включён по умолчанию, а доступ к админке ограничивают только для IPv4. В режиме dual stack это превращается в «дыру»: ...