Права доступа и capabilities

Планировщик | Синхронизация

Планировщик распределяет процессорное время: выбирает поток с наименьшим vruntime, вытеснение через аппаратный таймер гарантирует, что ни один поток не захватит ядро навсегда. Но планировщик решает только вопрос «кому дать CPU». Он не спрашивает, имеет ли процесс право читать /etc/shadow, привязываться к порту 80 или отправлять сигнал чужому процессу. Эти вопросы принадлежат другой подсистеме ядра — контролю доступа (access control).

Рассмотрим конкретную ситуацию. Nginx обслуживает HTTP-трафик на порту 80. Для привязки к порту ниже 1024 требуются привилегии. Конфигурационные файлы в /etc/nginx/ должны быть доступны только администратору и самому Nginx. Логи в /var/log/nginx/ принадлежат определённому пользователю. Статические файлы сайта в /var/www/ читаются Nginx, но не модифицируются. Если на той же машине работает PostgreSQL, его процессы не должны иметь доступа к конфигурации Nginx, а Nginx — к файлам базы данных. Как ядро обеспечивает все эти границы?

Идентичность процесса

Каждый процесс в Linux имеет владельца. В заметке о процессах мы видели, что task_struct хранит UID и GID. На самом деле идентичность процесса устроена сложнее — у него не одна пара UID/GID (User ID / Group ID), а несколько.

Real UID и real GID (RUID, RGID) — идентификатор пользователя, который запустил процесс. Администратор вошёл в систему как root (UID 0) и набрал su - www-data — shell, порождённый su, получает RUID пользователя www-data (UID 33). Real UID определяет, кому принадлежит процесс: сигнал SIGTERM можно отправить только процессу с тем же RUID (или будучи root).

Effective UID и effective GID (EUID, EGID) — идентификатор, который ядро проверяет при доступе к ресурсам. Когда процесс вызывает open("/etc/nginx/nginx.conf", O_RDONLY), ядро сравнивает EUID процесса с UID владельца файла в inode. В большинстве случаев EUID совпадает с RUID. Различие появляется при setuid-программах, о которых речь ниже.

Зачем два идентификатора? Real UID отвечает на вопрос «кто запустил», effective UID — на вопрос «с чьими правами работает». Обычный пользователь запускает passwd для смены пароля. Программа должна записать новый хеш в /etc/shadow, который доступен только root. Решение: файл /usr/bin/passwd имеет специальный бит (setuid), из-за которого процесс получает EUID = 0 (root), сохраняя RUID обычного пользователя. Программа работает с привилегиями root, но знает, кто её запустил.

Supplementary groups (дополнительные группы) — пользователь может входить в несколько групп одновременно. Пользователь deploy входит в группы deploy, www-data и docker. Процесс, запущенный от deploy, наследует все три группы. При проверке доступа ядро сравнивает GID файла со всеми группами процесса, а не только с primary GID.

Увидеть полную идентичность текущего пользователя можно командой id:

$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data),4(adm),27(sudo)

Информация о пользователях хранится в /etc/passwd (имя, UID, GID, домашний каталог, shell), о группах — в /etc/group (имя группы, GID, список членов). Пароли (хеши) — в /etc/shadow, доступном только root.

Алгоритм проверки прав доступа

В заметке о файловых системах мы видели, что inode хранит три набора rwx-битов: для владельца (owner), группы (group) и остальных (others). Когда процесс вызывает open(), ядро выполняет проверку, сравнивая идентичность процесса с метаданными inode.

Первый шаг — проверка EUID. Если EUID процесса равен 0 (root), проверка прав пропускается: root имеет доступ ко всему. Это упрощает администрирование, но создаёт фундаментальную проблему безопасности — любая ошибка в процессе root даёт полный контроль над системой.

Если EUID не равен 0, ядро проходит три уровня:

  1. Если EUID процесса совпадает с UID владельца файла — применяются биты owner (первая тройка rwx). Остальные уровни не проверяются.
  2. Если EUID не совпал, но EGID процесса или одна из supplementary groups совпадает с GID файла — применяются биты group (вторая тройка).
  3. Если ни UID, ни GID не совпали — применяются биты others (третья тройка).

Важный момент: уровни не складываются. Если пользователь — владелец файла, применяются только биты owner, даже если биты group дают больше прав.

inode /etc/nginx/nginx.conf
  owner: root (uid=0)
  group: root (gid=0)
  permissions: rw-r----- (0640)

  процесс с euid=0 (root):  -> uid == 0 -> доступ разрешён (любой)
  процесс с euid=33 (www-data), groups=[33,0]:
                             -> euid != uid владельца (0)
                             -> gid 0 в groups? да
                             -> group bits: r-- -> чтение разрешено, запись нет
  процесс с euid=999 (postgres), groups=[999]:
                             -> euid != 0, gid 0 не в groups
                             -> others: --- -> доступ запрещён

В этом примере Nginx (работающий с supplementary group root) может читать конфигурацию, но не изменять её. PostgreSQL не видит файл вообще.

Для файлов три бита означают: r (read) — чтение содержимого, w (write) — изменение содержимого, x (execute) — исполнение как программы.

Для директорий семантика другая: r — чтение списка имён файлов (ls), w — создание и удаление файлов в директории, x — вход в директорию (cd) и обращение к файлам по пути через неё. Без x на директории невозможно обратиться ни к одному файлу внутри, даже если на самих файлах стоят разрешающие права. Без r, но с x можно открыть файл по известному имени, но не получить список содержимого.

Это объясняет типичные права директорий: 755 (rwxr-xr-x) для публичных каталогов, 750 (rwxr-x---) для каталогов группы, 700 (rwx------) для приватных.

Специальные биты

Стандартные rwx-биты не решают все задачи. Три специальных бита расширяют модель.

Setuid

Бит setuid (set user ID on execution) — при запуске исполняемого файла процесс получает EUID, равный UID владельца файла, а не UID запустившего пользователя. Числовое значение — 4000 в восьмеричной нотации, отображается как s вместо x в правах владельца:

$ ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 68208 Mar 23 10:00 /usr/bin/passwd

Буква s в позиции execute владельца означает: любой пользователь может запустить passwd, но процесс получит EUID = 0 (root). Программа passwd использует привилегии root для записи в /etc/shadow, но проверяет RUID, чтобы не дать пользователю изменить чужой пароль.

Setuid-программы — постоянный источник уязвимостей. Баг в программе с setuid root даёт атакующему привилегии root. Поэтому setuid-программ в системе минимум: passwd, su, sudo, ping (хотя ping в современных дистрибутивах использует capabilities вместо setuid).

Setuid на скриптах (bash, python) в Linux игнорируется: между exec() скрипта и началом его интерпретации существует окно, в котором файл можно подменить. Ядро предотвращает эту атаку, запрещая setuid для интерпретируемых файлов.

Setgid

Бит setgid (set group ID, числовое значение 2000) работает аналогично setuid, но для группы: процесс получает EGID, равный GID файла.

На директориях setgid имеет другой эффект: файлы, создаваемые внутри такой директории, наследуют GID директории, а не primary GID создающего процесса. Это решает проблему совместной работы. Команда веб-разработчиков использует общую директорию /var/www/project/:

$ chmod 2775 /var/www/project/
$ chown root:www-data /var/www/project/

Без setgid каждый разработчик создавал бы файлы с GID своей primary group — другие члены команды не смогли бы их изменить. С setgid все файлы автоматически получают GID www-data, и любой член группы имеет к ним доступ.

Sticky bit

Sticky bit (числовое значение 1000) применяется к директориям. В директории со sticky bit удалить или переименовать файл может только владелец файла, владелец директории или root — даже если права на запись в директорию есть у всех.

Классический пример — /tmp:

$ ls -ld /tmp
drwxrwxrwt 15 root root 4096 Mar 23 10:00 /tmp

Буква t в конце означает sticky bit. Все пользователи могут создавать файлы в /tmp (права 777), но удалить чужой файл не могут. Без sticky bit пользователь Alice могла бы удалить временные файлы пользователя Bob, потому что у неё есть write-доступ к директории /tmp.

umask: маска создания файлов

Когда процесс вызывает open("/var/log/nginx/access.log", O_CREAT | O_WRONLY, 0666), он указывает желаемые права 0666 (rw-rw-rw-). Но ls -l покажет права 0644 (rw-r—r—). Куда делись биты записи для группы и остальных?

Ядро применяет umask (user file-creation mask) — битовую маску, которая убирает указанные биты из запрошенных прав. Итоговые права вычисляются как mode & ~umask. При umask 022:

запрошено:  0666  ->  rw-rw-rw-
umask:      0022  ->  ----w--w-
~umask:     0755  ->  rwxr-xr-x
результат:  0666 & 0755 = 0644  ->  rw-r--r--

umask 022 означает: у группы и остальных убирается бит записи. Это значение по умолчанию для большинства дистрибутивов. Для директорий запрошенные права обычно 0777, и с umask 022 результат — 0755 (rwxr-xr-x).

Почему open() запрашивает 0666, а не 0644 напрямую? Потому что umask — это политика пользователя, а не программы. Программа говорит «мне нужен файл для чтения и записи всеми», а umask конкретного окружения решает, разрешить ли это. На сервере с umask 077 тот же вызов создаст файл с правами 0600 (rw-------) — доступ только для владельца.

Nginx master process обычно запускается с umask 022 или 027 (umask 027 убирает все права для others). Текущую маску можно посмотреть командой umask, установить — umask 027.

Capabilities: разделение привилегий root

Вернёмся к Nginx. Для привязки к порту 80 (порт ниже 1024 — «привилегированный порт» в терминологии Unix) процесс исторически должен был работать как root. Но root имеет полный контроль над системой: может читать любой файл, убивать любой процесс, загружать модули ядра, менять настройки сети. Nginx нужен один конкретный привилегий — привязка к низкому порту. Давать ради этого доступ ко всей системе — нарушение принципа наименьших привилегий (principle of least privilege).

Linux capabilities (процессные capabilities появились в ядре 2.2, 1999 год; файловые capabilities — возможность назначать capabilities исполняемым файлам через расширенные атрибуты — добавлены в 2.6.24, 2008 год) разбивают привилегии root на ~40 независимых флагов. Каждый флаг разрешает конкретную операцию:

CAP_NET_BIND_SERVICE — привязка к привилегированным портам (ниже 1024). Это единственная capability, нужная Nginx для работы на порту 80.

CAP_SYS_PTRACE — подключение к чужому процессу через ptrace(). Нужна отладчикам (gdb, strace), но опасна в продакшене: позволяет читать память других процессов.

CAP_NET_RAW — создание raw-сокетов. Нужна утилите ping для отправки ICMP-пакетов (Internet Control Message Protocol).

CAP_CHOWN — изменение владельца файла. Без неё даже root (если capabilities ограничены) не сможет выполнить chown.

CAP_DAC_OVERRIDE — игнорирование rwx-битов на файлах. По сути, это и есть та часть «всемогущества root», которая позволяет читать и писать любые файлы.

CAP_SYS_ADMIN — самая широкая capability, которую иногда называют «новый root». Покрывает монтирование файловых систем, управление namespaces, настройку cgroups и десятки других операций.

Наборы capabilities

У каждого потока три набора capabilities, хранящихся в task_struct:

Permitted (разрешённые) — максимальный набор capabilities, которые поток может активировать. Это верхняя граница: поток не может получить capability, которой нет в permitted.

Effective (действующие) — набор capabilities, которые ядро проверяет прямо сейчас. Системный вызов bind() проверяет: есть ли CAP_NET_BIND_SERVICE в effective наборе? Effective всегда является подмножеством permitted.

Inheritable (наследуемые) — capabilities, которые могут быть переданы через execve() дочернему процессу. Механизм наследования сложен и используется редко — на практике capabilities чаще назначаются через файловые атрибуты.

Посмотреть capabilities процесса можно через /proc/<pid>/status:

$ grep Cap /proc/1/status
CapInh: 0000000000000000
CapPrm: 000001ffffffffff
CapEff: 000001ffffffffff

Утилита capsh декодирует битовую маску в имена:

$ capsh --decode=000001ffffffffff
0x000001ffffffffff=cap_chown,cap_dac_override,...,cap_net_bind_service,...

Capabilities на файлах

Capabilities можно назначить исполняемому файлу, аналогично setuid, но гораздо точнее. Утилита setcap записывает capabilities в расширенные атрибуты файла:

$ setcap cap_net_bind_service=+ep /usr/sbin/nginx

Флаги после = определяют, в какие наборы добавить capability: e — effective, p — permitted. После этой команды Nginx может привязываться к порту 80 без root. Проверить capabilities файла — getcap:

$ getcap /usr/sbin/nginx
/usr/sbin/nginx cap_net_bind_service=ep

Другой пример — ping. В старых системах /bin/ping имел setuid root. В современных дистрибутивах вместо этого:

$ getcap /bin/ping
/bin/ping cap_net_raw=ep

Процесс ping получает единственную capability CAP_NET_RAW — достаточно для отправки ICMP, но не для чтения чужих файлов, убийства процессов или монтирования дисков.

Контейнеры активно используют capabilities для ограничения привилегий. Docker по умолчанию оставляет контейнеру только 14 capabilities из ~40 возможных: CHOWN, DAC_OVERRIDE, FSETID, FOWNER, MKNOD, NET_RAW, SETGID, SETUID, NET_BIND_SERVICE и несколько других. Флаг --cap-drop=ALL --cap-add=NET_BIND_SERVICE оставляет контейнеру единственную capability — привязку к низким портам.

Сброс привилегий: паттерн безопасного демона

Capabilities объясняют, как дать процессу минимум привилегий. Но на практике Nginx использует другой, проверенный временем паттерн: запуск с полными привилегиями и последующий сброс до минимума.

Nginx запускается как root. Master process выполняет привилегированные операции: читает конфигурацию (/etc/nginx/nginx.conf, принадлежащий root), открывает лог-файлы, вызывает bind() для порта 80. После этого master порождает worker processes, которые обрабатывают HTTP-запросы.

