Как настроить двухфакторную авторизацию через Telegram для SSH

от автора

В эпоху цифровых технологий безопасность играет ключевую роль в защите ваших данных и систем. Особенно это актуально для серверов и систем, доступ к которым осуществляется через SSH. Даже если вы используете сложные пароли и надежные методы шифрования, одной аутентификации может быть недостаточно для полноценной защиты вашего сервера от несанкционированного доступа.

Двухфакторная аутентификация (2FA) – это мощный инструмент, который значительно повышает уровень безопасности, требуя подтверждения вашей личности с помощью второго фактора. В этом контексте, двухфакторная аутентификация через Telegram является достаточно эффективным решеним, которое можно легко интегрировать в процесс SSH-подключения.

Зачем это может пригодиться? Во-первых, это значительно усложняет жизнь потенциальным злоумышленникам. Даже если пароль окажется скомпрометирован, доступ к вашему серверу будет возможен только после подтверждения входа через Telegram, что практически исключает риск несанкционированного доступа. Во-вторых, Telegram предоставляет удобный интерфейс и высокий уровень безопасности для отправки уведомлений и запросов на подтверждение, что делает процесс аутентификации простым и доступным.

В этой статье мы шаг за шагом рассмотрим, как настроить двухфакторную авторизацию для SSH с использованием Telegram-бота. Разберем все необходимые шаги – от создания бота до интеграции с вашим сервером, чтобы вы могли обеспечить дополнительный уровень безопасности для вашего окружения.

Ее величество, настройка

Создание бота в Telegram и получение необходимых реквизитов:

Все давно описано, но на всякий подробная инструкция для получения токена и chat id ниже:

Создаем бота через @BotFather. Для чего нужно ввести команду /newbot , далее нужно вести имя и ник бота. В результате нужно получить токен, для доступа к боту.

Процесс создания бота и получения токена доступа для статьи: 7449414805:AAGuDLfYOeC1ylwooYkt1xEEbpGxRKOXc8I

Процесс создания бота и получения токена доступа для статьи: 7449414805:AAGuDLfYOeC1ylwooYkt1xEEbpGxRKOXc8I

Далее необходимо запустить созданного бота через команду /start и что-то в него написать, например, «Привет, Хабр!»

Сообщение нужно для того, чтобы впоследствии узнать ваш Chat ID. Сделать это можно при помощи следующей команды (не забываем указать токен вашего бота):

curl -s https://api.telegram.org/bot{BOT_TOKEN}/getUpdates | grep -o '"id":[0-9]*' | head -1 | awk -F: '{print $2}'

Пример того, как это выглядит:

Стрелочкой указан результат, значение Chat_ID: 9414805

Стрелочкой указан результат, значение Chat_ID: 9414805

На этом часть настройки Telegram бота заканчивается.

Настройка на машине с Linux

Все шаги выполняются на системе Linux Debian 12. Но адаптировать их под вашу точно не составит труда 😉

Для начала, необходимо установить Python, если вдруг его нет.

sudo apt update && sudo apt install -y python3 python3-pip

И несколько pip пакетов, которые нам пригодятся:

pip3 install python-telegram-bot aiofiles requests --break-system-packages

Создадим сам python-скрипт, которые реализуют логику двухфакторной аутентификации.

Не забудьте изменить TOKEN и CHAT_ID на ваши.

