двухфакторная аутентификация ssh

Двухфакторная аутентификация SSH по TOTP: Google Authenticator и PAM

Двухфакторная аутентификация ssh через TOTP добавляет к паролю или ключу одноразовый код Google Authenticator и проверяет его через PAM.

Разберём установку пакета, файл секрета, связку PAM с sshd_config, исключения для автоматизации и аварийный доступ на случай ошибки.

Двухфакторная аутентификация SSH по TOTP: Google Authenticator и PAM

2FA для SSH не делает сервер неуязвимым, но закрывает частый сценарий взлома: украденного пароля или одного приватного ключа уже недостаточно для входа. Чтобы защита сработала, нужно заранее проверить PAM-конфиг, правила SSH и резервный доступ администратора.

Содержание

Как работает TOTP и связка Google Authenticator + PAM в SSH

Двухфакторная аутентификация ssh строится на простой идее: к привычному фактору «что вы знаете» (пароль или приватный ключ) добавляется устройство, генерирующее одноразовый код. На стороне Linux за проверку этого кода отвечает PAM-модуль из пакета libpam-google-authenticator. Такая связка подходит для серверов, где уже есть SSH-доступ по ключам или паролям, но нужен дополнительный контроль входа.

Модуль pam_google_authenticator поддерживает два режима: time-based (TOTP) и counter-based (HOTP). В TOTP код привязан к текущему времени, поэтому он меняется через короткие интервалы. В HOTP код привязан к счётчику. На наших проектах мы почти всегда выбираем TOTP: приложение Google Authenticator на смартфоне администратора показывает текущий код, а сервер сверяет его локально, без обращения в интернет.

Чтобы и приложение, и сервер вычисляли одинаковые коды, между ними заранее распределяется общий секретный ключ. Этот ключ PAM по умолчанию ищет в файле .google_authenticator в домашнем каталоге пользователя. Файл должен быть доступен на чтение только владельцу — режим 0600, иначе модуль откажется его использовать. Такой дизайн означает, что компрометация одной учётной записи не открывает чужие секреты: каждый ключ лежит в своём home и защищён правами доступа.

Практически важны не внутренние детали sshd, а выбранная комбинация входа. Код можно требовать вместе с паролем, вместе с публичным ключом или только для части пользователей. Дальше в статье мы разберём каждую схему и покажем, где оставить исключения для автоматизации и аварийного доступа.

Как работает TOTP и связка Google Authenticator + PAM в SSH

Установка libpam-google-authenticator и инициализация ключей пользователя

В Debian и Ubuntu PAM-модуль второго фактора поставляется пакетом libpam-google-authenticator и ставится из штатного репозитория. После установки секретные ключи генерируются отдельно для каждого пользователя, и команду инициализации нужно запускать именно от его имени, а не от root:

Утилита задаёт несколько вопросов. Первый — использовать ли time-based токены: для классической связки с Google Authenticator мы отвечаем утвердительно, выбирая режим TOTP. Дальше она выводит QR-код и текстовый секрет, которые администратор сканирует в мобильное приложение, и предлагает сохранить настройки. По умолчанию ключ записывается в файл .google_authenticator в домашнем каталоге пользователя; на финальных вопросах мы соглашаемся включить защиту от повторного использования кода и ограничение частоты попыток.

Права на файл критичны. Модуль требует, чтобы секрет был доступен на чтение только владельцу — режим 0600, — и по умолчанию проверяет, что владелец файла совпадает с входящим пользователем. Если вы переносите готовые ключи между серверами или разворачиваете их из шаблона, легко получить чужого владельца или слишком широкие права; тогда вход просто не пройдёт. На наших проектах мы после раскладки ключей всегда прогоняем stat -c '%a %U' ~/.google_authenticator, чтобы убедиться в правах и владельце.

Иногда домашние каталоги монтируются по сети или доступны только для чтения, и хранить секрет в ~/.google_authenticator неудобно. На этот случай модуль поддерживает опцию secret=, позволяющую указать нестандартное расположение файла с секретом. Мы используем её, когда выносим ключи в отдельный защищённый каталог: тогда конфиг PAM явно указывает путь к секретам, а администратор может заранее подготовить файлы и проверить права доступа до включения 2FA.

Установка libpam-google-authenticator и инициализация ключей пользователя
				
					# от имени root — установка модуля
sudo apt-get update
sudo apt-get install libpam-google-authenticator

# от имени самого пользователя — генерация секрета
google-authenticator
				
			

Настройка PAM и sshd_config: комбинации «ключ + TOTP» и «пароль + TOTP»

Настройка делится на две части: подключение модуля в стеке PAM и разрешение второго фактора в sshd. В файл /etc/pam.d/sshd мы добавляем строку, которая включает проверку одноразового кода:

Дальше всё зависит от того, какую комбинацию факторов мы строим. Для схемы «пароль + TOTP» удобна опция forward_pass: она запрашивает у пользователя системный пароль и проверочный код в одном приглашении, а пароль затем передаёт следующему модулю PAM. Следующий модуль при этом настраивается с use_first_pass или try_first_pass, иначе пароль будет запрошен дважды.

Для более строгой схемы «ключ + TOTP» пароль вообще не используется — вход разрешён только при наличии правильного приватного ключа и действующего кода. Это наш стандартный выбор для двухфакторной аутентификации ssh на серверах с доступом из интернета. Управляет комбинацией директива AuthenticationMethods в sshd_config:

Здесь publickey проверяет ключ, а keyboard-interactive отдаёт управление PAM, где и срабатывает pam_google_authenticator. Запятая между методами означает «и», а не «или»: SSH потребует оба фактора подряд. Если вы используете старые версии OpenSSH, та же опция могла называться ChallengeResponseAuthentication — проверьте, какой параметр понимает ваш sshd, прежде чем перезапускать сервис. После правки конфигов сначала проверяем синтаксис командой sudo sshd -t и только потом аккуратно перезапускаем демон, не разрывая текущую сессию.

Обратите внимание на порядок действий: по умолчанию, если секретный файл у пользователя отсутствует, попытка входа будет отклонена — кроме случая, когда задана опция nullok. Поэтому перед тем как включать обязательный второй фактор для всех, мы сначала раскладываем ключи и только потом ужесточаем AuthenticationMethods. На наших проектах мы держим вторую открытую SSH-сессию с правами root до тех пор, пока новый вход не проверен полностью.

				
					# /etc/pam.d/sshd
auth required pam_google_authenticator.so
				
			

Исключения для автоматизации: service-аккаунты, Ansible и бэкапы без 2FA через nullok и Match

2FA хорошо защищает людей, но ломает автоматизацию. Ansible, скрипты резервного копирования и мониторинг ходят по SSH без участия человека и не могут ввести одноразовый код. Если включить обязательный TOTP для всех, такие подключения встанут — это первое, что мы проверяем при внедрении.

Самый аккуратный приём — изолировать автоматику в отдельные учётные записи и описать для них исключение через Match в sshd_config. Так живые администраторы проходят двухфакторную аутентификацию ssh, а service-аккаунты входят строго по ключу:

Блок Match переопределяет глобальную настройку только для перечисленных пользователей: им достаточно публичного ключа, TOTP не запрашивается. Мы намеренно ограничиваем такие аккаунты ещё и по источнику (Match User ... Address ...) и по разрешённым командам, чтобы ослабление политики не превратилось в дыру.

Второй инструмент — опция nullok на уровне PAM, которую дописывают к строке модуля как auth required pam_google_authenticator.so nullok. Она разрешает войти без одноразового кода тем, кто ещё не настроил секрет. Это важно помнить: по умолчанию, если секретного файла нет, вход отклоняется, и именно nullok снимает это требование на время выкатки.

Здесь кроется риск, о котором надо сказать прямо. Не оставляйте nullok в конфиге навсегда — мы сталкивались с ситуацией, когда временное послабление на период миграции забыли убрать, и часть учёток месяцами могла заходить вообще без второго фактора, формально считаясь «защищёнными». nullok уместен только как переходный режим: раскатали ключи всем пользователям — убрали опцию и вернули жёсткую проверку. На парке клиентских серверов мы фиксируем дату снятия nullok прямо в задаче, чтобы исключение не пережило миграцию.

Если задача шире одной настройки SSH, посмотрите также разбор защиты OpenSSH от перебора без fail2ban и материал про 2FA для RDP без Active Directory.

				
					# /etc/ssh/sshd_config
AuthenticationMethods publickey,keyboard-interactive

Match User ansible,backup
    AuthenticationMethods publickey
				
			

Защита от самоблокировки: резервные коды и break-glass-доступ

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

Первая линия защиты — резервные коды. При инициализации утилита google-authenticator выдаёт набор одноразовых аварийных кодов; мы распечатываем их или кладём в корпоративный менеджер паролей, отдельно от смартфона. Каждый такой код срабатывает один раз, и его достаточно, чтобы войти, когда приложение недоступно. Терять этот список нельзя — он хранится вместе с секретом в файле .google_authenticator, доступном только владельцу.

Вторая линия — break-glass-доступ, то есть заранее подготовленный аварийный вход. На физических и виртуальных серверах мы держим доступ через консоль IPMI/KVM или панель гипервизора, который не зависит от sshd и его PAM-стека. Так даже при полностью сломанной конфигурации SSH остаётся канал, чтобы откатить изменения и не вызывать выезд на площадку.

Третья линия — процедурная, и она важнее любых опций. Не включайте обязательный второй фактор одной командой на всём парке: мы сталкивались с тем, как опечатка в AuthenticationMethods отрезала доступ сразу к десяткам машин, и восстанавливать пришлось через консоль провайдера. Поэтому мы всегда оставляем вторую открытую root-сессию на время правок, проверяем конфиг командой sudo sshd -t до перезапуска и сначала обкатываем политику на одной тестовой учётной записи. Только убедившись, что новый вход с кодом и аварийным кодом работает, мы распространяем настройку дальше.

Массовое внедрение на парке серверов клиентов и борьба с рассинхронизацией времени

Когда серверов десятки или сотни, ручная настройка не масштабируется. Массовое внедрение двухфакторной аутентификации ssh мы раскатываем через Ansible: один плейбук ставит пакет libpam-google-authenticator, раскладывает шаблоны /etc/pam.d/sshd и sshd_config и копирует заранее сгенерированные секреты в домашние каталоги. Ключевое требование — сохранить владельца и режим 0600 на каждом файле .google_authenticator, иначе модуль откажется его читать.

Чтобы не плодить копии конфигов под разные схемы хранения, удобна опция secret=: она позволяет явно указать путь к файлу секрета, если стандартное расположение в домашнем каталоге не подходит. В таком варианте плейбук отвечает не только за содержимое файла, но и за владельца, права доступа и проверку, что PAM действительно читает нужный путь.

Самая частая проблема массовой эксплуатации TOTP — рассинхронизация времени. Код привязан к часам, и если время на сервере «уехало», правильный код перестаёт совпадать. Сам модуль по умолчанию пытается компенсировать расхождение часов между сервером и устройством, и это поведение можно отключить опцией noskewadj — она актуальна только для режима TOTP. Но полагаться на компенсацию вместо нормальной синхронизации времени мы не советуем.

Правильный вариант — держать NTP включённым на всём парке и регулярно проверять его состояние:

Для серверов в одной сети мы поднимаем внутренний NTP-сервер и подписываем на него всех клиентов, чтобы расхождение часов оставалось пренебрежимо малым. Это снимает подавляющее большинство обращений вида «код не подходит». Если же вы используете счётчиковый режим HOTP, синхронизация времени не нужна, но появляется риск рассинхронизации счётчика; на этот случай есть опция no_increment_hotp, которая не наращивает счётчик при неудачных попытках и относится только к режиму HOTP. По нашему опыту для парка серверов TOTP с надёжным NTP практичнее HOTP, поэтому счётчиковый режим мы оставляем для редких офлайн-устройств.

				
					sudo timedatectl set-ntp true
timedatectl status
				
			

Первоисточники: Ubuntu manpage, Ubuntu tutorial, Google Authenticator PAM wiki и RFC 6238

Любую настройку безопасности стоит сверять с первоисточниками, а не с пересказами. Базовый документ по нашей теме — официальная manpage Ubuntu по модулю pam_google_authenticator. Она описывает назначение PAM-модуля, режимы TOTP и HOTP, требования к файлу .google_authenticator и ключевые опции вроде nullok, secret=, forward_pass, noskewadj и no_increment_hotp.

Manpage же описывает все опции, которые мы упоминали выше. Там указано, что секрет по умолчанию хранится в файле .google_authenticator в домашнем каталоге с правами 0600, что путь можно переопределить опцией secret=, а опция forward_pass запрашивает пароль и код одним приглашением. Отдельно задокументировано поведение nullok: он разрешает вход без кода тем, кто ещё не настроил секрет, тогда как без него попытка входа без секретного файла отклоняется.

Практическую сторону дополняет официальный учебник Ubuntu по настройке второго фактора для SSH — он полезен как проверенная последовательность действий на свежей системе. Технические детали модуля и нестандартные опции, включая noskewadj для TOTP и no_increment_hotp для HOTP, подробнее всего расписаны в upstream-wiki проекта Google Authenticator PAM. А математику одноразовых кодов задаёт RFC 6238, описывающий алгоритм TOTP: понимание этого документа объясняет, почему второй фактор так чувствителен к точности часов.

По нашему опыту связка из этих четырёх источников закрывает почти все вопросы по внедрению: manpage отвечает за поведение модуля, учебник Ubuntu — за порядок настройки, wiki проекта — за тонкие опции, а RFC 6238 — за принцип работы TOTP. Когда документация расходится с реальным поведением сервиса, мы доверяем manpage установленной версии пакета и проверяем фактическое поведение на тестовом сервере, прежде чем менять конфигурацию на боевых машинах.

Что проверить перед включением 2FA

Часто задаваемые вопросы

Ответы на часто задаваемые вопросы по теме статьи.

Модуль pam_google_authenticator запрашивает у пользователя одноразовый код TOTP или HOTP после ввода системного пароля либо после проверки ключа. Секретный ключ хранится в файле .google_authenticator в домашней директории пользователя. Без этого файла вход блокируется, если не активирована опция nullok.
По умолчанию модуль пытается компенсировать разницу во времени. Если это вызывает проблемы, можно отключить автокоррекцию опцией noskewadj, но тогда требуется точная синхронизация часов на клиенте и сервере через NTP.
Да, администратор может сгенерировать секретные ключи централизованно и разместить их в домашних директориях пользователей. Важно убедиться, что права доступа к файлам установлены в 0600 и владелец файла соответствует пользователю.
Для автоматизации лучше использовать отдельные service-аккаунты и ограничивать их через Match в sshd_config. Такие аккаунты можно оставить на входе по публичному ключу, а двухфакторную проверку требовать только от живых администраторов.

Константин Тютюнник — ведущий инженер IT For Prof. Настраивает Linux-серверы, PAM-стек и SSH-доступ для клиентской инфраструктуры, где нельзя рисковать потерей административного входа.

Нужно включить 2FA для SSH без простоя и самоблокировки? Поможем проверить PAM-конфиг, подготовить аварийный доступ и раскатать настройки по серверам. Услуги: администрирование серверов и мониторинг сайта и сервера.