How To Secure Telegram Bot Webhook Endpoints

How To Secure Telegram Bot Webhook Endpoints

What You’ll Need

Table of Contents

Why Default Telegram Webhooks Are Vulnerable

When you configure a Telegram bot using the setWebhook API endpoint, Telegram routes incoming user messages, commands, and callback queries directly to your public HTTP POST endpoint. By default, your server receives these requests over the internet. If an attacker discovers your endpoint URL, they can easily craft malicious JSON payloads that mimic genuine Telegram updates.

Without validation, your backend application might process forged payment updates, execute administrative commands, or trigger expensive backend automation logic. Unlike standard signed HTTP requests where signatures are computed per request, Telegram relies on a shared secret header system combined with network level origin validation. If you want to review standard cryptographic request signing methods across other platforms, check out my guide on Implementing HMAC Signature Verification for Inbound Webhooks.

To properly secure a Telegram bot webhook, you must implement a defense in depth strategy:

  1. Verify the secret token header sent by Telegram with every HTTP POST request.
  2. Filter incoming traffic so that only IP addresses owned by Telegram can talk to your webhook port.
  3. Handle authentication failures gracefully at the reverse proxy layer before payloads hit your application logic.

Let’s walk through building a resilient, hardened endpoint architecture from the application layer down to the network infrastructure.

Step 1: Implementing Secret Token Verification in Express.js

Telegram allows you to specify a secret_token parameter when registering your webhook via the setWebhook method. The token must be a string containing 1 to 256 characters (only A-Z, a-z, 0-9, _, and - are allowed). When configured, Telegram attaches this value to every request inside the X-Telegram-Bot-Api-Secret-Token HTTP header.

When setting up your application on a cloud platform like a Hetzner VPS, you must enforce strict secret matching in your code. Below is a complete Node.js application using Express that sets the webhook programmatically and validates every incoming update.

Create a file named server.js with the following implementation:

const express = require('express');
const crypto = require('crypto');

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

const PORT = process.env.PORT || 3000;
const BOT_TOKEN = process.env.BOT_TOKEN || '123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ';
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'a_very_long_secure_random_string_32_chars';
const WEBHOOK_URL = process.env.WEBHOOK_URL || 'https://bot.example.com/telegram/webhook';

async function registerTelegramWebhook() {
  const endpoint = `https://api.telegram.org/bot${BOT_TOKEN}/setWebhook`;
  const payload = {
    url: WEBHOOK_URL,
    secret_token: WEBHOOK_SECRET,
    allowed_updates: ['message', 'callback_query']
  };

  try {
    const response = await fetch(endpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(payload)
    });

    const result = await response.json();
    if (result.ok) {
      console.log('Telegram webhook registered successfully.');
    } else {
      console.error('Failed to register Telegram webhook:', result.description);
    }
  } catch (error) {
    console.error('Error connecting to Telegram API:', error.message);
  }
}

function validateTelegramSecret(req, res, next) {
  const incomingSecret = req.headers['x-telegram-bot-api-secret-token'];

  if (!incomingSecret) {
    console.warn('Unauthorized request attempt: Missing secret token header.');
    return res.status(401).json({ error: 'Missing security token.' });
  }

  const expectedBuffer = Buffer.from(WEBHOOK_SECRET);
  const incomingBuffer = Buffer.from(incomingSecret);

  if (expectedBuffer.length !== incomingBuffer.length) {
    console.warn('Unauthorized request attempt: Invalid secret token length.');
    return res.status(403).json({ error: 'Access denied: Invalid token.' });
  }

  const isValid = crypto.timingSafeEqual(expectedBuffer, incomingBuffer);

  if (!isValid) {
    console.warn('Unauthorized request attempt: Secret token mismatch.');
    return res.status(403).json({ error: 'Access denied: Invalid token.' });
  }

  next();
}

app.post('/telegram/webhook', validateTelegramSecret, (req, res) => {
  const update = req.body;
  console.log(`Received valid update ID: ${update.update_id}`);

  if (update.message) {
    const chatId = update.message.chat.id;
    const text = update.message.text;
    console.log(`Message from ${chatId}: ${text}`);
  }

  res.status(200).send('OK');
});

app.listen(PORT, async () => {
  console.log(`Server listening on port ${PORT}`);
  await registerTelegramWebhook();
});

Notice the use of crypto.timingSafeEqual. Using standard string comparison operators like == or === can make your application vulnerable to timing attacks. Converting your secret string values into buffers and comparing them using constant-time cryptographic checks prevents attackers from guessing your token character by character.

💡 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 to Telegram IP Ranges in Nginx

