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

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

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

Казалось бы, хватает двух уровней — root и обычный пользователь. Nginx обслуживает HTTP-трафик на порту 80; для привязки к порту ниже 1024 требуются привилегии. Конфигурационные файлы в /etc/nginx/ должны быть доступны только администратору и самому Nginx. Логи в /var/log/nginx/ принадлежат определённому пользователю. Статические файлы сайта в /var/www/ читаются, но не модифицируются. Если рядом работает PostgreSQL, его процессы не должны иметь доступа к конфигурации Nginx, а Nginx — к файлам базы данных. Одного флага «root / не-root» уже мало: Nginx нужен порт 80, но не /etc/shadow; workers должны читать статические файлы, но не конфигурацию master. Требуется цепочка проверок тоньше, чем бинарный бит.

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

Каждый процесс в 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------) для приватных.

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

Программа passwd должна записать новый хеш пароля в /etc/shadow — файл, принадлежащий root. Но запускает её обычный пользователь. Стандартные 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: маска создания файлов

Nginx создаёт лог-файлы — но с какими правами они получатся? Nginx вызывает 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

Для привязки к порту 80 (порт ниже 1024 — «привилегированный порт» в терминологии Unix) процесс Nginx исторически должен был работать как 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 недостаточно: процесс может иметь разрешённые capabilities, но не активировать их все сразу — это снижает поверхность атаки. Ядро хранит capabilities не прямо в полях task_struct, а в credentials-структуре struct cred (доступ к ней — через указатель task_struct->cred). В cred — пять наборов, три из них в первом приближении:

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

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

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

Ещё два набора работают на уровне execve-границ и критически важны для контейнеров:

Bounding (ограничивающий) — потолок, через который capabilities не могут попасть в permitted нового процесса при execve(). Если capability удалена из bounding, ни file-capabilities, ни inherited-капабилити не восстановят её в потомке. Это основной механизм ограничения: Docker по умолчанию удаляет ~15 capabilities из bounding (включая CAP_SYS_ADMIN, CAP_NET_ADMIN, CAP_SYS_MODULE) до запуска контейнера — значит, ни одна программа внутри контейнера уже не сможет их получить, даже с setcap-файлом.

Ambient (окружающие, Linux 4.3, 2015) — capabilities, которые попадают в permitted и effective нового процесса после execve() без необходимости файловых capabilities. Важная оговорка: ambient сохраняется только при запуске «обычного» файла. Если execve запускает privileged-файл — setuid, setgid или с назначенными file capabilities — ambient сбрасывается в ноль. Это защита: setuid-binary не должен унаследовать посторонние capabilities из окружения. Применяется для wrapper’ов без setcap, которым нужно пробросить capability — systemd-run --user --property=AmbientCapabilities=... использует именно этот набор.

Модель передачи capabilities через execve (по man 7 capabilities). В формулах P — наборы текущего процесса, F — capability-атрибуты исполняемого файла, P' — наборы процесса после execve(); & — пересечение, | — объединение битовых масок:

P'(ambient)     = privileged-файл ? 0 : P(ambient)
P'(permitted)   = (P(inheritable) & F(inheritable)) | (F(permitted) & P(bounding)) | P'(ambient)
P'(effective)   = F(effective) ? P'(permitted) : P'(ambient)
P'(inheritable) = P(inheritable)

Важно: Bounding задаёт жёсткий потолок через F(permitted) & P(bounding) — capability, которой нет в bounding, не попадёт в permitted даже с setcap. Ambient передаётся только при execve обычного файла.

Посмотреть 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 по умолчанию даёт контейнеру лишь часть из ~40 capabilities, остальные удаляет из bounding-набора.

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

Capabilities появились относительно поздно (файловые capabilities — Linux 2.6.24, 2008 год). До них — и во многих системах до сих пор — использовался другой паттерн: запуск с полными привилегиями и последующий сброс до минимума. 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 обратно? Для этого нужно понять, что у процесса не два UID (RUID и EUID), а три. Saved UID (SUID) — третий идентификатор, который ядро сохраняет при смене EUID. Он нужен именно для того, чтобы процесс мог временно понизить привилегии и вернуться: непривилегированный процесс вызывает seteuid(unprivileged), выполняет работу, затем вызывает seteuid(saved_uid) и восстанавливает прежний EUID. Но это работает только когда SUID содержит привилегированное значение.

setuid(), вызванный root-процессом, меняет все три UID одновременно: real, effective и saved. Когда root вызывает setuid(33), RUID, EUID и SUID все становятся 33. SUID больше не хранит 0 — возврата нет. Это необратимый сброс привилегий.

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

$ 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 и границы доступа

Идентичность процесса, rwx-биты на inode, специальные биты, umask, capabilities, сброс привилегий через saved UID — все эти механизмы складываются в один сценарий обработки запроса к 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


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