Как известно, пользователи и группы в 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. Регистрация доступна по ссылке для всех желающих.

Комментарии (0)