Критический момент — порядок сброса привилегий. Worker process выполняет три шага строго в указанной последовательности:

1. setgid(gid)    -- сменить effective и real GID на www-data
2. setuid(uid)    -- сменить effective и real UID на www-data
3. (порт уже открыт master'ом, fd унаследован через fork)

Порядок setgid перед setuid критичен. Вызов setuid(uid) меняет EUID с 0 на непривилегированное значение. После этого процесс больше не root и не может вызвать setgid() — для изменения GID нужен EUID 0 или capability CAP_SETGID. Если вызвать setuid первым, setgid вернёт ошибку EPERM.

master process (uid=0, gid=0)
  |
  | bind(80) -- привилегированная операция, нужен root
  | open("/var/log/nginx/access.log") -- файлы root:adm
  |
  +-- fork() --> worker process (uid=0, gid=0)
                   |
                   | setgid(33)  -- gid = www-data
                   | setuid(33)  -- uid = www-data
                   |             -- с этого момента: uid=33, gid=33
                   |             -- порт 80 доступен через fd от master
                   |             -- /etc/shadow недоступен
                   |             -- повторный setuid(0) вернёт EPERM
                   |
                   v
                 обработка HTTP-запросов

Почему нельзя просто вернуть EUID 0 обратно? Потому что setuid(), вызванный root-процессом, меняет все три UID одновременно: real, effective и saved. Saved UID (SUID) — третий идентификатор, который обычно позволяет переключаться между RUID и EUID. Когда root вызывает setuid(33), RUID, EUID и SUID все становятся 33. Возврата нет — это необратимый сброс привилегий.

Вот что видно в системе:

$ ps aux | grep nginx
root       900  ...  nginx: master process /usr/sbin/nginx
www-data   901  ...  nginx: worker process
www-data   902  ...  nginx: worker process

Master работает как root, workers — как www-data. Если в worker обнаружится уязвимость, атакующий получит права www-data, а не root. Он сможет читать статические файлы сайта, но не /etc/shadow, не конфигурацию Nginx и не данные PostgreSQL.

Конфигурация Nginx задаёт пользователя директивой user:

user www-data;
worker_processes 4;

Полная картина: Nginx и границы доступа

Соберём все механизмы вместе и проследим, как они защищают систему.

Master process (UID 0) стартует. Он читает /etc/nginx/nginx.conf (owner root, rights 0640) — EUID 0 проходит проверку root. Открывает /var/log/nginx/access.log (owner www-data:adm, rights 0640) — root обходит проверку. Выполняет bind() на порт 80 — UID 0 имеет все capabilities. Вызывает fork() для каждого worker. Workers вызывают setgid(33), затем setuid(33) — необратимый сброс до www-data.

Worker (UID 33) получает запрос на файл /var/www/site/index.html (owner www-data:www-data, rights 0644). Ядро сравнивает EUID 33 с UID владельца 33 — совпадение, применяются owner bits: rw-. Чтение разрешено.

Атакующий через уязвимость в обработчике запросов получает выполнение кода в контексте worker. Он пробует open("/etc/shadow", O_RDONLY). Файл: owner root:shadow, rights 0640. EUID 33 != 0, GID 33 не входит в group shadow, others bits: ---. Доступ запрещён — EACCES.

Атакующий пробует bind() на порт 443 для перехвата HTTPS. Порт ниже 1024 — ядро проверяет CAP_NET_BIND_SERVICE в effective наборе worker. Capability отсутствует (сброшена вместе с привилегиями). Возвращается EACCES.

Атакующий пробует kill(950, SIGTERM) для завершения PostgreSQL — PID (Process ID) 950, UID 999. Ядро проверяет: EUID отправителя (33) не равен EUID получателя (999) и не равен 0. Сигнал не доставлен — EPERM.

Каждый уровень — идентичность процесса, rwx-биты на inodes, capabilities — формирует границу. Ни один из них не является абсолютной защитой сам по себе, но вместе они реализуют DAC (Discretionary Access Control, дискреционный контроль доступа) — модель, в которой владелец ресурса решает, кому дать доступ.

DAC имеет фундаментальное ограничение: он доверяет пользователям. Пользователь может установить права 0777 на свои файлы, setuid-программа может содержать баг, root по-прежнему обходит все проверки. Следующий слой защиты — MAC (Mandatory Access Control, мандатный контроль доступа), реализованный в SELinux (Security-Enhanced Linux) и AppArmor: политики безопасности задаются администратором и не могут быть изменены владельцем ресурса. Расширенные ACL (Access Control Lists, setfacl/getfacl) позволяют назначать права конкретным пользователям и группам за пределами модели owner/group/others. PAM (Pluggable Authentication Modules) управляет аутентификацией — кто может войти в систему и при каких условиях. Эти механизмы — тема отдельных заметок.

Sources


Планировщик | Синхронизация