Вебхук Telegram-бота: проверка secret_token до очереди и CRM

10 мин чтения

Как отклонять запросы с неверным webhook secret_token, проверить прокси и спланировать ротацию. Локальный Python-тест без бота и CRM.

Вебхук Telegram-бота: проверка secret_token до очереди и CRM

Проверяйте X-Telegram-Bot-Api-Secret-Token до разбора бизнес-события и записи задания в очередь. Ожидаемое значение задаётся на вашей стороне и передаётся Telegram при регистрации вебхука через secret_token. Запрос без подходящего значения не должен запускать квалификацию, создавать лид или менять сделку. Однако совпадение секрета не подтверждает права пользователя и не защищает от повторной доставки.

Ниже разобрана входная граница собственной интеграции Telegram Bot API → обработчик → очередь → CRM. Понадобятся доступ к коду приёмника, его настройкам и тестовому окружению. Это не инструкция по настройке проверенной встроенной функции 24go.site. Пример работает локально с вымышленными значениями, не обращается к Telegram и не читает настоящие секреты. Он проверяет решение приёмника, а не безопасность развёрнутого сервиса целиком.

Какую проверку даёт secret_token

В описании setWebhook Telegram документирует необязательный secret_token: строку длиной от 1 до 256 символов из A-Z, a-z, 0-9, _ и -. Если параметр задан, Telegram включает его в заголовок X-Telegram-Bot-Api-Secret-Token каждого webhook-запроса. Передать параметр при регистрации недостаточно: ваш приёмник должен действительно проверять заголовок.

Это общий секрет между отправителем и приёмником, а не подпись содержимого сообщения. В указанном контракте нет подписанного времени или одноразового nonce. Следовательно, тот, кто получил значение, может предъявить его повторно; сам заголовок не доказывает свежесть JSON и не заменяет защиту ключей. Защищайте соединения и не записывайте значение в журналы, трассировки, скриншоты или сообщения об ошибках.

Не путайте webhook secret с токеном бота. Токен бота используется для доступа к Bot API; здесь обсуждается отдельное значение для входящих запросов. Разделение позволяет менять входной секрет, не раскрывая намеренно токен бота. Но утечку токена бота нужно разбирать отдельно: замена только webhook secret не устраняет все последствия компрометации доступа к API.

Успешная проверка отвечает на узкий вопрос: предъявлен ли секрет, который принимает этот endpoint? Она не отвечает на вопрос, имеет ли конкретный Telegram-пользователь право менять данную сделку. После приёма события всё ещё нужно проверить отправителя, его доступ к объекту и допустимость действия. Нельзя считать идентификатор CRM из входящего JSON достаточным разрешением на изменение записи.

Поставьте отказ перед побочным эффектом

Задайте последовательность обработки так, чтобы отказ не успевал создать работу:

  1. На внешнем слое ограничьте размер запроса и время чтения. Заголовок можно проверить до прикладного разбора JSON, но HTTP-сервер всё равно может буферизовать тело. Ограничения на сервере и прокси остаются необходимыми.
  2. Убедитесь, что метод и маршрут соответствуют вашему webhook endpoint. Не перенаправляйте запрос с секретом на произвольный адрес и не создавайте запасной маршрут без той же проверки.
  3. Извлеките один заголовок секрета штатными средствами HTTP-фреймворка. Имена HTTP-заголовков регистронезависимы; значение секрета сравнивайте точно, не меняя регистр и не удаляя пробелы.
  4. Проверьте конфигурацию приёмника. Пустое ожидаемое значение - ошибка настройки, а не разрешение принимать запросы без секрета. Лучше обнаружить её при запуске и не объявлять экземпляр готовым.
  5. При несовпадении завершите обработку без постановки задания и без вызова CRM. Не возвращайте клиенту ожидаемое значение или подробности сравнения.
  6. Только после этого разбирайте JSON, проверяйте его структуру и передавайте событие в надёжное хранилище. Успешное HTTP-подтверждение должно соответствовать фактической гарантии сохранения в вашей системе.
Коды ответа ниже - наша примерная политика, а не обязательный выбор Telegram: 403 для проверки секрета, завершившейся отказом, 503 при отсутствии настройки и 202 как решение «разрешить следующий шаг». В реальном обработчике последний код нельзя отправлять до успешного сохранения события. Если хранилище недоступно, авторизация могла пройти, но запрос ещё не принят целиком.

