Как стать автором
Обновить

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

Уровень сложностиПростой
Время на прочтение9 мин
Количество просмотров12K

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

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

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

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

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

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

Все давно описано, но для полноты картины:

Создаем бота через @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)

# Словарь для хранения запросов на подтверждение
request_data = {}

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"🏠 Hostname: {hostname}\n"
			   f"📍 Remote IP: {remote_ip}\n"
			   f"🌐 System IP: {local_ip}\n"
			   f"🔌 Provider: {provider}\n"
			   f"🏙️ City: {city}\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 request_data
	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()))
	# Сохраняем информацию о запросе в словаре
	request_data[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 request_data:
							if callback_data.startswith('allow_'):
								del request_data[req_id]  # Удаляем обработанный запрос из словаря
								sys.exit(0)  # Разрешаем вход
							elif callback_data.startswith('deny_'):
								del request_data[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'  # Токен вашего Telegram-бота
CHAT_ID = '9414805'  # ID чата в Telegram, куда будут отправляться сообщения
IP_INFO_URL = 'http://ipinfo.io/{}/json'  # URL для получения информации о IP-адресе (город, провайдер и т.д.)

# Создаем объект бота с использованием токена
bot = telegram.Bot(token=TOKEN)

# Словарь для хранения запросов на подтверждение
request_data = {}

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"🏠 Hostname: {hostname}\n"
			   f"📍 Remote IP: {remote_ip}\n"
			   f"🌐 System IP: {local_ip}\n"
			   f"🔌 Provider: {provider}\n"
			   f"🏙️ City: {city}\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 request_data
	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()))
	# Сохраняем информацию о запросе в словаре
	request_data[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 request_data:
							if callback_data.startswith('allow_'):
								del request_data[req_id]  # Удаляем обработанный запрос из словаря
								sys.exit(0)  # Разрешаем вход
							elif callback_data.startswith('deny_'):
								del request_data[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 перезапущен."
'

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

Теги:
Хабы:
Всего голосов 25: ↑23 и ↓2+24
Комментарии35

Публикации

Истории

Работа

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань