Аутентификатор показывает новый шестизначный код, VPN его отклоняет, а тот же код прекрасно проверяется, когда вы тестируете его из собственной оболочки. Эта разница — вся история. Модуль PAM, который не может открыть секрет регистрации, возвращает ошибку, выглядящую точно так же, как неверный код, и среда службы, скрывающая домашние каталоги, — частая причина. Прежде чем ослаблять какие-либо права, определите, в какой из трёх ситуаций вы находитесь.

Какая из трёх ошибок это?

Первая — секрет, который модуль вообще не может прочитать. Вторая — секрет, который он может прочитать, но не может обновить. Третья — код, который действительно неверен, обычно из-за дрейфа часов или повторно использованного значения.

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

Представление файловой системы в юните — обычный виновник, когда сбоит только служба.

systemctl show -p ProtectHome -p ProtectSystem -p ReadWritePaths [email protected]
ls -l /var/lib/vpn-otp/*/.google_authenticator 2>/dev/null | head -n 20

Вариант A: вынести секрет из домашнего каталога

Когда служба скрывает /home, а вы хотите сохранить эту защиту, храните секрет там, где она может его видеть. Модуль принимает путь secret= и учётную запись user=, которая выполняет файловые операции, что позволяет центральному хранилищу обслуживать множество идентичностей.

Это аккуратный вариант, и он требует наибольшей осторожности, потому что общее хранилище делает один каталог границей безопасности каждой учётной записи.

auth required pam_google_authenticator.so secret=/var/lib/vpn-otp/${USER}/.google_authenticator user=vpn-otp

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

Вариант B: оставить файл и открыть путь

Если секрет должен остаться в домашнем каталоге пользователя, юниту нужно представление, включающее этот единственный каталог без раскрытия всех домашних каталогов на хосте. Drop-in может добавить узко ограниченное bind-монтирование под изолированным домашним каталогом.

Это сохраняет владение по пользователю как модель, ценой изменения юнита, которое нужно пересматривать при каждом изменении песочницы.

Две границы легко упустить. Путь только для чтения, добавленный под каталогом, который скрывает песочница, не раскрывает его, потому что родительского каталога просто нет. И монтирование должно быть записываемым, если модуль должен обновлять состояние, что вводит следующий вариант. Пишите drop-in отдельным файлом, а не редактируйте упакованный юнит в /usr/lib, чтобы обновление пакета не отменило его молча.

Вариант C: сохранить песочницу и исправить записи состояния

Чтение секрета — только половина аутентификации. Модуль может перезаписывать сведения о смещении времени, защиту от повторного воспроизведения или счётчик, и файл, прошедший проверку чтения, всё ещё может не записаться.

Этот сбой выглядит как плохой код при следующей попытке, а не при этой, поэтому его путают с дрейфом часов.

Дайте ему собственный записываемый каталог и направьте туда путь секрета, вместо того чтобы делать записываемым весь домашний каталог. Подтвердите владельца каталога, свободное место файловой системы и то, ограничивает ли юнит записи в другом месте. Затем проверьте два последовательных входа, потому что второй доказывает путь записи, а первый — только чтение.

Проверяйте как служба, а не как вы сами

Каждая важная проверка должна выполняться внутри песочницы юнита с идентичностью юнита.

Команда, успешная от root, сообщает, что файл существует, и ничего больше. Вход через настоящую службу сообщает, может ли модуль прочитать секрет, использовать его и записать обратно нужное.

Наконец, сохраните требование нетронутым. Добавление варианта с нулевым паролем, принятие сбоев только для чтения или удаление песочницы — всё это убрало бы симптом изменением политики. Пользователь, обязанный предъявить код, по-прежнему должен получать отказ, когда секрет недоступен.

Похожие статьи