Как известно, пользователи и группы в Linux определяются по целочисленному идентификатору, который используется при описании владельца и группы файла, а также для создания контекста текущего пользователя после авторизации. Но как это работает внутри? И можно ли создать свою реализацию для взаимного преобразования имен и идентификаторов и для аутентификации пользователей? В этой статье мы детально рассмотрим анатомию подсистем NSS (Name Security Service) и создадим свою простую реализацию подсистем для использования с текстовым файлом со списком пользователей и паролей. Во второй части статьи мы поговорим о PAM и обсудим возможные способы ее реализации и применения.
Начнем с подсистемы NSS, которая отвечает за преобразование идентификаторов в строковое представление и наоборот, а также за извлечение дополнительных данных, связанных с типом ресурса. NSS является частью стандартной библиотеки и проксирует запросы к функциям разрешения имен (например, getipnodebyaddr) через подключаемые библиотеки. В NSS поддерживаются "из коробки" следующие типы ресурсов (определены в nss/databases.def):
aliases (alias-lookup.c) — почтовые псевдонимы, используется системным вызовом getaliasent, setaliasent, endaliasent, getaliasbyname;
ethers (ethers-lookup.c) — номера узлов ethernet, используется вызовами ether_aton, ether_hostton;
group (grp-lookup.c) — список групп и пользователей, которые в них присоединены (используется системными вызовами getgrent, setgrent, endgrent, getgrgid, getgrname)
gshadow (sgrp-lookup.c) — политика групп (используется setsgent, getsgent, endsgent, getsgname)
hosts или ahosts/ahostsv4/ahostsv6 (hosts-lookup.c) — связь адреса и символьного имени (источником может быть, например, dns). Используется вызовами getaddrinfo, gethostbyaddr, gethostbyaddr_r, gethostbyname, gethostbyname2, gethostbyname_r, getipnodebyaddr, getipnodebyname;
initgroups — дополнительный список групп и связанных с ними пользователей (для этого источника всегда выполняется слияние с другими данными), используется getgrouplist.
netgroup (netgrp-lookup.c) — список прав доступа пользователей к хостам (
networks (network-lookup.c) — список доступных подсетей (используется getnetbyaddr, getnetent, getnetbyaddr_r, getnetbyname, getnetbyname_r);
shadow или passwd (spwd-lookup.c) — информация о пользователях, паролях и политике ротации паролей (используется getpwent, getpwent_r, getpwname_r, getpwuid_r, setpwent, endpwent);
protocols (proto-lookup.c) — список зарегистрированных сетевых протоколов, используется getprotoent и др.
publickey (key-lookup.c) — публичные и приватные ключи для NFS и NIS+.
rpc (rpc-lookup.c) — зарегистрированные удаленные процедуры, используется getrpcbyname и др
services (service-lookup.c) — зарегистрированные сетевые сервисы, используется getservent и др.
В действительности библиотека nss только проксирует запросы на источники данных, список которых определяется в файле конфигурации nsswitch.conf (или в значениях по умолчанию, которые определены в соответствующих C-файлах). Также в nsswitch.conf наряду с перечислением источников данных может указываться поведение при получении определенного статуса из предыдущего источника (в квадратных скобках). Например, можно прервать обработку, если значение не было найдено, а не продолжать вызовы библиотек далее по списку.
Каждый источник возвращает один из статусов:
NSS_STATUS_TRYAGAIN — источник временно недоступен (например, при переполнении очереди запросов)
NSS_STATUS_UNAVAIL — источник данных недоступен (например, при ошибке подключения к базе данных или сетевому сервису разрешения имен)
NSS_STATUS_NOTFOUND — запись с таким идентификатором не найдена в источнике данных
NSS_STATUS_SUCCESS — данные получены успешно, в переданном объекте лежит заполненная структура данных
NSS_STATUS_RETURN — нужно прервать обработку цепочки вызовов (нельзя переопределить через nsswitch.conf, всегда выполняет NSS_ACTION_RETURN)
И при итерации по источникам применяется одно из действий:
NSS_ACTION_CONTINUE (по умолчанию для первых трех статусов) — при совпадении статуса — игнорировать результаты источника и перейти к следующему (который может переопределить данные), за исключением initgroups, где это действие будет интерпретироваться как NSS_ACTION_MERGE.
NSS_ACTION_RETURN (по умолчанию для последних двух статусов) — немедленный выход и возврат данных в приложение
NSS_ACTION_MERGE — сохранить текущие данные и позволить их объединить с более поздними (сейчас применимо только для group)
При обработке запросов nss может быть собран с поддержкой nscd. NSCD (Name Service Caching Daemon) — самостоятельное приложение, которое доступно через сокет (может запускаться как в режиме демона с опцией -d, так и в интерактивном режиме -f). Nscd использует механизмы inotify для обнаружения изменений в /etc/passwd, /etc/hosts и /etc/resolv.conf для инвалидации соответствующих кэшей, но всегда есть возможность сделать сброс кэша соответствующего типа данных, например nscd -i hosts
. Время жизни для типа данных задается в конфигурации /etc/nscd.conf.
Название so-файлов и функций предопределены и создаются по следующей схеме:
libnss_<источник>.so — для разделяемой библиотеки (например, libnss_files.so)
_nss_<источник>_<func> — реализация соответствующей системной функции (например, источник может быть files, а func — gethostbyname_r). Функция должна вернуть nss_status (подключается из nss.h)
_nss_<источник>_init — функция инициализации (вызывается при использовании nscd для регистрации источника и его конфигурации кэширования)
Для запроса ресурсов через nss можно использовать инструмент getent, который может работать как в режиме перечисления всех доступных ресурсов этого типа, например:
getent passwd
Отобразит информацию о доступных учетных записях и их конфигурации. Также может быть запрошена информация о конкретной записи (по идентификатору или по символьному имени), например:
getent passwd root
Существует большое количество реализаций nss, например для включения в список доступных узлов (hosts) запущенных контейнеров (libnss-mymachines) или виртуальных машин (libnss-libvirt), подключения дополнительных файлов для описания пользователей и групп (libnss-extrausers), использования ldap для получения информации о пользователях, группах, псевдонимах и др.
Для практики мы разработаем простое расширение, которое добавит группы виртуальных пользователей service<N> (где N изменяется от 0 до 99) без права авторизации и проверим, что новые пользователи будут доступны в getent passwd. Для разработки будет использоваться C.
Подключим необходимые заголовочные файлы и определим константы для нашего генератора пользователей:
#include <stdio.h>
#include <nss.h>
#include <string.h>
#include <stdlib.h>
#include <pwd.h>
const int N = 30;
const uid_t start_uid = 5000;
char *prefix = "user";
char *nologin = "/usr/sbin/nologin";
char *pwdir = "/tmp/service";
Существует два режима работы библиотеки — поиск по идентификатору/имени и итерация по последовательности записей. В первом случае используются функции getpwuid_r и getpwnam_r (соответственно для id и имени). Во втором задействованы три функции — setpwent начинает итерацию, getpwent_r заполняет результат следующей строкой итератора, endpwent завершает итерацию. Сделаем прежде всего внутреннюю функцию для заполнения виртуального пользователя (строки возвращаются ссылкой на буфер):
void set_passwd(uid_t id, struct passwd *result, char* buffer) {
char *username = malloc(strlen(prefix)+3);
strcpy(username, prefix);
char *buf = malloc(3);
sprintf(buf, "%2d", start_uid);
strcpy(username + strlen(prefix), buf);
strcpy(buffer, username);
strcpy(buffer+strlen(username)+1, nologin);
strcpy(buffer+strlen(username)+strlen(nologin)+2, pwdir);
result->pw_name = buffer;
result->pw_shell = buffer+strlen(username)+1;
result->pw_uid = id;
result->pw_gid = 0; //root
result->pw_dir = buffer+strlen(username)+strlen(nologin)+2;
free(username);
}
Создадим реализацию итератора:
uid_t current_uid = 0;
//перемещение итератора в начало списка
enum nss_status _nss_dynamic_setpwent(void) {
current_uid = start_uid;
return NSS_STATUS_SUCCESS;
}
//прочитать следующий элемент списка
enum nss_status _nss_dynamic_getpwent_r(struct passwd *result, char *buf, size_t buflen, int *errnop) {
if (current_uid == 0 || current_uid >= start_uid + N) {
return NSS_STATUS_NOTFOUND;
} else {
set_passwd(current_uid, result, buf);
current_uid++;
return NSS_STATUS_SUCCESS;
}
}
//завершение итерации
enum nss_status _nss_dynamic_endpwent(void) {
current_uid = 0;
}
И функции для реализации получения данных о пользователях по идентификатору или имени:
//поиск по идентификатору (будет от start_uid до start_uid+N-1 включительно)
enum nss_status _nss_dynamic_getpwuid_r(uid_t uid, struct passwd *result, char *buf, size_t buflen, int *errnop) {
*errnop = 0;
if (result) {
if (uid < start_uid || uid >= start_uid + N) {
//не наш идентификатор
return NSS_STATUS_NOTFOUND;
} else {
//создать соответствующую структуру
set_passwd(uid, result, buf);
return NSS_STATUS_SUCCESS;
}
} else {
//если некуда записывать результат
return NSS_STATUS_UNAVAIL;
}
}
//поиск по имени пользователя
enum nss_status
_nss_dynamic_getpwnam_r(const char *name, struct passwd *result, char *buf, size_t buflen, int *errnop) {
*errnop = 0;
if (result) {
if (strncmp(name, prefix, strlen(prefix))) {
uid_t id = start_uid + atoi(name + strlen(prefix));
set_passwd(id, result, buf);
return NSS_STATUS_SUCCESS;
} else {
return NSS_STATUS_NOTFOUND;
}
} else {
return NSS_STATUS_UNAVAIL;
}
}
Создадим файл для сборки CMakeLists.txt:
cmake_minimum_required(VERSION 3.22)
project(nss_dynamic C)
set(CMAKE_C_STANDARD 17)
add_library(nss_dynamic SHARED library.c)
И выполним сборку библиотеки: cmake --build .
Теперь добавим каталог поиска в ld.so.conf.d: echo `pwd` >/etc/ld.so.conf.d/nss_dynamic.conf
и обновим пути ldconfig
Теперь нужно зарегистрировать новый источник данных для базы passwd, при этом мы будем расширять существующий список пользователей, изменим /etc/nsswitch.conf строку passwd:
passwd: files [SUCCESS=merge] dynamic
Теперь мы можем запросить список всех пользователей (getent passwd) и информацию о конкретном пользователе (getent passwd 5000 или getent passwd user11). В любом случае мы увидим, что к списку зарегистрированных пользователей в /etc/passwd будут добавлены сгенерированным нашим расширением пользователи.
Аналогично может быть сделана альтернативная реализация DNS (например, для использования DNS-over-HTTPS можно установить https://github.com/dimkr/nss-tls), извлечение списка групп из внешнего источника (например, для извлечения из PostgreSQL можно использовать https://github.com/jandd/libnss-pgsql).
Мы познакомились с основными идеями использования и реализации NSS. Во второй части статьи мы поговорим про PAM и модули для контроля доступа, проверки политики пароля, создания окружения пользователя (например, домашнего каталога).
Сегодня в 20:00 пройдет бесплатный открытый урок «Текстовый редактор Vim», на котором рассмотрим: типичные методы ввода текста в Линукс, текстовые редакторы командной строки и основы работы в Vim. Регистрация доступна по ссылке для всех желающих.