Legan Studio
Все статьи
~ 5 мин чтения

Webhook secret_token: безопасная проверка апдейтов

Как защитить webhook бота через secret_token, IP-whitelist Telegram, HMAC и rate-limit. Полный разбор для aiogram, grammY и nginx.

  • Telegram
  • webhook
  • безопасность
  • DevOps

Webhook — это HTTPS-эндпоинт вашего бота, на который Telegram присылает апдейты. Без защиты он становится открытой дверью: любой может узнать URL и слать туда фейковые update, имитируя сообщения от пользователей. С 2022 года в Bot API есть нативный механизм защиты — secret_token. В сочетании с IP-whitelist и HMAC он делает webhook практически непробиваемым.

Разберём, как поставить secret_token, что проверять на сервере, как сочетать это с nginx-фильтром по IP Telegram и почему этого недостаточно при использовании self-hosted Bot API.

Что такое secret_token

При вызове setWebhook можно передать параметр secret_token — произвольную строку 1–256 символов из [A-Za-z0-9_-]. Telegram запоминает её и при каждом POST на ваш webhook добавляет HTTP-заголовок:

X-Telegram-Bot-Api-Secret-Token: <ваш_secret>

Ваш сервер проверяет, что заголовок совпадает с ожидаемым. Не совпадает — отдаёте 401 и игнорируете апдейт.

Установка secret_token

from aiogram import Bot
import secrets

SECRET = secrets.token_urlsafe(32)  # храните в env

await bot.set_webhook(
    url="https://api.example.com/tg/webhook",
    secret_token=SECRET,
    allowed_updates=["message", "callback_query", "chat_member", "pre_checkout_query"],
    drop_pending_updates=True,
)

secrets.token_urlsafe(32) даёт 43-символьный токен с энтропией 256 бит — этого более чем достаточно. Сохраните его в переменной окружения и не коммитьте.

Проверка на стороне сервера: aiogram

aiogram 3 проверяет secret_token автоматически, если передать его в setup_application:

from aiogram import Bot, Dispatcher
from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application
from aiohttp import web

bot = Bot(token=BOT_TOKEN)
dp = Dispatcher()

app = web.Application()
SimpleRequestHandler(
    dispatcher=dp,
    bot=bot,
    secret_token=SECRET,  # автоматическая проверка
).register(app, path="/tg/webhook")
setup_application(app, dp, bot=bot)

web.run_app(app, host="0.0.0.0", port=8080)

Запросы без правильного заголовка получают 401 и не доходят до хендлеров.

Проверка на стороне сервера: grammY

import { Bot, webhookCallback } from "grammy";
import express from "express";

const bot = new Bot(process.env.BOT_TOKEN!);
const SECRET = process.env.WEBHOOK_SECRET!;

const app = express();
app.use(express.json());

app.post("/tg/webhook", (req, res) => {
  if (req.header("x-telegram-bot-api-secret-token") !== SECRET) {
    return res.status(401).send("unauthorized");
  }
  return webhookCallback(bot, "express")(req, res);
});

app.listen(8080);

В обоих случаях константное время сравнения не критично — secret_token не подбирается тайминг-атаками за разумное время, его длина 256 бит.

IP-whitelist Telegram

Telegram присылает апдейты с фиксированного диапазона IP. С июля 2024 актуальный список:

149.154.160.0/20
91.108.4.0/22

Это можно проверять в nginx ещё до Python-приложения:

geo $is_telegram {
    default 0;
    149.154.160.0/20 1;
    91.108.4.0/22 1;
}

server {
    listen 443 ssl http2;
    server_name api.example.com;

    location /tg/webhook {
        if ($is_telegram = 0) { return 403; }
        proxy_pass http://bot:8080;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Telegram-Bot-Api-Secret-Token $http_x_telegram_bot_api_secret_token;
    }
}

Двойная защита — IP + secret — закрывает 99.99% сценариев атак.

Rate-limit на уровне nginx

Полезно ограничить частоту запросов на webhook, чтобы случайный шторм не положил приложение:

limit_req_zone $binary_remote_addr zone=tg:10m rate=200r/s;

location /tg/webhook {
    limit_req zone=tg burst=400 nodelay;
    if ($is_telegram = 0) { return 403; }
    proxy_pass http://bot:8080;
}

200 r/s с burst 400 покрывает любой здоровый бот. Если нужно больше — уберите rate-limit, но оставьте connection-limit (limit_conn).

Что проверять обязательно