Telegram описывает повтор запросов при ответе не из диапазона 2XY. Поэтому отказ на настоящем событии из-за ошибки конфигурации может вызвать повторные доставки. Не «чините» это безусловным 200: так можно скрыть потерю события. Восстановите согласованность секрета и готовность приёмника, затем проверьте, что поток событий возобновился. Точные сроки повторов здесь не предполагаются.

Таблица решений для приёмника

УсловиеРешение до очередиЧто проверять оператору
Ожидаемый секрет отсутствуетНе принимать; экземпляр не готовДоставку настройки всем экземплярам
Заголовка нет или значение пустоеОтклонить без бизнес-действийРегистрацию вебхука и передачу заголовка прокси
Значение отличается на один символОтклонить, не исправлять автоматическиСогласованность зарегистрированной и ожидаемой версий
В значении есть лишний пробелОтклонить по выбранной политике точного сравненияСериализацию настройки; не добавлять strip() как обход
Секрет верен, JSON некорректенАвторизация прошла, событие не принятоОтдельные проверки формата и лимитов тела
Секрет верен, событие уже сохраненоПрименить дедупликациюКлюч события и существующую запись обработки
Секрет верен, пользователь не имеет правНе выполнять запрещённое действиеБизнес-авторизацию, а не транспортный секрет
Политику для дублирующихся заголовков задайте отдельно: отклоняйте неоднозначный ввод на согласованном слое и тестируйте именно свой стек. Разные серверы могут представлять повторные поля списком, объединённой строкой или одним выбранным значением. Нельзя утверждать, что пример с одним строковым аргументом проверил поведение дублирующихся заголовков на сетевом пути.

Локальная проверка без настоящего секрета

Сохраните код в ingress_check.py и выполните python3 ingress_check.py. Нужна только стандартная библиотека Python 3; пакеты, бот, сеть и CRM не используются. Все строки ниже - публичные тестовые данные, их нельзя переносить в рабочую конфигурацию. Функция получает уже извлечённое значение заголовка, а не сырые HTTP-байты.

В документации Python указано, что hmac.compare_digest позволяет избежать короткого замыкания сравнения по содержимому. Строковые аргументы должны состоять из ASCII-символов, поэтому пример сначала проверяет тип и допустимый алфавит. Это не обещание полной защиты сетевого сервиса от анализа времени: документация отдельно оговаривает возможные утечки при различии типов и длин аргументов.

import hmac
import re

TOKEN = re.compile(r"[A-Za-z0-9_-]{1,256}")


def decision(received, expected):
    if not isinstance(expected, str) or not TOKEN.fullmatch(expected):
        return 503
    if not isinstance(received, str) or not TOKEN.fullmatch(received):
        return 403
    return 202 if hmac.compare_digest(received, expected) else 403


def run_checks():
    expected = "Demo_receiver_token_123"
    cases = [
        ("match", expected, expected, 202),
        ("missing", None, expected, 403),
        ("empty", "", expected, 403),
        ("wrong", "Demo_receiver_token_124", expected, 403),
        ("space", expected + " ", expected, 403),
        ("unicode", "token_я", expected, 403),
        ("too_long", "A" * 257, expected, 403),
        ("bad_config", expected, "", 503),
        ("repeat", expected, expected, 202),
    ]
    queued = []
    for label, received, configured, want in cases:
        status = decision(received, configured)
        assert status == want, (label, status, want)
        if status == 202:
            queued.append(label)
        print(f"{label}: {status}")
    assert queued == ["match", "repeat"]
    print("accepted_for_next_step:", len(queued))


if __name__ == "__main__":
    run_checks()

В локальном запуске примера проверены девять случаев: match и repeat дают 202, шесть некорректных входных значений дают 403, пустая конфигурация - 503. Итоговая строка: accepted_for_next_step: 2. Список queued здесь лишь считает разрешения на следующий шаг и не является настоящей очередью. Повтор намеренно принимается ещё раз: сравнение секрета не хранит историю событий. Это показывает границу проверки, а не недостаток, который нужно исправлять ротацией.

Для обработки повторов используйте отдельный ключ события и постоянное хранилище. Этот этап подробнее разобран в руководстве по идемпотентности Telegram → CRM. Не объединяйте секрет и update_id в один критерий: первый ограничивает вход, второй помогает различать доставки уже принятого события.

Что меняется за обратным прокси

Обратный прокси принимает внешний HTTP-запрос и пересылает его приложению. Составьте схему реального пути: публичный адрес → TLS termination → прокси или балансировщик → конкретный обработчик. Для каждого участка определите, кто может подключиться и где проверяется заголовок. Наличие HTTPS на публичном адресе само по себе ничего не говорит о доступности внутреннего порта.

