Как я сделал Telegram‑бота для управления Linux‑серверами через SSH

Мне довольно часто нужно было зайти на свой сервер: посмотреть какие‑то данные, проверить его состояние или обновить один из сервисов. Каждый раз для этого приходилось брать ноутбук, открывать SSH‑клиент, подключаться к серверу и только потом выполнять нужные команды. Ради небольшой операции это занимало больше времени, чем хотелось.
Поэтому я решил сделать Telegram‑бота, который сможет подключаться к нужному мне серверу по SSH и выполнять команды, которые я использую чаще всего. Идея была простой: открыть Telegram, выбрать сервер, выполнить нужное действие и сразу получить результат.
Сначала мне казалось, что вся схема будет выглядеть примерно так:
Telegram → SSH → Linux
Но когда я начал делать бота, оказалось, что само SSH‑подключение — далеко не самая сложная часть. Нужно было решить, как безопасно хранить данные для подключения, проверять пользователя, работать с ключами серверов и не позволить боту подключаться куда угодно.
Задача: быстрые операции, а не замена SSH
Бот не должен превращать Telegram в универсальный терминал. Для долгой диагностики, интерактивных команд и редактирования большого конфига обычный SSH удобнее и прозрачнее.
Сценарий бота сейчас следующий: открыть телефон, выбрать сервер, выполнить действие и получить результат. Такой процесс полезен, когда нужно быстро проверить состояние сервиса или выполнить короткую операцию без ноутбука.
Что умеет бот
показывать uptime, load average, загрузку CPU, RAM и диска;
выполнять команды через SSH и присылать длинный вывод файлом;
загружать и скачивать файлы через SFTP;
редактировать небольшие UTF-8-файлы с созданием резервной копии;
перезапускать разрешённые systemd‑сервисы после подтверждения;
устанавливать пакеты через APT или DNF/YUM;
вести список серверов и показывать быстрые действия для выбранного сервера.

Состояние диалога хранит только идентификаторы и несекретные параметры. Пароль или приватный ключ используются для проверки подключения, шифруются перед сохранением, а сообщение пользователя с секретом удаляется после обработки.
Как команда доходит до сервера
Когда я только начинал делать бота, хотелось построить всё максимально просто: нажал кнопку в Telegram — бот подключился по SSH — выполнил команду.
Но довольно быстро стало понятно, что между нажатием кнопки и самим SSH нужно сделать ещё несколько проверок.

Когда приходит команда или callback, я сначала определяю пользователя и сервер. Затем backend ещё раз проверяет, принадлежит ли этот сервер пользователю и доступна ли ему выбранная операция. Только после этого бот подключается по SSH или SFTP.
Данные о серверах, пользователях, тарифах и лимитах я храню в PostgreSQL. Там же находятся зашифрованные данные для подключения.
Отдельно пришлось разобраться с лимитами. Попытку я учитываю перед выполнением операции, но транзакцию с базой не держу открытой всё время, пока выполняется SSH‑команда. Иначе какая‑нибудь долгая команда могла бы зря держать соединение с PostgreSQL.
После выполнения результат возвращается в Telegram. Если вывод небольшой — отправляю обычным сообщением. Если текста получилось много — бот присылает его файлом.
Почему я не доверяю кнопкам Telegram
Сначала может показаться, что если пользователь видит кнопку, значит ему уже можно выполнять это действие. Но я решил на это не рассчитывать.
Например, старый callback можно отправить повторно или попробовать передать другой server_id. Поэтому перед выполнением команды backend всё равно проверяет владельца сервера.
У меня это примерно выглядит так:
server = await servers.get_owned(
server_id=server_id,
telegram_user_id=current_user.id,
)
if server is None:
raise AccessDenied
await permissions.require(operation, subscription)
await limits.reserve(operation)
await execute(server, operation)То есть одного server_id мне недостаточно. Я сразу ищу сервер с учётом Telegram ID текущего пользователя. Если такой записи нет, операция дальше вообще не выполняется.
Подключение и первый ключ хоста
Пользователь добавляет название сервера, публичный IPv4- или IPv6-адрес, SSH‑порт, имя пользователя и способ входа. Поддерживаются пароль и приватный SSH‑ключ. На этом этапе важно проверить не только реквизиты, но и сам сервер.

