Никто не любит каждый раз вводить пароли. В мире веб-приложений протоколы для 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'.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#!/usr/bin/env python3 import os import socket SOCK_FILE = '\0counter-server' ## Счетчик counter = 0 ## Создаем сокет sock = socket.socket(family=socket.AF_UNIX, type=socket.SOCK_STREAM) sock.bind(SOCK_FILE) sock.listen() while True: conn, _ = sock.accept() command = conn.recv(1024).decode().strip() if command == 'inc': print("Received an increment request") counter += 1 response = "Success" elif command == 'read': print("Received a read request") response = "Counter value: {0}".format(counter) else: response = "Invalid command" conn.send(response.encode()) conn.send(b'\n') conn.close() |
Попробуем запустить его:
1 2 |
$ sudo ./counter-server.py Counter server started |
Перейдем в другую консоль и попробуем подключиться с помощью socat. В случае с обычным stream-сокетом протокол был бы UNIX-CONNECT, но поскольку наш — «необычный», нужен ABSTRACT-CONNECT.
1 2 3 4 5 6 7 |
$ socat - ABSTRACT-CONNECT:counter-server inc Success $ socat - ABSTRACT-CONNECT:counter-server read Counter value: 1 |
Добавляем авторизацию
Теперь переходим к получению данных о подключившемся процессе через SO_PEERCRED. Для традиционных сокетов и абстрактных она работает одинаково.
Формально, чтобы включить эту опцию, нужно поставить сокету флаг SO_PASSCRED = 1. На практике из Python 3.7 на ядре 5.0.x работает и без его явной установки, но лишним не будет.
1 2 3 4 5 |
sock = socket.socket(family=socket.AF_UNIX, type=socket.SOCK_STREAM) ## Установка опции для передачи данных о клиенте sock.setsockopt(socket.SOL_SOCKET, socket.SO_PASSCRED, 1) sock.bind(SOCK_FILE) sock.listen() |
Прочитать данные о клиенте можно с помощью функции socket.getsockopt с флагом SO_PEERCRED. В качестве третьего аргумента обязательно нужно указать размер буфера для чтения. Укажем 1024 — это гораздо больше, чем достаточно:
1 |
cred = conn.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 1024) |
Эта функция вернет нам объект типа bytes, из которого еще нужно извлечь информацию. Увы, встроенной функциональности для этого в Python нет, поэтому разбирать придется самим. Из man 7 unix можно узнать, что она возвращает запись типа ucred из <sys/socket.h>:
1 2 3 4 5 |
struct ucred { pid_t pid; /* process ID of the sending process */ uid_t uid; /* user ID of the sending process */ gid_t gid; /* group ID of the sending process */ }; |
Стандарт POSIX не определяет размер этих типов, но на практике в Linux они все — 32-разрядные знаковые целые. Таким образом, мы можем разобрать ее на части с помощью struct.unpack('iii', cred) (не забудь добавить import struct).
РЕКОМЕНДУЕМ:
Автоматизация системы мониторинга с помощью Zabbix LLD
Модифицируем основной цикл нашего сервера:
1 2 3 4 5 6 |
while True: conn, _ = sock.accept() cred = conn.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 1024) pid, uid, gid = struct.unpack('iii', cred) print("Connection from PID={0} UID={1} GID={2}".format(pid, uid, gid)) |
Теперь, если запустить сервер и подключиться к нему, мы увидим картину вроде такой:
1 2 3 4 |
$ sudo ./counter-server.py Counter server started Connection from PID=8108 UID=1003 GID=1003 Received a read request |
Уже неплохо. Осталось только сравнить идентификатор группы (GID) с желаемой группой wheel.
Получаем информацию о группах
Эта часть самая простая и легко выполняется средствами стандартной библиотеки Python.
Функции для этих целей находятся в модулях pwd и grp. Имена функций и возвращаемые словари совпадают с POSIX вплоть до названий полей, хотя в Python с его поддержкой пространств имен grp_gid и подобное выглядит немного странно.
1 2 3 4 |
import pwd import grp GROUP_ALLOW = 'wheel' |
Для примера мы используем группу администраторов по умолчанию в системах на основе Red Hat, но на ее месте могла быть любая группа.
Перепишем обработку команды inc с проверкой прав пользователя. Поскольку по умолчанию процесс получает GID основной (первой) группы пользователя, для удобства в ущерб производительности мы получим имя пользователя по его UID с помощью getpwuid и будем проверять наличие этого имени в группе (поле gr_mem в словаре, который возвращает grp.getgrnam):
1 2 3 4 5 6 7 8 9 10 |
if command == 'inc': print("Received an increment request") login = pwd.getpwuid(uid).pw_name group = grp.getgrnam(GROUP_ALLOW) if login in group.gr_mem: counter += 1 response = "Success" else: response = "You don't have a permission" |
Теперь пользователь будет получать сообщение об ошибке, если не состоит в группе wheel:
1 2 3 |
$ sudo -u nobody socat - ABSTRACT-CONNECT:counter-server inc You don’t have a permission |
Переносимость
К сожалению, SO_PEERCRED не стандартизована в POSIX, и детали реализации в разных системах несколько отличаются. В частности, OpenBSD использует другой порядок значений: uid, gid, pid, а не pid, uid, gid. Другие системы используют похожие по смыслу, но отличающиеся в реализации опции, например SCM_CRED в FreeBSD. В самом Linux также есть механизм SCM_CREDENTIALS, который позволяет передавать любой UID и GID, на которые у пользователя есть права.
РЕКОМЕНДУЕМ:
Как написать веб-приложение устойчивое к ботнетам
Универсальную библиотеку, которая скрывала бы эти различия от пользователя и работала одинаково на всех системах, я не нашел, но частичные реализации для разных языков отыскать вполне можно.
Что интересно, относительно переносимая getpeereid(), которая возвращает только UID и GID, в стандартную библиотеку Python и многих других языков не входит. Но если задаться целью, все реализуемо.