Проверьте, что клиенты не могут обойти защитный слой и обратиться к приложению напрямую. Если секрет проверяет только прокси, прямой доступ к backend в обход этой проверки нарушает границу защиты. Если секрет проверяет приложение, прокси должен сохранить входное значение. Не настраивайте прокси так, чтобы он безусловно вставлял правильный секрет в каждый внешний запрос: тогда произвольный клиент получит доверенный заголовок уже внутри вашей инфраструктуры.

В официальном примере aiogram secret_token передаётся и при регистрации webhook, и при создании SimpleRequestHandler. Там же показан web-сервер, привязанный к localhost за прокси. Это полезные точки для проверки, а не универсальная схема развёртывания. Контейнерная сеть или отдельный балансировщик могут потребовать другой адрес привязки и соответствующие сетевые ограничения. Проверьте поведение именно той версии фреймворка, которую используете.

В тестовом развёртывании отправьте через публичный адрес запрос с заведомо неверным тестовым значением и убедитесь, что в хранилище не появилось задания. Повторите проверку через любой доступный внутренний маршрут, не выходя за разрешённые пределы своей инфраструктуры. Затем проверьте другой результат с правильным тестовым значением: доходит ли запрос до обработчика, который проверяет и сохраняет событие. Эти сетевые проверки здесь не выполнялись. Локальная функция не обнаружит потерю заголовка на прокси, открытый внутренний порт или неверно настроенный маршрут.

Ротация без постоянного запасного входа

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

Для плановой замены подготовьте новый секрет в разрешённом хранилище и доставьте конфигурацию всем экземплярам приёмника. Если временно принимаются два значения, явно определите ограниченное окно, ответственного за изменение и момент удаления старого значения. Затем зарегистрируйте новое значение через существующий безопасный инструмент управления ботом и проверьте поступление новых тестовых событий. После переключения удалите старое значение на всех экземплярах и подтвердите отказ при запросе со старым тестовым значением. Это эксплуатационная рекомендация, а не встроенная функция автоматической ротации Telegram.

Если старый секрет мог утечь, окно совместимости продлевает возможность его использования. Вместе с ответственным за инцидент решите, нужно ли немедленно отозвать старое значение, даже если это приведёт к временному перерыву. Не переходите к режиму «принимать всё, пока разбираемся». Не используйте и drop_pending_updates только для того, чтобы убрать ошибку: в документации эта настройка описана как удаление ожидающих обновлений, а не как исправление авторизации.

Не вставляйте рабочие значения в команды из статьи, историю shell или тикеты. Записывайте версию конфигурации, время переключения и результат проверки, но не сам секрет. Отдельно проверьте, как инструмент управления получает и передаёт значение. Общее руководство не подтверждает, что секрет не появляется в аргументах процесса или отладочных журналах.

Как разбирать отказы и определять готовность

Если после изменения стало больше отказов, сначала выясните, на каком уровне сформирован ответ: внешним защитным сервисом, прокси, HTTP-фреймворком или вашим проверяющим кодом. Один 403 сам по себе не доказывает, что неверен secret_token. Сопоставьте время запроса и идентификатор трассировки, не копируя чувствительные заголовки. Проверьте выбранный endpoint, версию настройки на каждом экземпляре и передачу заголовка. Расширение прав CRM не исправит ошибку на входе.

Метод getWebhookInfo в Bot API позволяет смотреть состояние вебхука, включая число ожидающих обновлений и сведения о последней ошибке, если они есть. Это источник диагностических данных, а не подтверждение значения секрета, которое ожидает приложение. Прежде чем делиться полным ответом, проверьте его: адрес вебхука и сведения об инфраструктуре тоже могут быть чувствительными.

Начните мониторинг со счётчиков отказов на входе, ошибок конфигурации и возраста необработанных событий. Отказ проверки не должен приводить к записи всей переписки в журналы. Для разрешённого запроса отдельно отслеживайте надёжное сохранение и выполнение работы: транспортная авторизация не доказывает успешную работу CRM. Проверьте также конвейер журналирования и метрик: он может записывать чувствительные заголовки независимо от обработчика.

Приёмник готов к следующему этапу, если неверный тестовый секрет не создаёт задание ни через один доступный маршрут, правильный проходит к проверке формата и сохранению, отсутствие настройки блокирует готовность, а повторный правильный запрос обрабатывается отдельной дедупликацией. Права пользователя и фактический результат в CRM проверяются отдельно. Это наблюдаемые проверки собственной системы, а не гарантия безопасности и не разрешение собирать контакты или отправлять нежелательные сообщения.