cat > telegram_auth.py <<EOF import telegram from telegram import InlineKeyboardButton, InlineKeyboardMarkup import sys import os import asyncio from datetime import datetime import requests  # Для получения информации о городе и провайдере import subprocess  # Для выполнения системных команд  # Конфигурация TOKEN = '7449414805:AAGuDLfYOeC1ylwooYkt1xEEbpGxRKOXc8I'  # Токен вашего Telegram-бота CHAT_ID = '9414805'  # ID чата в Telegram, куда будут отправляться сообщения IP_INFO_URL = 'http://ipinfo.io/{}/json'  # URL для получения информации о IP-адресе (город, провайдер и т.д.)  # Создаем объект бота с использованием токена bot = telegram.Bot(token=TOKEN)  # Словарь для хранения запросов на подтверждение requests = {}  def get_local_ip():     """     Функция для получения локального IP-адреса машины, на которой выполняется скрипт.     Использует команду 'hostname -I' для получения IP-адресов и возвращает первый из них.     """     try:         result = subprocess.run(['hostname', '-I'], capture_output=True, text=True)         return result.stdout.strip().split()[0]  # Возвращаем первый IP из списка     except Exception:         return 'Неизвестен'  # Возвращаем 'Неизвестен' в случае ошибки  def get_hostname():     """     Функция для получения имени хоста машины, на которой выполняется скрипт.     Использует команду 'hostname' для получения имени хоста.     """     try:         result = subprocess.run(['hostname'], capture_output=True, text=True)         return result.stdout.strip()  # Возвращаем имя хоста     except Exception:         return 'Неизвестен'  # Возвращаем 'Неизвестен' в случае ошибки  async def send_telegram_message(username, remote_ip, request_id):     """     Асинхронная функция для отправки сообщения в Telegram с информацией о попытке входа.     Сообщение включает время входа, IP-адрес, информацию о городе и провайдере,     локальный IP и имя хоста.     """     # Получаем информацию о IP     ip_info = {}     try:         response = requests.get(IP_INFO_URL.format(remote_ip))         ip_info = response.json()  # Преобразуем ответ в формат JSON     except Exception:         ip_info = {}  # Если возникла ошибка, оставляем словарь пустым      # Извлекаем информацию из ответа     city = ip_info.get('city', 'Неизвестно')     provider = ip_info.get('org', 'Неизвестно')     login_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')  # Текущее время в нужном формате     local_ip = get_local_ip()  # Получаем локальный IP     hostname = get_hostname()  # Получаем имя хоста      # Формируем текст сообщения     message = (f"? Login Time: {login_time}\n"                f"? Remote IP: {remote_ip}\n"                f"? System IP: {local_ip}\n"                f"?️ City: {city}\n"                f"? Provider: {provider}\n"                f"? Hostname: {hostname}\n"                f"? Username: {username}")          # Создаем кнопки для ответа (разрешить или запретить вход)     reply_markup = InlineKeyboardMarkup([         [InlineKeyboardButton("Разрешить", callback_data=f"allow_{request_id}"),          InlineKeyboardButton("Запретить", callback_data=f"deny_{request_id}")]     ])     try:         await bot.send_message(chat_id=CHAT_ID, text=message, reply_markup=reply_markup)     except Exception:         pass  # Игнорируем ошибки при отправке сообщения  async def main():     """     Основная асинхронная функция, которая запускает процесс обработки входящих запросов.     """     global requests     username = os.getenv('PAM_USER')  # Получаем имя пользователя из переменной окружения PAM_USER     remote_ip = os.getenv('PAM_RHOST')  # Получаем IP-адрес удаленного хоста из переменной окружения PAM_RHOST      if not username or not remote_ip:         sys.exit(1)  # Если данные отсутствуют, завершаем выполнение с кодом 1      # Создаем уникальный идентификатор запроса на основе текущего времени     request_id = str(int(datetime.now().timestamp()))     # Сохраняем информацию о запросе в словаре     requests[request_id] = {'username': username, 'remote_ip': remote_ip, 'timestamp': datetime.now().isoformat()}      # Отправляем сообщение в Telegram с запросом на подтверждение входа     await send_telegram_message(username, remote_ip, request_id)      update_id = None  # ID последнего обновления для бота     start_time = datetime.now()  # Время начала обработки запросов      while True:         try:             # Проверяем, прошло ли более 60 секунд с начала обработки запросов             if (datetime.now() - start_time).total_seconds() > 60:                 sys.exit(1)  # Завершаем выполнение, если прошло больше 60 секунд              # Получаем обновления от бота             updates = await bot.get_updates(offset=update_id, timeout=10)             for update in updates:                 update_id = update.update_id + 1  # Обновляем ID последнего обновления                 if update.callback_query:  # Проверяем, есть ли обратный вызов с кнопки                     callback_data = update.callback_query.data  # Извлекаем данные из обратного вызова                     if callback_data.startswith('allow_') or callback_data.startswith('deny_'):                         req_id = callback_data.split('_')[1]  # Извлекаем ID запроса из данных обратного вызова                         if req_id in requests:                             if callback_data.startswith('allow_'):                                 del requests[req_id]  # Удаляем обработанный запрос из словаря                                 sys.exit(0)  # Разрешаем вход                             elif callback_data.startswith('deny_'):                                 del requests[req_id]  # Удаляем обработанный запрос из словаря                                 sys.exit(1)  # Запрещаем вход         except Exception:             pass  # Игнорируем ошибки в процессе обработки         await asyncio.sleep(1)  # Ожидаем перед следующим запросом  if __name__ == "__main__":     asyncio.run(main())  # Запускаем основную асинхронную функцию EOF

Добавляем конфигурацию PAM для аутентификации через Telegram (не забываем изменить путь к файлу telegram_auth.py):

