Библиотека — Linux — пользователи, группы, PAM и systemd-logind [X]
Шифр
linux-users-pam
Разделы
linux security
Сверено
2 сентября — в этот день утверждения статьи сверялись с первоисточниками, а не правился текст
Объём
26 201 знак · 24 раздела · чтения около 25 минут

Linux — пользователи, группы, PAM и systemd-logind


Обзор

Вопрос «кто может войти в систему и что ему при этом полагается» в Linux разложен на три слоя, которые живут почти независимо и обычно смешиваются в голове в один. Первый отвечает за то, что такое пользователь: набор записей с числовыми идентификаторами, лежащих в файлах или приходящих из каталога. Второй — за то, как принимается решение пустить: стек модулей PAM, вызываемый программой входа. Третий — за то, что происходит после: сессия, её ресурсы и судьба процессов, которыми управляет systemd-logind.

Смешение слоёв объясняет большую часть недоумений. Пользователь есть в /etc/passwd, но войти не может — вопрос ко второму слою. Пароль принят, а команда сразу завершается — к первому (оболочка nologin). Вход прошёл, но фоновый процесс умирает при выходе — к третьему. Правка в /etc/pam.d/ дала не тот результат — почти всегда к порядку строк, а не к самому модулю.

Статья идёт по этим слоям снизу вверх. Особое внимание — PAM: это единственный из трёх, где неверная правка стоит потери доступа к машине, и единственный, чья модель вычисления результата неочевидна настолько, что «поправить по аналогии» регулярно даёт противоположный эффект.

Версии-якоря: Linux-PAM 1.7, shadow-utils 4.20, systemd 261. Там, где поведение зависит от дистрибутива или менялось между версиями, это сказано явно — это как раз та тема, где умолчания Debian и Fedora расходятся, а документация описывает upstream.

Учётные записи

UID — единственное, что видит ядро

Ядро не знает имён пользователей. Оно оперирует числами: у процесса есть UID и GID, у файла — владелец и группа, тоже числа. Имя alice существует исключительно в пространстве пользователя, как результат поиска по числу в какой-нибудь базе.

Из этого следуют вещи, которые иначе выглядят странностями:

  • Две записи с одним UID и разными именами — для ядра один и тот же пользователь. Права разделяются полностью; это иногда используют намеренно (второй root с UID 0 под другим именем), и это же изредка получается по ошибке.
  • Удаление пользователя не меняет владельца его файлов. Владелец остаётся числом, и ls -l начинает показывать голый UID вместо имени. Следующий созданный пользователь, получивший тот же номер, унаследует чужие файлы.
  • Переименование пользователя ничего не ломает: файлы принадлежат числу, а не имени.

Три файла

/etc/passwd   alice:x:1000:1000:Alice:/home/alice:/bin/bash
              имя :пароль:UID:GID:GECOS:домашний каталог:оболочка

/etc/shadow   alice:$y$j9T$...:20304::::::
              имя:хеш:смена:мин:макс:предупр:неактив:истечение:резерв

/etc/group    developers:x:1001:alice,bob
              имя:пароль:GID:список дополнительных членов

Разделение passwd и shadow историческое, но существенное: /etc/passwd обязан быть читаем всем (иначе ls -l не покажет имена), а хеши паролей в общедоступном файле — это офлайновый перебор. Поэтому x во втором поле означает «смотри в /etc/shadow», доступный только root.

В полях shadow стоит разобраться отдельно, потому что через них выражается устаревание паролей. Второе поле — хеш, третье — дата последней смены в днях от 1970-01-01, дальше — минимальный и максимальный возраст пароля, срок предупреждения, срок неактивности и дата истечения учётной записи. Ноль в поле даты смены означает «сменить при следующем входе», пустое поле — устаревание отключено.

Заметное изменение: в shadow-utils 4.20 поле минимального возраста пароля объявлено устаревшим — документация говорит, что оно должно быть пустым и игнорируется. Заодно убраны сами инструменты: chage -m, passwd -n и переменная PASS_MIN_DAYS в login.defs больше не существуют, и указание любого из них теперь завершается ошибкой, а не предупреждением. Следом помечены устаревшими и остальные поля устаревания — sp_max, sp_warn, sp_inact вместе с chage -M, -W и -I; upstream ссылается на NIST SP 800-63B, который прямо запрещает требовать периодическую смену пароля. Arch перешёл на 4.20 летом 2026 года, в стабильных дистрибутивах живут более старые ветки, так что на конкретной машине проверять надо свою.

Управлять этим руками не нужно, для всего есть команды:

chage -l alice                  # показать параметры устаревания
chage -E 2026-12-31 alice       # учётная запись истекает в конце года
passwd -l alice                 # заблокировать пароль
passwd -S alice                 # статус: L/P/NP — заблокирован/пароль/без пароля

Заблокирован, без пароля и «пароль не подойдёт»

Три состояния поля пароля различаются, и разница практическая. Восклицательный знак в начале означает заблокированный пароль: passwd -l просто дописывает ! перед хешем, и снятие блокировки возвращает прежний пароль. Звёздочка означает, что подобрать пароль невозможно в принципе — никакой ввод не даёт такой хеш; так помечают системные учётные записи. Пустое поле означает вход без пароля — и это ровно то, чего никогда не должно случиться на работающей системе.

Важная тонкость: ни !, ни * не запрещают вход другими способами. Заблокированная парольная аутентификация не мешает войти по SSH-ключу. Чтобы действительно закрыть учётную запись, меняют оболочку или ставят дату истечения.

Системные и обычные пользователи

Граница — договорённость, выраженная в /etc/login.defs. По умолчанию обычные пользователи получают UID из диапазона UID_MIN–UID_MAX, то есть 1000–60000, а системные — из SYS_UID_MIN–SYS_UID_MAX, где нижняя граница по умолчанию 101, а верхняя — UID_MIN минус единица. Дистрибутивы эти числа подправляют, но порядок величин сохраняют.

Системным учётным записям обычно ставят оболочку /usr/sbin/nologin. Разница между ней и /bin/false небольшая, но осмысленная: nologin печатает вежливое сообщение и завершается с ненулевым кодом, false молча завершается. Сообщение настраивается через /etc/nologin.txt, и в логах его наличие помогает понять, что вход отвергла именно оболочка, а не аутентификация.

Хеши паролей

Формат поля — $идентификатор$параметры$соль$хеш. Что встречается сегодня:

Префикс Алгоритм Комментарий
$y$ yescrypt умолчание в Debian, Ubuntu и Fedora; устойчив к перебору на GPU
$6$ sha512crypt долгое время стандарт, upstream-умолчание shadow-utils
$2b$ bcrypt распространён в приложениях, в системном shadow реже
$1$ md5crypt безнадёжно устарел, встречается на древних системах

Алгоритм задаётся параметром ENCRYPT_METHOD в /etc/login.defs, который принимает BCRYPT, SHA256, SHA512 или YESCRYPT. Здесь снова расходятся upstream и дистрибутивы: в самом shadow-utils умолчание — SHA512, а Debian и производные ставят YESCRYPT в своём login.defs. Отсюда встречающаяся на практике пёстрая картина: на одной машине в /etc/shadow соседствуют записи с $6$ и $y$ — старые учётные записи сохраняют прежний хеш до следующей смены пароля.

nsswitch.conf: откуда вообще берутся пользователи

Предположение «пользователи лежат в /etc/passwd» верно ровно до первой машины, подключённой к каталогу. Что именно является источником, определяет /etc/nsswitch.conf:

passwd:     files systemd sss
group:      files systemd sss
shadow:     files
hosts:      files resolve [!UNAVAIL=return] dns

Источники опрашиваются слева направо. files — привычные текстовые файлы, sss — SSSD, то есть LDAP, Active Directory или FreeIPA, systemd — модуль nss-systemd.

