proxykey API KEY VAULT

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.

2026-07-06 · proxykey · field report

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:

  1. store the real tokens in the vault (encrypted, never returned again);
  2. issue one pass per bot — each with its own limits and IP binding;
  3. change the base URL and the token in each bot's config;
  4. 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.

The fix The proxy accepts a composite form, <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.

What we shipped because of it Rebinding now requires an explicit confirmation that spells out the consequences; the stats screen shows a "Sources" table (which IPs actually used this pass, flagged when outside the binding); and IPs are enriched with ASN data locally — the log immediately shows "that's our hosting provider" vs "that's someone else". Good security should make dangerous actions inconvenient.

The result

BeforeAfter
Tokens in .env, units, shell historyConfigs hold only revocable passes
Leak = BotFather revoke + restart everythingLeak = one click of "revoke"; the original untouched
No idea who calls the Bot API or howEvery call logged: ~1,700+ requests/day on the broadcast bot; bursts and 4xx visible at once
Token works from any IP403 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

Repeat this with your bot

Everything from this case — the panel, passes, limits, logs — is free, no card required.

Get started →