cat > /etc/pam.d/telegram-auth <<EOF auth requisite pam_exec.so stdout /usr/bin/python3 /root/telegram_auth.py EOF

Включаем аутентификацию через Telegram в SSH:

sed -i '/^auth\s.*pam_exec.so/d' /etc/pam.d/sshd && \ echo "auth include telegram-auth" >> /etc/pam.d/sshd

Перезапускаем SSH для применения изменений:

systemctl restart sshd

Проверка

Выполним подключение по SSH к хосту и введем пароль:

ssh root@192.168.50.77

В этот же момент прилетает сообщение в созданном Telegram-боте:

Если доступ разрешаем, то авторизация происходит успешно:

Если запрещаем, то в доступе отказано.

Как все настроить одной командой (только осталось свои реквизиты указать):
bash -c ' apt update && apt install python3 python3-pip -y && pip3 install python-telegram-bot aiofiles requests --break-system-packages && cat <<EOF > /root/telegram_auth.py import telegram from telegram import InlineKeyboardButton, InlineKeyboardMarkup import sys import os import asyncio from datetime import datetime import requests import subprocess  # Конфигурация TOKEN = "7449414805:AAGuDLfYOeC1ylwooYkt1xEEbpGxRKOXc8I" CHAT_ID = "9414805" IP_INFO_URL = "http://ipinfo.io/{}/json"  bot = telegram.Bot(token=TOKEN)  # Словарь для хранения запросов requests = {}  def get_local_ip():     try:         result = subprocess.run(["hostname", "-I"], capture_output=True, text=True)         return result.stdout.strip().split()[0]     except Exception:         return "Неизвестен"  def get_hostname():     try:         result = subprocess.run(["hostname"], capture_output=True, text=True)         return result.stdout.strip()     except Exception:         return "Неизвестен"  async def send_telegram_message(username, remote_ip, request_id):     ip_info = {}     try:         response = requests.get(IP_INFO_URL.format(remote_ip))         ip_info = response.json()     except Exception:         ip_info = {}      city = ip_info.get("city", "Неизвестно")     provider = ip_info.get("org", "Неизвестно")     login_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")     local_ip = get_local_ip()     hostname = get_hostname()      message = (f"? Hostname: {hostname}\n"                f"? Login Time: {login_time}\n"                f"? Remote IP: {remote_ip}\n"                f"? System IP: {local_ip}\n"                f"?️ City: {city}\n"                f"? Provider: {provider}\n"                f"? Username: {username}")          reply_markup = InlineKeyboardMarkup([         [InlineKeyboardButton("Разрешить", callback_data=f"allow_{request_id}"),          InlineKeyboardButton("Запретить", callback_data=f"deny_{request_id}")]     ])     try:         await bot.send_message(chat_id=CHAT_ID, text=message, reply_markup=reply_markup)     except Exception:         pass  async def main():     global requests     username = os.getenv("PAM_USER")     remote_ip = os.getenv("PAM_RHOST")      if not username or not remote_ip:         sys.exit(1)      request_id = str(int(datetime.now().timestamp()))     requests[request_id] = {"username": username, "remote_ip": remote_ip, "timestamp": datetime.now().isoformat()}      await send_telegram_message(username, remote_ip, request_id)      update_id = None     start_time = datetime.now()      while True:         try:             if (datetime.now() - start_time).total_seconds() > 60:                 sys.exit(1)                          updates = await bot.get_updates(offset=update_id, timeout=10)             for update in updates:                 update_id = update.update_id + 1                 if update.callback_query:                     callback_data = update.callback_query.data                     if callback_data.startswith("allow_") or callback_data.startswith("deny_"):                         req_id = callback_data.split("_")[1]                         if req_id in requests:                             if callback_data.startswith("allow_"):                                 del requests[req_id]                                 sys.exit(0)                             elif callback_data.startswith("deny_"):                                 del requests[req_id]                                 sys.exit(1)         except Exception:             pass         await asyncio.sleep(1)  if __name__ == "__main__":     asyncio.run(main()) EOF cat <<EOF > /etc/pam.d/telegram-auth auth requisite pam_exec.so stdout /usr/bin/python3 /root/telegram_auth.py EOF sed -i "/^auth\s.*pam_exec.so/d" /etc/pam.d/sshd echo "auth include telegram-auth" >> /etc/pam.d/sshd && systemctl restart sshd && echo "Скрипт завершен. Аутентификация через Telegram настроена и SSH перезапущен." '

На этом все 🙂 Надеюсь, кому-то пригодится.


ссылка на оригинал статьи https://habr.com/ru/articles/838940/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *