How to secure a Telegram bot: data and DDoS

A bot looks more harmless than a website. It has no forms, no admin panel open to the internet and no login page where a password can be guessed. That is exactly why its security is remembered last. Meanwhile a bot holds precisely the things people break in for: the token, a database of people, and money passing through it.
The bot token
It all starts with the token. The bot token effectively is the bot, because whoever holds it reads every incoming message and writes to anyone in your name. The token must not sit in the code, reach the repository or show up in logs. Keep it in environment variables, and close the file holding them to everyone except the process owner. If the token does leak, changing passwords is pointless. The token is revoked through @BotFather, and the old one stops working instantly.
Logs and personal data
The second most common leak happens through logs, and it is the most galling one, because it happens without any ill intent. A developer writes the whole message into the log to make debugging easier, and a month later a text file on the server holds phone numbers, addresses and card numbers that people sent to the bot. What belongs in the log is the user identifier and the event type. Keep the content only where you cannot work without it, and with sensitive fields stripped out.
For personal data a simple rule applies: the surest way not to lose something is not to collect it. Every field the bot asks for should be needed for something. If a name and a way to get in touch are enough for an enquiry, passport details and a date of birth are not: they add no benefit and plenty of liability. Whatever you do have to collect gets encrypted and separated: the live database and the backups should not sit on one disk in the clear.
Admin commands and signature checks in WebApp
One hole that turns up more often than one would like is admin commands with no check on who called them. A button that says "export all users" or "top up balance" lives in the same bot as the ordinary menu, and the difference between a manager and a stranger fits in one line of code. Keep the list of administrator identifiers in the settings, put the check at the entry to the handler rather than in the middle, and always duplicate it on the button press, because a button can be pressed from an old message.
If the bot has a mini app, a check on initData is added. Telegram passes user data into the WebApp together with a signature, and that signature has to be verified on the server with a key derived from the bot token. Without the check anyone can open your API and claim somebody else's identifier, and with it somebody else's orders and balance. It is easy to skip, because without it everything works right up until the day someone tries.
Load and rate limiting
Now about load. A real DDoS in the usual sense does not normally reach a bot, because Telegram stands between the attacker and you, and requests arrive from its servers rather than directly. However you can overload a bot without a botnet: one person holding down a button is enough. Every press turns into a request, a database call, and sometimes a call to a paid external service that you are paying for.
The answer is per-user rate limiting. We decide how many actions per second we are willing to handle from one identifier and drop the rest silently. Heavy operations such as generating a report, calling a model or exporting data go into a queue so the bot answers straight away and does not hold the connection. Always acknowledge the button press, because without an answer Telegram shows a spinner, the person presses again, and the load doubles by itself.
The server, backups and monitoring
At the server level it is ordinary hygiene, with nothing bot-specific about it. Key-only login, password authentication switched off, the minimum number of ports open outward, automatic security updates enabled. The database listens on localhost only, since a PostgreSQL exposed to the internet is found by scanning within hours. If there are several services, split them into containers so that a hole in one does not open access to everything else.
Backups look like a dull subject right up until the first incident. A copy sitting on the same server as the database is not a backup: the server disappears along with it, and it makes no difference whether that was a disk failure or an unpaid invoice. A copy has to travel somewhere else, be taken on a schedule with no human involved, and be restored at least once. Otherwise nobody knows whether it works or whether it is an archive with an error inside.
The last point is finding out about a failure before your customers do. A "the bot is alive and answering" check once a minute and a notification into a separate chat cost almost nothing, and the difference between "noticed in five minutes" and "noticed on Monday" is measured in lost enquiries. We build all of the above in from the start and do not sell it as a separate line, because it is not an optional extra but the normal state of a working service.