Системная аутентификация с сокетами UNIX

Системная аутентификация с сокетами UNIX

Никто не любит каждый раз вводить пароли. В мире веб-приложений протоколы для SSO (Single Sign-On) довольно распространены и очень легко реализуемы благодаря встроенной в браузер возможности хранить cookie. Ряд популярных приложений, к примеру консольный клиент PostgreSQL (psql), предоставляют такую возможность для локальных подключений через сокеты UNIX. В данной статье мы рассмотрим, как это сделать с помощью опции SO_PEERCRED.

В качестве примера мы напишем сервер, который просто хранит какое-то число, при этом пользователи из группы wheel могут его изменять, а все остальные — только получать значение. Писать будем на Python, но все сказанное применимо к любому языку. В этой статье речь идет о Linux, если не указано обратное.

Абстрактные сокеты Linux

Для взаимодействия с сервером мы будем использовать вариант UNIX domain sockets — abstract namespace sockets. В отличие от сетевых сокетов, в случае с локальными сокетами UNIX ядро знает, какой процесс подключается к сокету, и знает все о пользователе (создателе) процесса, что и позволяет переиспользовать системную аутентификацию.

В классической реализации сокеты UNIX представляют собой файлы. Это позволяет применить к ним права доступа и запретить пользователям или группам подключаться к ним, но более сложную модель привилегий на этом не построить.

РЕКОМЕНДУЕМ:
Лучший редактор бинарных файлов для Windows

Основной недостаток классической реализации — опция SO_REUSEADDR для них не работает. Файл сокета должен быть создан процессом, который на нем слушает. Если процесс упал, а файл сокета остался, то новому процессу сначала нужно его удалить. Правильно написанный сервер должен использовать lock-файлы, чтобы предотвратить случайный запуск нескольких экземпляров процесса и обеспечить безопасное удаление старого файла.

Для решения этой проблемы в Linux существуют так называемые abstract namespace sockets. По своей сути они идентичны традиционным сокетам UNIX, но не являются файлами и автоматически исчезают с завершением процесса, который их создал. К ним также неприменимы обычные права доступа, и авторизация остается на совести приложения — но мы ведь к этому и стремимся.

Чтобы создать абстрактный сокет, нужно добавить в начало его «пути к файлу» нулевой байт. В остальном все так же, как с обычными.

Пишем сервер

Для начала мы напишем основу для сервера, пока без авторизации. Чтобы не писать разбор сообщений, мы сделаем всего две команды без аргументов: read (вернуть значение счетчика) и inc (увеличить счетчик на единицу).

Сокет мы назовем counter-server. Соответственно, путь его будет '\0counter-server'.

Попробуем запустить его:

Перейдем в другую консоль и попробуем подключиться с помощью socat. В случае с обычным stream-сокетом протокол был бы UNIX-CONNECT, но поскольку наш — «необычный», нужен ABSTRACT-CONNECT.

Добавляем авторизацию

Теперь переходим к получению данных о подключившемся процессе через SO_PEERCRED. Для традиционных сокетов и абстрактных она работает одинаково.

Формально, чтобы включить эту опцию, нужно поставить сокету флаг SO_PASSCRED = 1. На практике из Python 3.7 на ядре 5.0.x работает и без его явной установки, но лишним не будет.

Прочитать данные о клиенте можно с помощью функции socket.getsockopt с флагом SO_PEERCRED. В качестве третьего аргумента обязательно нужно указать размер буфера для чтения. Укажем 1024 — это гораздо больше, чем достаточно:

Эта функция вернет нам объект типа bytes, из которого еще нужно извлечь информацию. Увы, встроенной функциональности для этого в Python нет, поэтому разбирать придется самим. Из man 7 unix можно узнать, что она возвращает запись типа ucred из <sys/socket.h>:

Стандарт POSIX не определяет размер этих типов, но на практике в Linux они все — 32-разрядные знаковые целые. Таким образом, мы можем разобрать ее на части с помощью struct.unpack('iii', cred) (не забудь добавить import struct).

РЕКОМЕНДУЕМ:
Автоматизация системы мониторинга с помощью Zabbix LLD

Модифицируем основной цикл нашего сервера:

Теперь, если запустить сервер и подключиться к нему, мы увидим картину вроде такой:

Уже неплохо. Осталось только сравнить идентификатор группы (GID) с желаемой группой wheel.

Получаем информацию о группах

Эта часть самая простая и легко выполняется средствами стандартной библиотеки Python.

Функции для этих целей находятся в модулях pwd и grp. Имена функций и возвращаемые словари совпадают с POSIX вплоть до названий полей, хотя в Python с его поддержкой пространств имен grp_gid и подобное выглядит немного странно.

Для примера мы используем группу администраторов по умолчанию в системах на основе Red Hat, но на ее месте могла быть любая группа.

Перепишем обработку команды inc с проверкой прав пользователя. Поскольку по умолчанию процесс получает GID основной (первой) группы пользователя, для удобства в ущерб производительности мы получим имя пользователя по его UID с помощью getpwuid и будем проверять наличие этого имени в группе (поле gr_mem в словаре, который возвращает grp.getgrnam):

Теперь пользователь будет получать сообщение об ошибке, если не состоит в группе wheel:

Переносимость

К сожалению, SO_PEERCRED не стандартизована в POSIX, и детали реализации в разных системах несколько отличаются. В частности, OpenBSD использует другой порядок значений: uid, gid, pid, а не pid, uid, gid. Другие системы используют похожие по смыслу, но отличающиеся в реализации опции, например SCM_CRED в FreeBSD. В самом Linux также есть механизм SCM_CREDENTIALS, который позволяет передавать любой UID и GID, на которые у пользователя есть права.

РЕКОМЕНДУЕМ:
Как написать веб-приложение устойчивое к ботнетам

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

Что интересно, относительно переносимая getpeereid(), которая возвращает только UID и GID, в стандартную библиотеку Python и многих других языков не входит. Но если задаться целью, все реализуемо.

Понравилась статья? Поделиться с друзьями:
Добавить комментарий