В эпоху цифровых технологий безопасность играет ключевую роль в защите ваших данных и систем. Особенно это актуально для серверов и систем, доступ к которым осуществляется через SSH. Даже если вы используете сложные пароли и надежные методы шифрования, одной аутентификации может быть недостаточно для полноценной защиты вашего сервера от несанкционированного доступа.
Двухфакторная аутентификация (2FA) – это мощный инструмент, который значительно повышает уровень безопасности, требуя подтверждения вашей личности с помощью второго фактора. В этом контексте, двухфакторная аутентификация через Telegram является достаточно эффективным решеним, которое можно легко интегрировать в процесс SSH-подключения.
Зачем это может пригодиться? Во-первых, это значительно усложняет жизнь потенциальным злоумышленникам. Даже если пароль окажется скомпрометирован, доступ к вашему серверу будет возможен только после подтверждения входа через Telegram, что практически исключает риск несанкционированного доступа. Во-вторых, Telegram предоставляет удобный интерфейс и высокий уровень безопасности для отправки уведомлений и запросов на подтверждение, что делает процесс аутентификации простым и доступным.
В этой статье мы шаг за шагом рассмотрим, как настроить двухфакторную авторизацию для SSH с использованием Telegram-бота. Разберем все необходимые шаги – от создания бота до интеграции с вашим сервером, чтобы вы могли обеспечить дополнительный уровень безопасности для вашего окружения.
Ее величество, настройка
Создание бота в Telegram и получение необходимых реквизитов:
Все давно описано, но на всякий подробная инструкция для получения токена и chat id ниже:
Создаем бота через @BotFather. Для чего нужно ввести команду /newbot , далее нужно вести имя и ник бота. В результате нужно получить токен, для доступа к боту.
Далее необходимо запустить созданного бота через команду /start и что-то в него написать, например, «Привет, Хабр!»

Сообщение нужно для того, чтобы впоследствии узнать ваш Chat ID. Сделать это можно при помощи следующей команды (не забываем указать токен вашего бота):
curl -s https://api.telegram.org/bot{BOT_TOKEN}/getUpdates | grep -o '"id":[0-9]*' | head -1 | awk -F: '{print $2}'
Пример того, как это выглядит:
На этом часть настройки 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/
Добавить комментарий