Мне довольно часто нужно было зайти на свой сервер: посмотреть какие‑то данные, проверить его состояние или обновить один из сервисов. Каждый раз для этого приходилось брать ноутбук, открывать 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