Проверка SSH‑ключа
При первом подключении к серверу я не хотел просто молча доверять ему и сохранять соединение. Поэтому бот получает ключ хоста и показывает его SHA256-отпечаток пользователю.
Пользователь подтверждает отпечаток, после чего я сохраняю ключ. При следующем подключении бот сравнивает его с сохранённым. Если ключ внезапно изменился — подключение останавливается.
Здесь используется обычный TOFU (Trust On First Use). Конечно, у него есть минус: при самом первом подключении мы ещё не знаем, правильный ли сервер перед нами. Поэтому отпечаток лучше отдельно сверить, например с тем, который показывает хостинг.
Пароли и приватные SSH‑ключи я тоже не хотел хранить в PostgreSQL в открытом виде, поэтому перед сохранением они шифруются. При этом я понимаю, что одно шифрование базы не решает всё: самому приложению всё равно нужен ключ, чтобы расшифровать данные. Поэтому стараюсь не писать секреты в логи и отдельно ограничивать права SSH‑пользователя.
Зачем я проверяю IP‑адрес
С адресами серверов обнаружилась ещё одна проблема. Пользователь сам указывает IP, а уже мой backend пытается к нему подключиться.
Если вообще никак не проверять этот адрес, можно передать не публичный сервер, а, например, 127.0.0.1 или адрес из внутренней сети. Получается, что бот начнёт делать запросы туда, куда я изначально вообще не планировал давать доступ.
Поэтому перед SSH‑подключением я проверяю IP. Сейчас бот принимает публичные IPv4 и IPv6, а localhost, приватные сети, link‑local, multicast и другие служебные адреса отбрасывает ещё до попытки подключения.
Причём сделать это нужно до SSH, а не после неудачной авторизации. Даже если пароль неправильный, само приложение уже попробует установить соединение с указанным адресом.
Что делать с командами и sudo
С обычными командами возник другой вопрос: сколько вообще свободы давать боту?
Давать SSH‑пользователю полный sudo мне показалось плохой идеей. Например, вот такое правило фактически снимает почти все ограничения:
bot ALL=(ALL) NOPASSWD: ALLПоэтому я пошёл в другую сторону: если боту нужно перезапускать конкретный сервис, лучше разрешить именно эту команду:
bot ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginxКонечно, это менее универсально, зато мне спокойнее, когда бот не получает root‑доступ ко всему серверу.
Для самих команд я также добавил таймаут и ограничение размера ответа. Иначе достаточно случайно запустить слишком долгую или слишком «разговорчивую» команду — и бот будет ждать её или пытаться отправить огромный ответ в Telegram.
Файлы передаю через SFTP. Редактор специально оставил простым: он рассчитан на небольшие UTF-8-файлы, а перед изменением бот делает резервную копию.
Лимиты и платежи
Когда я добавил тарифы, появилась ещё одна проблема — нельзя полагаться на то, что кнопка уже была показана пользователю. Между открытием меню и нажатием на неё тариф или оставшийся лимит могли измениться.
Поэтому доступ я проверяю ещё раз непосредственно перед выполнением команды.
С платежами похожая история. Telegram может повторно прислать один и тот же update, поэтому один платёж не должен случайно продлить подписку два раза. Для этого ID обработанного платежа сохраняется в базе и повторно уже не принимается.
Что получает пользователь
На телефоне вся цепочка сводится к нескольким действиям: выбрать операцию, выбрать сервер, подтвердить потенциально опасное действие и получить результат. Детали подключения и проверки остаются на backend.

Это и есть главный компромисс проекта: интерфейс остаётся коротким, но каждая короткая операция проходит полный набор проверок.
Что оказалось сложнее самого SSH
Само подключение по SSH оказалось далеко не самой сложной частью. Больше всего времени у меня ушло на три вещи:
Безопасность подключения. Нужно было проверять IP‑адрес до подключения и отдельно разобраться с сохранением и проверкой ключа хоста.
Права пользователя. Я не хотел доверять только данным из Telegram‑кнопок, поэтому перед каждой операцией backend ещё раз проверяет, что сервер действительно принадлежит этому пользователю.
Работа с командами. Пришлось добавить таймауты и ограничения на размер вывода, потому что команда может выполняться долго или вернуть слишком много данных.
Это мой первый такой проект и первая статья на Хабре, поэтому наверняка какие‑то решения можно было сделать иначе.
Сейчас бот уже работает, но пока я в основном проверял его на своих сценариях. Больше всего мне интересно мнение людей, которые сами работают с VPS: стали бы вы вообще доверять подобному боту доступ к тестовому серверу и каких ограничений по безопасности здесь, на ваш взгляд, ещё не хватает?
Репозиторий: https://github.com/DevoidBark2/telegram‑server‑bot‑showcase
Если кому‑то захочется попробовать самого бота, вот ссылка: https://t.me/programmer_assistant_pro_bot
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.