Последний стоит знать даже на машине без каталогов. Он отвечает за пользователей, которых нет ни в одном файле: динамические учётные записи сервисов с DynamicUser=, учётные записи systemd-homed, записи из JSON-файлов в /etc/userdb/. Он же гарантирует, что root и nobody остаются разрешимыми, даже если /etc/passwd повреждён или отсутствует. Документация рекомендует ставить systemd после files, чтобы файлы имели приоритет.

Практический вывод один, и он важнее остальных: проверять пользователей нужно через getent, а не через grep по файлу.

getent passwd alice          # спросит все источники по порядку
getent group developers
getent passwd | wc -l        # все видимые записи, включая каталог
id alice                     # UID, GID и полный список групп

grep alice /etc/passwd ничего не найдёт для доменного пользователя, который при этом прекрасно входит в систему, — и это регулярный источник путаницы при разборе чужой инфраструктуры.

PAM

Зачем он есть

До PAM каждая программа, проверявшая пароль, делала это сама: читала /etc/shadow, вычисляла хеш, сравнивала. Добавить в систему двухфакторную аутентификацию или LDAP означало пропатчить login, su, sshd, ftpd и всё остальное.

PAM (Pluggable Authentication Modules) выносит эту логику в разделяемые библиотеки, вызываемые через общий интерфейс. Программа говорит «аутентифицируй пользователя», а из каких модулей это сложится — вопрос конфигурации в /etc/pam.d/, где на каждый сервис приходится файл со своим именем.

Четыре независимых стека

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

Тип Отвечает за Типичное содержимое
auth доказательство личности проверка пароля, токен, отпечаток
account право входить сейчас срок действия записи, время суток, источник
password смену пароля требования к сложности, запись нового хеша
session обвязку сессии монтирование домашнего каталога, лимиты, логирование

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

Флаги управления и почему порядок критичен

Каждая строка — это тип, флаг управления, модуль и его параметры:

auth    required    pam_unix.so    try_first_pass

Флагов четыре, и их семантика неочевидна:

Флаг При неудаче модуля При успехе
required стек продолжается, но итог уже провален продолжается
requisite немедленный выход с ошибкой продолжается
sufficient игнорируется, стек продолжается немедленный успех, если раньше не падал required
optional важно только если модуль в стеке единственный то же

Главная неочевидность — required. Провал не прерывает стек: остальные модули всё равно отработают, а отказ вернётся в конце. Сделано это намеренно, чтобы по времени ответа и поведению нельзя было определить, на каком именно шаге вход отвергнут. requisite ведёт себя «как ожидается» и прерывает сразу — его используют там, где продолжать бессмысленно или опасно.

Второй источник ошибок — sufficient. Он даёт немедленный успех только если ни один предшествующий required не провалился. Отсюда прямое следствие: sufficient, поставленный выше нужного required, открывает вход в обход проверки, а поставленный ниже — может не сработать вовсе. Порядок строк здесь не стилистика, а логика.

Полезно знать, что простые флаги — это сокращения. Документация приводит точные соответствия:

required    [success=ok new_authtok_reqd=ok ignore=ignore default=bad]
requisite   [success=ok new_authtok_reqd=ok ignore=ignore default=die]
sufficient  [success=done new_authtok_reqd=done default=ignore]
optional    [success=ok new_authtok_reqd=ok default=ignore]

Развёрнутая форма [значение=действие ...] умеет больше: die — прервать с ошибкой, done — прервать с успехом, ignore — не влиять на итог, а число означает «пропустить столько следующих строк». Именно числовые переходы делают сложные стеки нечитаемыми, и именно они встречаются в конфигурации, которую генерируют дистрибутивные инструменты.

include и substack

Строки не пишут заново для каждого сервиса — общие куски выносят. Debian и производные держат их в common-auth, common-account, common-password, common-session; RHEL и Fedora — в system-auth и password-auth.

Разница между двумя способами подключения тонкая, но иногда решающая. include вставляет строки так, будто они написаны здесь: переход die внутри включённого файла обрывает весь стек. substack изолирует: done и die внутри прерывают только вложенный стек, числовые переходы не выпрыгивают наружу, а сам подстек снаружи считается одним модулем при подсчёте пропускаемых строк.

Практически это значит, что заменять include на substack (и наоборот) «для порядка» нельзя — меняется поведение при отказах.

Разбор реального стека

Пример из документации pam_faillock — хорошая иллюстрация того, как выглядит настоящая конфигурация:

auth     required                pam_securetty.so
auth     required                pam_env.so
auth     required                pam_nologin.so
auth     [success=1 default=bad] pam_unix.so
auth     [default=die]           pam_faillock.so authfail
auth     sufficient              pam_faillock.so authsucc
auth     required                pam_deny.so

Читается это так. Три первых модуля — предварительные проверки: терминал, окружение, наличие файла /etc/nologin. Дальше pam_unix проверяет пароль, и здесь начинается интересное: при успехе (success=1) пропускается одна следующая строка, то есть управление уходит на pam_faillock authsucc, который сбрасывает счётчик неудач и возвращает успех. При неудаче срабатывает default=bad, выполнение доходит до pam_faillock authfail, тот записывает неудачную попытку и обрывает стек через default=die.

Завершающий pam_deny.so — обязательный элемент: он гарантирует отказ, если управление дошло до конца стека, не встретив успеха. Стек auth, заканчивающийся ничем, вернул бы успех.

Из этого разбора видно, почему «добавить строку в /etc/pam.d/» — операция с последствиями. Вставка строки в середину сдвигает всё, на что указывают числовые переходы, и success=1 начинает пропускать не тот модуль.

Модули, которые встречаются постоянно

pam_unix — классическая проверка по /etc/shadow; он же участвует в смене пароля и в стеке сессии.

pam_faillock блокирует учётную запись после нескольких неудачных попыток; настраивается через /etc/security/faillock.conf. Пришёл на смену pam_tally2. Существенная деталь: root по умолчанию не блокируется, и это не недосмотр, а защита от отказа в обслуживании — иначе любой желающий мог бы запереть администратора снаружи, просто перебирая пароль root.

faillock --user alice            # показать неудачные попытки
faillock --user alice --reset    # сбросить счётчик

pam_limits применяет ограничения ресурсов из /etc/security/limits.conf и limits.d/ — количество открытых файлов, процессов, размер памяти. Работает в стеке session, и отсюда типичная ловушка: лимиты применяются к сессиям входа, но не к сервисам systemd — у тех свои директивы в юнит-файлах.

pam_systemd регистрирует сессию в logind, без него не будет ни XDG_RUNTIME_DIR, ни корректного учёта сессий. Тоже в session.

pam_env расставляет переменные окружения из /etc/environment и /etc/security/pam_env.conf.

Отладка без потери доступа

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

# Терминал 1 — открыт, root, ничего не трогаем: это путь к откату
# Терминал 2 — правим конфигурацию
# Терминал 3 — проверяем вход
ssh -v user@host

Проверить стек, не подключаясь, помогает pamtester (пакет с тем же именем):

pamtester sshd alice authenticate       # прогнать стек auth сервиса sshd
pamtester -v login alice acct_mgmt      # проверить стек account

Отладочный вывод самих модулей включается параметром debug в строке и уходит в системный журнал:

journalctl -f -t sshd -t sudo -t su
journalctl _COMM=sshd --since "5 min ago"

Отдельно стоит помнить про /etc/nologin: если файл существует, pam_nologin не пустит никого, кроме root. Смотрит модуль и в /run/nologin — именно его systemd-user-sessions.service создаёт перед выключением и снимает после загрузки, так что искать надо оба. Файл иногда остаётся после неудачного завершения работы системы и выглядит как загадочный отказ во входе при заведомо верном пароле.

systemd-logind

Сессии и seats

logind учитывает, кто и как вошёл. Единица учёта — сессия; сессии группируются по seat — комплекту физических устройств ввода-вывода, обычно единственному (seat0). Каждой сессии соответствует cgroup, что и позволяет отслеживать её процессы как целое.

loginctl list-sessions
loginctl show-session 3 -p Type -p Remote -p Active
loginctl user-status alice
loginctl terminate-session 3

Почему процессы умирают при выходе

Классический сюрприз: запустили долгую задачу, вышли из SSH, вернулись — процесса нет. Причина в KillUserProcesses: при значении yes cgroup сессии уничтожается вместе со всеми процессами при выходе.

Здесь важное расхождение, объясняющее противоречивые советы в интернете. Upstream-умолчание — yes, оно появилось в systemd 230 (май 2016) и вызвало заметный скандал именно потому, что ломало screen, tmux и ssh-agent. Debian вернул no в своей сборке, и производные унаследовали это. Поэтому на Debian и Ubuntu процессы обычно выживают, а на системах со сборкой по умолчанию — нет.

Проверить, что у вас:

loginctl show-session --property=KillUserProcesses   # без аргумента — свойства менеджера

Чинить это выключением KillUserProcesses — плохой путь: настройка глобальная и отключает учёт ресурсов сессии целиком. Правильных способов два.

Linger — разрешить пользователю иметь процессы вне сессий. Тогда для него постоянно работает экземпляр user@UID.service, живущий независимо от входов и выходов:

loginctl enable-linger alice
loginctl show-user alice -p Linger

Вынести процесс из сессии в отдельный scope под пользовательским менеджером — это то, что документация systemd предлагает для screen и tmux:

systemd-run --scope --user tmux new -d -s work
systemd-run --user --unit=backup /usr/local/bin/backup.sh

Оба варианта решают задачу правильно, в отличие от nohup и setsid: те отвязывают процесс от терминала, но не выводят его из cgroup сессии, поэтому при KillUserProcesses=yes он всё равно будет убит.

user@.service

Пользовательский экземпляр systemd — это полноценный менеджер юнитов, работающий от имени пользователя. Юниты лежат в ~/.config/systemd/user/, управление через systemctl --user, и там работают те же таймеры и зависимости, что и в системном. Общая механика юнитов разобрана в статье про systemd; здесь существенно то, что без linger этот экземпляр запускается при первом входе и останавливается после последнего выхода.

Нюансы и trade-off'ы

Правка файлов вручную. /etc/passwd, /etc/shadow и /etc/group правят через vipw, vipw -s и vigr, а не редактором напрямую. Эти обёртки берут блокировку и проверяют синтаксис перед сохранением — а сломанная строка в /etc/passwd означает, что войти не сможет никто.

Членство в группах не обновляется на лету. Добавили пользователя в группу — его текущие сессии об этом не узнают, потому что список групп фиксируется при входе. Требуется перелогин; newgrp даёт временный обходной путь в рамках одной оболочки. Регулярный источник недоумения при настройке доступа к docker или dialout.

Повторное использование UID. Удалили пользователя, создали нового — тот может получить освободившийся номер и стать владельцем файлов предшественника. При удалении лучше либо переносить файлы, либо оставлять UID зарезервированным.

Сломанный источник NSS блокирует вход. Если sss или другой модуль в nsswitch.conf перестал отвечать, обращения за пользователями начинают ждать тайм-аута. Симптом — вход занимает минуту вместо секунды, ls -l подвисает, getent passwd не возвращается. Диагностика начинается с getent, а не с PAM: если он висит, дело не в аутентификации.

Лимиты применяются не ко всему. pam_limits работает для сессий входа. Сервис systemd не проходит через PAM, и лимиты для него задаются в юните (LimitNOFILE= и родственные). Настройка limits.conf в надежде повлиять на демона — распространённая и безрезультатная процедура.

Разница между su и su -. Первая форма меняет UID, но сохраняет окружение, вторая имитирует полноценный вход и проходит другой стек PAM. Отсюда расхождения в PATH и в переменных, которые ставит pam_env.

Сравнение с FreeBSD

Модель учётных записей выглядит похоже, но устроена иначе в двух отношениях.

База пользователей компилируется. Основной файл — /etc/master.passwd, у него больше полей, чем у линуксовой пары passwd/shadow: в одной строке и хеш, и класс входа, и время смены пароля, и срок истечения записи. Текстовый файл — только источник: pwd_mkdb собирает из него хеш-базы /etc/pwd.db и /etc/spwd.db (последняя доступна только root) и заодно генерирует урезанный /etc/passwd для совместимости. Прямая правка master.passwd без последующей сборки не даёт никакого эффекта — редактируют через vipw, который вызывает pwd_mkdb сам.

Классы входа вместо разрозненных механизмов. Пятое поле записи — login class, ссылка на запись в /etc/login.conf. Класс задаёт лимиты ресурсов, переменные окружения, учёт и параметры аутентификации разом. То, что в Linux размазано между limits.conf, pam_env и pam_limits, во FreeBSD собрано в одном месте и применяется по принадлежности пользователя к классу. Файл login.conf тоже компилируется — после правки нужен cap_mkdb /etc/login.conf, иначе изменения не подхватятся.

PAM во FreeBSD есть, но это другая реализация — OpenPAM, а не Linux-PAM. Конфигурация лежит в /etc/pam.d/ (политики портов и пакетов — в /usr/local/etc/pam.d/) и выглядит знакомо, типы те же. А вот флаги расходятся: у OpenPAM их пять — добавлен binding, который при успехе обрывает цепочку с успехом, а при неудаче проваливает её целиком, — зато развёрнутой формы [значение=действие] и substack нет вовсе, так что числовые переходы из линуксовых стеков перенести не на что. Различия продолжаются в наборе модулей: части линуксовых модулей просто нет, а имена и параметры совпадают не всегда, поэтому переносить стек между системами копированием не стоит.

Управление сессиями отдельной подсистемы уровня logind не имеет: сессия завершается вместе с процессами по обычным правилам POSIX, никаких cgroup-групп и вопроса «убивать ли процессы при выходе» не возникает. Фоновый процесс, отвязанный через nohup или daemon, переживает выход всегда.

Источники

  • shadow(5) и дерево shadow-utils — поля /etc/shadow, признаки блокировки, объявление поля минимального возраста устаревшим в 4.20
  • Release 4.20.0: Ibores — удаление chage -m, passwd -n и PASS_MIN_DAYS, список отложенных на будущее удалений и обоснование через NIST SP 800-63B
  • login.defs(5) — ENCRYPT_METHOD (BCRYPT, SHA256, SHA512, YESCRYPT), диапазоны UID_MIN/UID_MAX (1000–60000) и SYS_UID_MIN/SYS_UID_MAX
  • pam.conf(5), раздел синтаксиса — семантика флагов, их эквиваленты в форме [значение=действие], различие include и substack
  • pam_faillock(8) — блокировка после неудачных попыток, разобранный пример стека, объяснение, почему root не блокируется
  • nss-systemd(8) — DynamicUser=, systemd-homed, drop-in записи в /etc/userdb/, рекомендация ставить systemd после files
  • logind.conf(5) и loginctl(1) — KillUserProcesses, KillExcludeUsers, enable-linger
  • systemd 230: смена умолчания KillUserProcesses — история изменения и возврат к no в Debian
  • OpenPAM: pam.conf(5) в дереве FreeBSD — порядок поиска политик, пять флагов управления вместе с binding, отсутствие формы [значение=действие]
  • FreeBSD: passwd(5), pwd_mkdb(8), login.conf(5) — master.passwd, компиляция баз, классы входа

Связанные заметки

Из библиотеки института уносят только знания. Читателей здесь не считают: посещение не регистрируется, отметок в издании не остаётся, счётчиков нет ни у одного издания фонда.

Издание пополняется и пересверяется. Текст доступен на условиях CC BY-SA 4.0 — унесённое можно передавать дальше на тех же условиях.