Case study: moving two production Telegram bots to proxy tokens in one evening
A field report without the gloss. Two working Telegram bots — one analytics, one broadcast, a couple thousand requests a day between them — with tokens sitting in configs in plain text. The goal: hide the originals in a vault without breaking anything. It took one evening and three rakes.
Starting point
The usual: bot tokens in .env on the server, in systemd units, and in shell history after every debugging session. Anyone who gets disk or log read access gets the bots too. The migration plan onto a credential proxy:
- store the real tokens in the vault (encrypted, never returned again);
- issue one pass per bot — each with its own limits and IP binding;
- change the base URL and the token in each bot's config;
- verify everything works, then purge the originals from the configs.
On paper — half an hour. Then came the rakes this post exists for.
Rake 1: the library rejected the pass
The first bot, on aiogram, failed before its first request: the library validates the <digits>:<hash> token format at startup, and a bare vlt_telegrambot_… pass doesn't survive the check.
<bot_id>:vlt_…: the digits before the colon are ignored, but the format becomes "valid" for aiogram/telebot/grammY. This is supported behavior, not a hack on top — including the URL-encoded %3A variant.
Rake 2: long polling kept getting cut off
The analytics bot runs on getUpdates with timeout=50 — the request hangs for up to 50 seconds waiting for updates. The first runs through the proxy died earlier than that: the proxy's header timeout was shorter than the polling timeout.
Fixed on the proxy side: getUpdates gets the full 300-second budget (headers included), and a test now locks the minimum budget at 60 seconds so a future refactor can't quietly break it. If you're building your own bot proxy — plan for this from day one: ordinary HTTP timeouts don't fit long polling.
Rake 3: the manual IP-rebind foot-gun
The passes were created in auto mode: the first request teaches the IP binding, and every foreign address gets 403 after that. Both bots bound correctly to their server.
Then one bot's binding got wiped by hand: the "re-learn IP" button was pressed "just in case" — and the pass went back to accepting whichever address came first. We spotted the vulnerability window in the panel's sources table.
The result
| Before | After |
|---|---|
Tokens in .env, units, shell history | Configs hold only revocable passes |
| Leak = BotFather revoke + restart everything | Leak = one click of "revoke"; the original untouched |
| No idea who calls the Bot API or how | Every call logged: ~1,700+ requests/day on the broadcast bot; bursts and 4xx visible at once |
| Token works from any IP | 403 from anywhere but the bot's server |
Total migration time for two bots, rakes included — one evening. Migrating a third bot later took ten minutes: the rakes had already been collected.
Checklist if you're repeating this
- feed the library the composite token form,
<bot_id>:vlt_…; - point the base URL at the proxy; leave the path and everything else alone;
- if the bot uses long polling — verify the proxy's timeouts before production;
- once the IP is bound — don't touch rebind without a reason;
- purge the old tokens:
.env, units, shell history (history -c) — only then call the migration done. The full breakdown on Telegram tokens is here.
Repeat this with your bot
Everything from this case — the panel, passes, limits, logs — is free, no card required.
Get started →