Telegram bot tokens leak more than any other API key: how to hide yours
A regular API key at least has a chance to hide in a request header. A Telegram bot token has none: it lives right in the URL — and settles into every log along the request's path. Here's what that costs and how to fix it without rewriting your bot.
Why the bot token is the worst case
The Bot API puts the token into the address itself:
https://api.telegram.org/bot123456:AAEhBOweik6ad.../sendMessage
The URL is the leakiest part of an HTTP request. A bot token automatically ends up:
- in the access logs of every proxy, load balancer and CDN on the path;
- in shell history after the first debugging
curl; - in webhook configs, docker-compose files and systemd units — the token is written into a URL, and those files get committed;
- in APM traces and HTTP client error messages (the URL is printed whole);
- in
docker inspectoutput and CI logs where env vars expand into commands.
With an OpenAI key you have to try hard to log it. With a bot token you have to try hard not to.
What the finder gets
Everything the bot can do, no exceptions:
- Intercept incoming messages — a foreign
getUpdatessteals the polling: user messages go to the attacker while your bot goes quiet; - Send as the bot — spam or phishing to an audience that trusts the bot;
- Silent mirroring —
setWebhookto a foreign server duplicates all traffic while the bot keeps working as if nothing happened; - in channels and groups where the bot is admin — deleting messages, banning users, posting.
Standard measures and their limits
- Revoke via @BotFather — the mandatory first step after a leak. But it treats the symptom: the new token lands in the same logs and configs.
- Webhook
secret_token— protects your endpoint from foreign POSTs, but does nothing for the token itself. - Environment variables — hide the token from code, but not from the URL: at request time it still expands into the address line.
The root problem isn't storage — it's that the real token participates in every single request.
The proxy approach: the real token never leaves the vault
The credential proxy scheme fits bots especially well. The real token is encrypted once in the vault; the bot gets a pass — a virtual token — and exactly two things change: the host and the token.
# before
https://api.telegram.org/bot123456:AAEh.../sendMessage
# after
https://api.proxykey.org/p/telegram-bot/123456:vlt_telegrambot_.../sendMessage
The proxy extracts the pass from the path, checks status, IP and limits, injects the real token on its side and forwards the request to Telegram unchanged. Now every log, config and shell history holds only a revocable pass: a leak is one click of "revoke", not a fire drill.
The format-validation trick
aiogram, telebot and grammY validate the <digits>:<hash> token format before the first request — a bare vlt_… pass fails their check. So the proxy accepts a composite form: prefix the pass with your bot id and a colon — the digits are ignored, and the libraries' validation passes.
# aiogram
bot = Bot(
token="123456:vlt_telegrambot_XXXX",
session=AiohttpSession(api=TelegramAPIServer.from_base(
"https://api.proxykey.org/p/telegram-bot"
)),
)
Long polling is covered too: getUpdates with timeout=50 doesn't get cut off — the proxy holds long requests, and file downloads work through the same /file/bot… path.
What you get in practice
- IP binding — the pass works only from the bot's server; a leaked copy gets 403 from anywhere else;
- rpm/rpd limits — a foreign spam run hits the ceiling you set;
- A log of every call — a 3 a.m. burst of
sendMessageat an unusual rate is visible immediately; - Zero-downtime revocation — rotate the pass in one click; the real token stays untouched, no BotFather trip needed.
How we moved two production bots to this scheme — including the rakes we stepped on — in a separate write-up.
FAQ
Does this work with webhooks?
Yes: the webhook is set with the same request through the proxy, and secret_token keeps protecting your endpoint. The real token no longer appears in the webhook config — only the pass does.
Will the bot get slower?
One extra hop — single to low double-digit milliseconds. Against the network latency to Telegram it's invisible, and long polling doesn't care at all.
What if the pass itself leaks?
Wrong IP — 403, over the limits — 429, revocation — one click without reissuing the real token. Compare that with the cleanup after the original leaks.
Hide your bot's token in a minute
Store the token in proxykey — get a pass with IP binding, rate limits and a live log. Free, no card required.
Protect your bot →