Securing Webhooks for Custom Telegram Bots
What You’ll Need
To follow this guide step by step, you will need the following tools and infrastructure:
- n8n Cloud or a self-hosted n8n instance for workflow processing
- A cloud server running Ubuntu 22.04 on Hetzner VPS or Contabo VPS (you can also use DigitalOcean)
- A registered custom domain configured with DNS A records via Namecheap
- Node.js (v18 or higher) and Nginx installed on your VPS server
- A Telegram Bot Token obtained from
@BotFather
Table of Contents
- Step 1: Validating Secret Tokens and Header Signatures
- Step 2: Restricting Access via Telegram IP Whitelisting in Nginx
- Step 3: Implementing Rate Limiting and Express Payload Validation
- Step 4: Centralized Logging and Forwarding to Downstream Pipelines
- Getting Started
Exposing a public webhook endpoint for your custom Telegram bot without security controls is a major liability. Malicious actors scan public IPv4 address spaces continuously, sending forged payloads to unauthenticated webhooks to trigger downstream backend tasks, drain your server resources, or corrupt database states.
Telegram provides native security mechanisms, but you must configure them correctly on your web application firewall and application logic. In this tutorial, I will walk you through securing custom Telegram webhooks using secret token headers, network-level IP whitelisting, rate limiting, and structured JSON body validation.
Step 1: Validating Secret Tokens and Header Signatures
When registering a webhook URL with the Telegram Bot API using the setWebhook method, Telegram allows you to specify a parameter named secret_token. This token must be a string containing 1 to 256 characters (only A-Z, a-z, 0-9, _, and -).
When Telegram delivers an incoming payload to your server, it attaches this secret token inside the HTTP header X-Telegram-Bot-Api-Secret-Token. If an incoming request lacks this exact header value, your application must reject it immediately before parsing the JSON body.
If you are managing modern distributed backend microservices, this header checking logic functions similarly to patterns covered in Implementing JWT Token Refresh Strategies Self-Hosted, where authorization state must be verified at the network boundary.
To deploy this layer, you can run your backend on a lightweight server hosted on Hetzner VPS to ensure minimal network latency.
Here is a full, fully functional Express.js server in TypeScript that registers a secure webhook endpoint with Telegram and enforces token validation middleware:
import express, { Request, Response, NextFunction } from 'express';
import crypto from 'crypto';
import https from 'https';
const PORT = 3000;
const BOT_TOKEN = '123456789:ABCdefGHIjklMNOpqrsTUVwxyZ';
const SECRET_TOKEN = 'SuperSecretToken_987654321_SecureKey';
const WEBHOOK_URL = 'https://bot.yourdomain.com/telegram/webhook';
const app = express();
app.use(express.json());
function registerTelegramWebhook(): void {
const postData = JSON.stringify({
url: WEBHOOK_URL,
secret_token: SECRET_TOKEN,
allowed_updates: ['message', 'callback_query']
});
const options = {
hostname: 'api.telegram.org',
port: 443,
path: `/bot${BOT_TOKEN}/setWebhook`,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData)
}
};
const req = https.request(options, (res) => {
let body = '';
res.on('data', (chunk) => {
body += chunk;
});
res.on('end', () => {
console.log('Telegram setWebhook response:', body);
});
});
req.on('error', (error) => {
console.error('Error setting webhook:', error);
});
req.write(postData);
req.end();
}
function validateTelegramSecret(req: Request, res: Response, next: NextFunction): void {
const receivedToken = req.headers['x-telegram-bot-api-secret-token'];
if (!receivedToken) {
res.status(401).json({ error: 'Unauthorized: Missing secret token header.' });
return;
}
const tokenBuffer = Buffer.from(Array.isArray(receivedToken) ? receivedToken[0] : receivedToken);
const secretBuffer = Buffer.from(SECRET_TOKEN);
if (tokenBuffer.length !== secretBuffer.length || !crypto.timingSafeEqual(tokenBuffer, secretBuffer)) {
res.status(403).json({ error: 'Forbidden: Invalid secret token.' });
return;
}
next();
}
app.post('/telegram/webhook', validateTelegramSecret, (req: Request, res: Response) => {
const update = req.body;
console.log('Received authenticated update ID:', update.update_id);
res.status(200).send('OK');
});
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
registerTelegramWebhook();
});
Using crypto.timingSafeEqual prevents side-channel timing attacks that could allow attackers to guess your secret token byte by byte based on response time differences.
💡 Fast-Track Your Project: Don’t want to configure this yourself? I build custom n8n pipelines and bots. Message me with code SYS3-HUGO.
Step 2: Restricting Access via Telegram IP Whitelisting in Nginx
Validating headers at the application level is necessary, but processing unauthenticated requests inside Node.js still consumes CPU cycles and event loop resources. You should drop unverified traffic at your reverse proxy layer before it reaches Node.js.
Telegram sends all webhook updates exclusively from two official IPv4 CIDR blocks:
149.154.160.0/2091.108.4.0/22
If you host your bot infrastructure on DigitalOcean or Contabo VPS, you can configure Nginx to reject any incoming webhook connection that does not originate from these subnet ranges. You can purchase your custom domain name via Namecheap and point its DNS A record to your server IP.
Here is a full Nginx configuration file (/etc/nginx/sites-available/telegram-bot.conf) that enforces SSL certificate management via Let’s Encrypt and implements strict IP whitelisting for your webhook path:
server {
listen 80;
server_name bot.yourdomain.com;
location /.well-known/acme-challenge/ {
root /var/www/html;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name bot.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/bot.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/bot.yourdomain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
client_max_body_size 2M;
location /telegram/webhook {
allow 149.154.160.0/20;
allow 91.108.4.0/22;
deny all;
proxy_pass http://127.0.0.1:3000/telegram/webhook;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Telegram-Bot-Api-Secret-Token $http_x_telegram_bot_api_secret_token;
}
location / {
return 404;
}
}
After placing this file in /etc/nginx/sites-available/telegram-bot.conf, activate the site link and reload Nginx:
sudo ln -s /etc/nginx/sites-available/telegram-bot.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
With this Nginx ruleset configured, any attacker attempting to send GET or POST requests directly to /telegram/webhook from an unauthorized IP address receives an HTTP 403 Forbidden error directly from Nginx without touching Node.js.
Step 3: Implementing Rate Limiting and Express Payload Validation
Even after whitelisting Telegram’s IP range and checking the secret token, malformed payloads or flood attacks can exploit processing bugs in your code. You must sanitize incoming payload structures before passing them to internal functions or AI language models.
When extending your bot to route payloads to external endpoints, such as in Connecting Local DeepSeek Models to Custom Webhooks, ensuring that input messages strictly contain valid text properties protects downstream inference pipelines from crashing on missing parameters.
We will use express-rate-limit for request throttling and zod for payload structural verification.
Install the required validation dependencies:
npm install express-rate-limit zod
Here is the complete TypeScript implementation of the enhanced server featuring strict request rate limits and structural schema parsing:
import express, { Request, Response } from 'express';
import rateLimit from 'express-rate-limit';
import { z } from 'zod';
const app = express();
app.use(express.json());
const webhookLimiter = rateLimit({
windowMs: 1 * 60 * 1000,
max: 100,
message: { error: 'Too many requests from this IP address, please try again later.' },
standardHeaders: true,
legacyHeaders: false,
});
const TelegramUserSchema = z.object({
id: z.number(),
is_bot: z.boolean(),
first_name: z.string(),
username: z.string().optional(),
});
const TelegramChatSchema = z.object({
id: z.number(),
type: z.enum(['private', 'group', 'supergroup', 'channel']),
title: z.string().optional(),
});
const TelegramMessageSchema = z.object({
message_id: z.number(),
from: TelegramUserSchema.optional(),
chat: TelegramChatSchema,
date: z.number(),
text: z.string().optional(),
});
const TelegramUpdateSchema = z.object({
update_id: z.number(),
message: TelegramMessageSchema.optional(),
edited_message: TelegramMessageSchema.optional(),
});
app.post(
'/telegram/webhook',
webhookLimiter,
(req: Request, res: Response): void => {
const parseResult = TelegramUpdateSchema.safeParse(req.body);
if (!parseResult.success) {
console.warn('Rejected invalid payload format:', parseResult.error.format());
res.status(400).json({ error: 'Bad Request: Schema validation failed.' });
return;
}
const update = parseResult.data;
if (update.message && update.message.text) {
console.log(`Valid text message received from user ${update.message.from?.id}: ${update.message.text}`);
} else {
console.log('Received non-text update format.');
}
res.status(200).send('OK');
}
);
app.listen(3000, () => {
console.log('Secure webhook validator listening on port 3000');
});
If an attacker manages to spoof the token or route traffic via an exploited proxy inside Telegram’s subnet range, sending garbage JSON will trigger a 400 Bad Request at the validation layer without crashing your application runtime.
Step 4: Centralized Logging and Forwarding to Downstream Pipelines
To maintain situational awareness over dropped packets, invalid tokens, and rate limit triggers, you should pipe security events to a monitoring system. For a comprehensive telemetry solution, read our guide on Building Centralized Log Management with Grafana Loki.
Once an update passes all security checks, you can forward it to an automation pipeline like n8n Cloud or your self-hosted n8n engine to execute database updates, run business logic, or trigger notifications.
Here is the complete script combining token validation, schema verification, Loki log aggregation, and forwarding validated updates to an internal n8n workflow URL:
import express, { Request, Response, NextFunction } from 'express';
import crypto from 'crypto';
import http from 'http';
import https from 'https';
import { z } from 'zod';
const app = express();
app.use(express.json());
const PORT = 3000;
const SECRET_TOKEN = 'SuperSecretToken_987654321_SecureKey';
const N8N_INTERNAL_WEBHOOK_URL = 'http://127.0.0.1:5678/webhook/telegram-ingest';
const LOKI_PUSH_URL = 'http://127.0.0.1:3100/loki/api/v1/push';
const TelegramUpdateSchema = z.object({
update_id: z.number(),
message: z.object({
message_id: z.number(),
chat: z.object({
id: z.number(),
}),
text: z.string().optional(),
}).optional(),
});
function logToLoki(level: string, message: string, meta: Record<string, string> = {}): void {
const payload = JSON.stringify({
streams: [
{
stream: { app: 'telegram-security-gateway', level, ...meta },
values: [[(Date.now() * 1000000).toString(), message]]
}
]
});
const req = http.request(LOKI_PUSH_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload)
}
});
req.on('error', (err) => {
console.error('Failed to send log to Loki:', err.message);
});
req.write(payload);
req.end();
}
function enforceSecretToken(req: Request, res: Response, next: NextFunction): void {
const receivedToken = req.headers['x-telegram-bot-api-secret-token'];
if (!receivedToken) {
logToLoki('warn', 'Missing X-Telegram-Bot-Api-Secret-Token header', { ip: req.ip || 'unknown' });
res.status(401).json({ error: 'Missing security token.' });
return;
}
const tokenBuffer = Buffer.from(Array.isArray(receivedToken) ? receivedToken[0] : receivedToken);
const secretBuffer = Buffer.from(SECRET_TOKEN);
if (tokenBuffer.length !== secretBuffer.length || !crypto.timingSafeEqual(tokenBuffer, secretBuffer)) {
logToLoki('error', 'Invalid secret token signature detected', { ip: req.ip || 'unknown' });
res.status(403).json({ error: 'Forbidden.' });
return;
}
next();
}
function forwardToN8n(payload: object): Promise<void> {
return new Promise((resolve, reject) => {
const data = JSON.stringify(payload);
const req = http.request(N8N_INTERNAL_WEBHOOK_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(data)
}
}, (res) => {
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
resolve();
} else {
reject(new Error(`n8n responded with HTTP status ${res.statusCode}`));
}
});
req.on('error', (err) => reject(err));
req.write(data);
req.end();
});
}
app.post('/telegram/webhook', enforceSecretToken, async (req: Request, res: Response): Promise<void> => {
const parseResult = TelegramUpdateSchema.safeParse(req.body);
if (!parseResult.success) {
logToLoki('warn', 'Rejected invalid payload schema', { errors: JSON.stringify(parseResult.error.format()) });
res.status(400).json({ error: 'Invalid update format.' });
return;
}
try {
await forwardToN8n(parseResult.data);
logToLoki('info', `Successfully forwarded update ${parseResult.data.update_id} to n8n`);
res.status(200).send('OK');
} catch (error: any) {
logToLoki('error', `Failed to forward update to n8n: ${error.message}`);
res.status(502).json({ error: 'Upstream automation engine unreachable.' });
}
});
app.listen(PORT, () => {
console.log(`Telegram Security Gateway active on port ${PORT}`);
});
Getting Started
Securing webhooks requires applying defense-in-depth across multiple infrastructure layers. By enforcing IP whitelisting in Nginx, secret tokens in Node.js, payload schema verification using Zod, and centralized logging, you make your Telegram bots resilient to unauthorized access and resource exhaustion attacks.
To set up your deployment environment, spin up an instance on Hetzner VPS, Contabo VPS, or DigitalOcean. Buy your domain via Namecheap, and use n8n Cloud to manage your downstream automation logic seamlessly.
Outsource Your Automation
Don’t have time? I build production n8n workflows, WhatsApp bots, and fully automated YouTube Shorts pipelines. Hire me on Fiverr, mention SYS3-HUGO for priority. Or DM at chasebot.online.
Want to automate this yourself?
Start with n8n Cloud (free tier available) or self-host on a Hetzner VPS for full control.
Want this engine running on your own VPS?
This blog publishes itself — daily, unattended, on free API tiers. The full engine, Hugo theme, and setup guide are available as System 3.
Get System 3