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 в диапазоне Telegram | nginx |
| Размер запроса < 1 MB | nginx client_max_body_size 1m |
| Content-Type: application/json | приложение |
| Rate-limit | nginx |
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
- Не ставят
secret_token— webhook полностью открыт. - Хранят токен в репозитории.
- Не фильтруют по IP Telegram, хотя бот не self-hosted.
- Слушают
0.0.0.0:8080без nginx-прокси с TLS. - Принимают
Content-Length> 1 MB и захлёбываются на DoS. - Логируют тело апдейта целиком — токены пользователей утекают в логи.
- Не используют
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 становится единственным разумным выбором.