GitHub webhooks на localhost: подпись SHA-256, ping и повтор доставки
Принимаем GitHub webhook локально в Flask: X-Hub-Signature-256, HTTPS-туннель, события ping/push, Recent deliveries и проверка повторов.

GitHub может отправлять события репозитория прямо на ваш компьютер. Например, вы разрабатываете интеграцию, которая реагирует на push, и хотите проверить обработчик до деплоя. Для этого нужны доступный HTTPS endpoint и проверка подписи каждого запроса.
В этой инструкции Flask принимает ping и push, проверяет X-Hub-Signature-256 и возвращает диагностический ответ. Пример не запускает сборки и не выполняет команды из webhook. Сначала протестируем его искусственным событием, затем подключим собственный тестовый репозиторий.
1. Создайте локальный обработчик
Нужны Python 3.10+, SSH-клиент и права на создание webhook в отдельном тестовом репозитории. Настройте окружение:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install 'Flask>=3.1,<4'
export GITHUB_WEBHOOK_SECRET="$(python -c 'import secrets; print(secrets.token_hex(32))')"
В PowerShell используйте .venv\Scripts\Activate.ps1 и $env:GITHUB_WEBHOOK_SECRET. Secret для webhook вы генерируете сами; personal access token GitHub для приёма webhook не требуется. Сохраните одно и то же значение для запуска обработчика и настройки репозитория.
Создайте app.py:
import hashlib
import hmac
import os
import re
import uuid
from flask import Flask, request
app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024
SECRET = os.environ['GITHUB_WEBHOOK_SECRET']
if not SECRET:
raise ValueError('GITHUB_WEBHOOK_SECRET must not be empty')
def verify_signature(raw_body, signature, secret):
if not isinstance(signature, str) or not re.fullmatch(r'sha256=[0-9a-f]{64}', signature):
return False
expected = 'sha256=' + hmac.new(
secret.encode('utf-8'), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
@app.post('/webhook/github')
def webhook():
raw_body = request.get_data()
if not verify_signature(raw_body, request.headers.get('X-Hub-Signature-256'), SECRET):
return 'Invalid signature', 403
if request.mimetype != 'application/json':
return 'Expected application/json', 415
payload = request.get_json(silent=True)
if not isinstance(payload, dict):
return 'Expected a JSON object', 400
delivery = request.headers.get('X-GitHub-Delivery', '')
try:
uuid.UUID(delivery)
except ValueError:
return 'Invalid delivery ID', 400
event = request.headers.get('X-GitHub-Event', '')
if event == 'ping':
if not isinstance(payload.get('hook_id'), int):
return 'Invalid ping payload', 400
elif event == 'push':
if (not isinstance(payload.get('ref'), str)
or not isinstance(payload.get('repository'), dict)):
return 'Invalid push payload', 400
else:
return {'ignored': True}, 202
# Учебный endpoint: без запуска команд и изменения данных репозитория.
return {'received': event, 'delivery': delivery}, 200
if __name__ == '__main__':
app.run(host='127.0.0.1', port=8000, debug=False)
Подпись вычисляется от исходных байтов тела, а не от повторно сериализованного JSON. Пробелы и UTF-8 влияют на HMAC. Проверка через compare_digest и SHA-256 описана в официальной инструкции GitHub. Учебный сервер ограничивает тело одним мегабайтом; для больших push-событий потребуется отдельная политика размера и обработки.
Запустите python app.py.
2. Проверьте обработчик без GitHub
В другом терминале с тем же GITHUB_WEBHOOK_SECRET задайте тестовое тело и подпись:
export GITHUB_BODY='{"hook_id":12345,"zen":"local test"}'
SIGNATURE=$(python -c 'import os,hmac,hashlib; print("sha256="+hmac.new(os.environ["GITHUB_WEBHOOK_SECRET"].encode(), os.environ["GITHUB_BODY"].encode(), hashlib.sha256).hexdigest())')
curl -i http://127.0.0.1:8000/webhook/github \
-H 'Content-Type: application/json' \
-H 'X-GitHub-Event: ping' \
-H 'X-GitHub-Delivery: 11111111-2222-4333-8444-555555555555' \
-H "X-Hub-Signature-256: ${SIGNATURE}" \
--data-raw "$GITHUB_BODY"
Ожидаются HTTP 200, received: ping и тот же delivery ID. Удалите подпись — получите 403. Измените тело, сохранив прежнюю подпись, — также 403. Это локальная проверка валидации; она не подтверждает настройку webhook в репозитории.
3. Создайте HTTPS-адрес
Для Free добавьте публичный SSH-ключ. Затем в новом терминале:
ssh -p 2222 -o ExitOnForwardFailure=yes \
-R 80:127.0.0.1:8000 tunnelhunt.ru
Возьмите выданный HTTPS-origin и повторите предыдущий запрос через https://YOUR-SUBDOMAIN.tunnelhunt.ru/webhook/github. Ожидается тот же ответ. Если локально работает, а снаружи нет, проверьте порт и соединение по инструкции localhost.
4. Настройте webhook тестового репозитория
В Settings → Webhooks своего тестового репозитория добавьте webhook:
- Payload URL: полный HTTPS URL
/webhook/githubиз предыдущего шага. - Content type:
application/json. - Secret: то же значение, с которым запущен Flask.
- Events: для этого примера достаточно push.
- SSL verification: включена; webhook активен.
Создание webhook требует подходящих прав; шаги описаны в документации GitHub. После сохранения GitHub отправляет ping. В Recent deliveries откройте событие и проверьте ответ своего обработчика. Затем сделайте тестовый push в собственный репозиторий и найдите доставку push с HTTP 200 и received: push.
Не передавайте содержимое webhook в shell или eval: подписанный запрос всё равно содержит данные коммитов и пользователей, которые нельзя исполнять как код. Для реальной интеграции проверяйте ожидаемый репозиторий, доступы и поля конкретного события.
Повтор: GitHub Redeliver и TunnelHunt Replay
GitHub позволяет повторно отправить доставку из интерфейса репозитория. В TunnelHunt можно найти входящий запрос в Logs, изменить локальный код и нажать Replay. Пока секрет тот же и тело сохранено, подпись должна пройти снова. После смены секрета старые подписанные доставки могут перестать проходить проверку.
Повтор с прежним X-GitHub-Delivery не должен создавать вторую бизнес-операцию. Для рабочего обработчика храните обработанные delivery ID и результат атомарно в БД или надёжно ставьте событие в очередь, прежде чем подтвердить приём. Ошибка после постановки в очередь не должна запускать работу дважды. Учебный пример ничего не меняет, поэтому повтор просто возвращает диагностический ответ.
HMAC защищает тело запроса; заголовки event и delivery не входят в вычисляемую подпись. Сопоставляйте тип события со схемой payload и своими правилами, а не считайте эти заголовки отдельной авторизацией. Ограничения и обработка доставок описаны в рекомендациях GitHub.
Диагностика
| Симптом | Что проверить |
|---|---|
| 403 | Один secret у GitHub и Flask, заголовок X-Hub-Signature-256, исходные байты тела |
| 415 | Content type в настройках webhook — application/json |
| 400 | JSON-объект, delivery ID и поля нужного события |
| 202 | Пришло событие, которое этот пример намеренно не обрабатывает |
| 413 | Тело больше учебного лимита 1 МБ |
| 502 | Flask и SSH работают, локальный порт совпадает |
| События не появляются | Webhook активен, выбрано нужное событие, проверьте Recent deliveries |
| URL работал до переподключения | Случайный поддомен изменился; обновите Payload URL или используйте резервирование |
Для повторных сессий разработки закрепите имя на Pro и подключайтесь привязанным SSH-ключом. Для восстановления соединения есть autossh, но компьютер и приложение должны оставаться включёнными. После теста отключите webhook в тестовом репозитории и остановите SSH.
Получите GitHub webhook на localhost
Проверьте искусственный ping локально и по HTTPS, затем подключите отдельный тестовый репозиторий. Постоянный поддомен Pro поможет сохранить Payload URL при новых SSH-сессиях.
Первый запуск: добавьте публичный SSH-ключ, запустите локальное приложение и команду SSH из статьи, затем откройте выданный HTTPS-адрес. Если ответа нет, пройдите проверку localhost и туннеля.
Free: 1 ГБ трафика в месяц, 1 туннель, сессия до 4 часов. Pro: 199 ₽/месяц, 50 ГБ, до 5 туннелей и резервирование поддомена. Компьютер, приложение и SSH-соединение должны работать; постоянный адрес сохраняет имя, а не доступность выключенного сервера.
Готовы запустить свой первый туннель?
Начните использовать TunnelHunt бесплатно уже сегодня и откройте доступ к локальному серверу всего одной командой.
Зарегистрироваться бесплатно →По теме

Как повторить вебхук: отладка HTTP-запросов через Replay
20 июня 2026 г.
Получите тестовый вебхук, найдите ошибку и повторите запрос после исправления. Рабочий пример на Python, диагностика подписей и ограничения Replay.

Как открыть localhost в интернете: публичный HTTPS через SSH
11 сентября 2026 г.
Пошагово открываем локальный сайт из интернета без настройки роутера: SSH-туннель, проверка ссылки, Docker, Vite и диагностика ошибок.

autossh: постоянный SSH-туннель, переподключение и systemd
16 июня 2026 г.
Настройка autossh на Linux и macOS: переподключение SSH, постоянный адрес по ключу, автозапуск через systemd и проверка HTTP после сбоя.

Telegram-бот локально: webhook, secret_token и отладка на localhost
5 июня 2025 г.
Запустите Telegram-бота на localhost: выберите polling или webhook, настройте HTTPS и secret_token, проверьте getWebhookInfo и исправьте ошибки доставки.