Validating the token in Express protects your business logic, but unauthorized HTTP requests still hit your Node.js event loop and consume memory. By placing Nginx in front of your application server, you can validate both IP origins and headers at the network edge before requests ever touch your application code.

Telegram sends all webhook notifications from two dedicated subnets:

  • 149.154.160.0/20
  • 91.108.4.0/22

If you are Deploying Open Source Workflow Systems On Hetzner, configuring Nginx to drop traffic outside these ranges dramatically reduces attack surface area.

Here is a complete production Nginx server configuration block that implements IP subnets restrictions and header checking simultaneously:

map $http_x_telegram_bot_api_secret_token $secret_token_status {
    default 0;
    "a_very_long_secure_random_string_32_chars" 1;
}

server {
    listen 80;
    server_name bot.example.com;
    return 301 https://$host$request_uri;
}

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

    ssl_certificate /etc/letsencrypt/live/bot.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/bot.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    location /telegram/webhook {
        allow 149.154.160.0/20;
        allow 91.108.4.0/22;
        deny all;

        if ($secret_token_status = 0) {
            return 403;
        }

        proxy_pass http://127.0.0.1:3000;
        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_cache_bypass $http_upgrade;
    }

    location / {
        return 404;
    }
}

This Nginx architecture provides two layers of edge defense:

  1. The allow and deny directives block non-Telegram IPs entirely with an HTTP 403 response code before parsing body content.
  2. The map block inspects the incoming X-Telegram-Bot-Api-Secret-Token header. If the header does not match your static secret key, Nginx terminates the connection immediately.

Step 3: Hardening Webhooks in n8n Workflows

If you rely on automation tools like n8n Cloud or self-hosted n8n instances to power your Telegram bots, you must implement secret token validation within your workflow nodes.

By default, an n8n Webhook node listens for incoming POST requests and immediately passes the payload downstream. To secure this workflow, follow these setup steps:

  1. In your Telegram Trigger or Webhook node, set the HTTP Method to POST and set the path to telegram-webhook.
  2. Add a Code Node directly after the Webhook node to check headers.
  3. Configure the Code Node to inspect $input.first().json.headers.

Here is the exact JavaScript code to paste into your n8n Code node:

const headers = $input.first().json.headers;
const expectedSecret = 'a_very_long_secure_random_string_32_chars';
const incomingSecret = headers['x-telegram-bot-api-secret-token'] || headers['X-Telegram-Bot-Api-Secret-Token'];

if (!incomingSecret) {
  throw new Error('Unauthorized: Missing Telegram Secret Token Header.');
}

if (incomingSecret !== expectedSecret) {
  throw new Error('Unauthorized: Telegram Secret Token Mismatch.');
}

return $input.all();

If the validation fails inside n8n, the Code node throws an error and stops the execution immediately. This prevents downstream automation nodes from executing database writes, triggering external APIs, or sending response messages to unverified senders.

To make this setup even more secure in production n8n environments, store your secret token in n8n Environment Variables (process.env.TELEGRAM_WEBHOOK_SECRET) rather than hardcoding values in node scripts.

Step 4: Monitoring and Logging Security Events

Securing your endpoints is only half the battle. You also need insight into failure patterns. When malicious scanners attempt to hit your webhook URL, tracking bad requests helps you adjust firewall rules and identify active security probes.

If you run your services inside Docker containers, streaming access logs and validation errors to a centralized dashboard is crucial. You can read my complete guide on How to Stream Container Logs to Loki to set up real time aggregation for your containerized services.

When configuring monitoring for Telegram webhooks, track these specific metrics:

  • HTTP 403 Count on Webhook Path: High volumes of 403 responses indicate unauthorized traffic actively targeting your endpoint URL.
  • Source IP Discrepancies: If requests containing valid secret tokens arrive from outside Telegram’s 149.154.160.0/20 or 91.108.4.0/22 subnets, your secret token has likely been compromised and must be revoked immediately.

To rotate a compromised secret token, execute a setWebhook API call with a fresh secret key string:

curl -X POST "https://api.telegram.org/bot123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ/setWebhook" \
     -H "Content-Type: application/json" \
     -d '{
           "url": "https://bot.example.com/telegram/webhook",
           "secret_token": "new_rotated_secure_secret_token_98765"
         }'

Updating the token via the API immediately invalidates the old token on Telegram’s servers. Once updated, adjust your Express environment variables or Nginx map rules to complete the rotation process without downtime.

Getting Started

To implement this security pipeline for your own Telegram bots:

  1. Provision an Linux host using Hetzner VPS or DigitalOcean.
  2. Configure a domain and free SSL certificate via Namecheap.
  3. Connect your webhook logic directly to n8n Cloud or host your own custom Node.js application server behind Nginx.

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
system online