ПроверкаГде
HTTPS с валидным сертификатомTelegram не работает с self-signed для public webhook
secret_token в заголовкеприложение
IP в диапазоне Telegramnginx
Размер запроса < 1 MBnginx client_max_body_size 1m
Content-Type: application/jsonприложение
Rate-limitnginx

Self-hosted Bot API

Если вы используете self-hosted Bot API сервер (для файлов > 50 MB или своего инфраструктурного контроля), IP проверка не работает — апдейты приходят с вашего собственного сервера. Остаётся secret_token + внутренняя сеть Docker.

services:
  telegram-bot-api:
    image: aiogram/telegram-bot-api:latest
    networks:
      - internal
  bot:
    build: .
    networks:
      - internal
networks:
  internal:
    internal: true  # без выхода в публичный интернет

Webhook URL — http://bot:8080/tg/webhook внутри docker-сети, недоступен снаружи.

drop_pending_updates и переключение

При смене URL или secret_token полезно сбросить очередь старых апдейтов:

await bot.delete_webhook(drop_pending_updates=True)
await bot.set_webhook(
    url=NEW_URL,
    secret_token=NEW_SECRET,
    drop_pending_updates=True,
)

Иначе после деплоя бот разом получит сотни старых сообщений и попытается на них ответить уже после события.

Топ-7 ошибок безопасности webhook

  1. Не ставят secret_token — webhook полностью открыт.
  2. Хранят токен в репозитории.
  3. Не фильтруют по IP Telegram, хотя бот не self-hosted.
  4. Слушают 0.0.0.0:8080 без nginx-прокси с TLS.
  5. Принимают Content-Length > 1 MB и захлёбываются на DoS.
  6. Логируют тело апдейта целиком — токены пользователей утекают в логи.
  7. Не используют drop_pending_updates при смене URL — старые апдейты летят в новый код.

Итого

secret_token — простейший и обязательный шаг защиты webhook. Сочетание secret_token + IP-whitelist Telegram + nginx rate-limit + HTTPS закрывает почти все векторы атак. Это 30 минут настройки, которые экономят дни на разбор последствий взлома: фейковых платежей, спам-рассылок от вашего бота и репутационных потерь.

Частые вопросы

Можно ли менять secret_token без даунтайма?

Да, через короткий «двойной» режим. На 5 минут включаете в коде проверку и старого, и нового токена, потом вызываете setWebhook с новым, и через 5 минут убираете проверку старого. За это окно Telegram переключится на новый токен, и ни один апдейт не потеряется.

Что если secret_token утёк?

Срочно сгенерируйте новый, вызовите setWebhook с новым secret_token. Старый сразу перестанет работать. Проверьте логи на запросы с X-Telegram-Bot-Api-Secret-Token равным старому — там может быть атакующий, который успел воспользоваться. Если бот принимал платежи — сверьте все транзакции за период компрометации.

Нужен ли secret_token, если уже есть IP-whitelist?

Да, нужны оба. IP-whitelist ловит запросы извне инфраструктуры Telegram, secret_token защищает от ситуации, когда атакующий попадает внутрь сети (через скомпрометированный сервис в той же VPC) и шлёт запросы с «внутреннего» IP. Defense in depth — норма.

Может ли secret_token содержать любые символы?

Только [A-Za-z0-9_-], длина 1–256. Любой другой символ — setWebhook вернёт ошибку. Стандартный secrets.token_urlsafe(32) даёт 43 безопасных символа с энтропией 256 бит — этого хватит на десятилетия.

Как защититься от DDoS на webhook?

В первую очередь — Cloudflare или другой CDN с DDoS-защитой перед nginx. Затем IP-whitelist Telegram на уровне nginx (только их трафик доходит до приложения). Затем rate-limit и connection-limit. И обязательно — асинхронный воркер, чтобы webhook возвращал 200 OK мгновенно, не ожидая обработки апдейта.

Telegram повторяет апдейт, если я ответил 5xx?

Да, Telegram делает несколько ретраев с экспоненциальной задержкой (до ~1 часа суммарно), потом дропает апдейт. Чтобы не дублировать обработку, делайте обработчик идемпотентным по update_id — храните последние 10000 ID в Redis и пропускайте повторы.

Можно ли работать без webhook, на long polling?

Да, для маленьких ботов (до 100 апдейтов в секунду) getUpdates через long polling — вполне рабочий вариант, и там нет проблемы с открытым URL: бот сам ходит за апдейтами. Минусы — выше latency (300–500 мс на пустой опрос) и сложнее масштабировать. С 1000+ апдейтов в секунду webhook становится единственным разумным выбором.