MAATRIX / Блог / Как SELinux принимает решение и почему отказ не виден в логах приложения

Как SELinux принимает решение и почему отказ не виден в логах приложения

MAATRIX

Приложение внезапно перестаёт открывать файл, слушать порт или писать в сокет — и в логе самого приложения при этом лежит одна строка: Permission denied. Владелец файла правильный, права 644 или 755 на месте, пользователь тот же, что и вчера, когда всё работало. Разработчик перепроверяет права ещё раз, потом ещё раз — и не находит ничего. Причина в том, что классические Unix-права здесь ни при чём: решение принял SELinux, а он ведёт собственный журнал, о котором приложение ничего не знает и в который не заглядывает.

Где именно SELinux встаёт на пути запроса

Когда процесс делает системный вызов — open(), connect(), bind(), read() — ядро Linux не выполняет его сразу. Запрос проходит через цепочку проверок, и SELinux встроен в эту цепочку через механизм LSM (Linux Security Modules) — хуки, которые ядро дёргает в критических точках перед тем, как выполнить операцию.

Порядок проверок для типичной операции с файлом выглядит примерно так:

  1. Ядро получает системный вызов от процесса.
  2. Срабатывает хук LSM — точка, где SELinux (если он загружен и включён) проверяет операцию по своей политике.
  3. Если политика разрешает — ядро переходит к обычной проверке DAC (Discretionary Access Control) — тем самым правам rwx, владельцу и группе, которые все привыкли считать единственным механизмом доступа в Linux.
  4. Только если и DAC разрешил — операция выполняется.

Важный нюанс: SELinux не заменяет права доступа Unix и не идёт после них — он идёт до них, как дополнительный барьер поверх стандартной модели. Классические права rwx никуда не делись и продолжают работать как раньше. Но теперь запрос должен пройти через оба фильтра. Если пройдёт DAC, но не пройдёт политика SELinux — операция всё равно будет отклонена. И наоборот: даже разрешающая политика SELinux не отменит обычный запрет по правам файла. Отказать может любая из двух систем, независимо от другой — и знать, какая именно сработала, приложение не может.

Здесь и рождается путаница, из-за которой администраторы годами воюют с правами файлов, хотя дело в политике SELinux: обе системы возвращают на уровне ядра один и тот же код ошибки — EACCES. Дальше он просто транслируется наверх как «Permission denied», без указания источника.

Что такое метки и почему это не то же самое, что владелец файла

Unix-права опираются на владельца (uid), группу (gid) и биты rwx. SELinux работает поверх принципиально другой модели — мандатного контроля доступа (MAC, Mandatory Access Control). Здесь у каждого объекта — файла, каталога, сокета, порта, процесса — есть метка безопасности, security context, обычно она выглядит так:

system_u:object_r:httpd_sys_content_t:s0

Разберём по частям:

  • system_u — SELinux-пользователь (не путать с Unix-пользователем — это отдельное пространство идентификаторов);
  • object_r — роль;
  • httpd_sys_content_tтип (type), самое важное поле для повседневной работы — именно тип определяет, что можно делать с объектом;
  • s0 — уровень чувствительности (актуален при включённом MLS/MCS, в большинстве серверных сценариев не используется активно).

Процессы тоже несут метку — свой собственный тип, называемый доменом (domain). Например, процесс nginx обычно работает в домене httpd_t. Политика SELinux — это набор правил вида «процесс с типом X может выполнять действие Y над объектом с типом Z». Не больше и не меньше. Если правила для конкретной пары «домен процесса — тип объекта» нет — операция запрещена по умолчанию, это принцип deny by default, на котором строится вся модель.

Отсюда и главная практическая ловушка: файл может быть создан с правильным владельцем и правами, но с неподходящей меткой — например, вы скопировали конфиг из домашнего каталога, и он унаследовал тип user_home_t вместо ожидаемого httpd_config_t. Права rwx при этом абсолютно корректны, chmod и chown ничего не покажут — а сервис всё равно не сможет прочитать файл, потому что тип объекта не тот, который разрешён политикой для этого домена.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Почему приложение видит только «Permission denied» и ничего больше

Здесь ключевой момент всей статьи. Когда SELinux блокирует операцию на уровне LSM-хука, ядро возвращает системному вызову код ошибки EACCES — тот же самый код, что возвращается при обычном отказе по правам Unix. Для процесса, который сделал вызов, оба случая выглядят абсолютно одинаково: вызов не удался, errno — «отказано в доступе».

Приложение не получает от ядра никакого дополнительного признака «это было решение SELinux, а не DAC». Библиотеки языков программирования (glibc, стандартная библиотека Python, JVM и так далее) просто транслируют этот код в исключение или ошибку — PermissionError, EACCES: permission denied, IOException: Permission denied — и это всё, что попадает в лог приложения.

Из этого следует важный вывод для диагностики: само приложение принципиально не может сообщить вам, что виноват SELinux — оно не хранит эту информацию, потому что ядро её ему не передавало. Читать логи nginx, php-fpm, postgresql или собственного демона в поисках причины — бессмысленно, если причина в политике SELinux: там физически нет данных, которые могли бы на неё указать. Общий подход к тому, как вообще искать причину сбоя по логам приложения, разобран в статье про чтение логов и поиск причины сбоя — но применительно к SELinux этот путь заведомо не даст ответа, потому что нужный факт находится в другом журнале.

Решение о блокировке фиксируется не в логе приложения, а в отдельной подсистеме — аудите ядра, и увидеть его можно только там.

Где на самом деле лежит решение SELinux — журнал аудита

Каждое решение SELinux, особенно отказ, генерирует событие аудита ядра — AVC (Access Vector Cache) denial. Это отдельный механизм логирования, независимый от journald-юнита конкретного сервиса и от того, куда сам сервис пишет свои записи.

В зависимости от конфигурации системы событие попадает в один из двух мест:

  • если запущен демон auditd — записи идут в /var/log/audit/audit.log;
  • если auditd не установлен или не запущен, а есть journald — события AVC могут попадать в системный журнал через audit dispatcher, и их можно найти через journalctl, но это менее надёжный путь, чем прямой лог аудита.

Сырая запись AVC выглядит примерно так:

type=AVC msg=audit(1735680000.123:456): avc:  denied  { read } for  pid=1842 comm="nginx" name="secret.conf" dev="dm-0" ino=131099 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:user_home_t:s0 tclass=file permissive=0

Читается это так:

  • denied { read } — какая именно операция запрещена;
  • comm="nginx" — какой процесс её пытался выполнить;
  • scontext=...httpd_t... — источник, то есть домен процесса (source context);
  • tcontext=...user_home_t... — цель, то есть метка объекта (target context);
  • tclass=file — класс объекта, к которому применялось правило;
  • permissive=0 — SELinux работал в режиме Enforcing, то есть реально заблокировал операцию (при permissive=1 он бы только залогировал событие, но пропустил запрос).

Именно по паре scontext/tcontext и видно суть проблемы из предыдущего раздела: процесс с доменом httpd_t пытался прочитать объект с типом user_home_t, для которого политика такого разрешения не выдаёт. Ни ls -la, ни stat на файле этого не покажут — там про метку либо ничего нет, либо она видна только при явном запросе (ls -Z), который мало кто делает по умолчанию, особенно если раньше с SELinux не сталкивался вплотную.

Разбор аудита сервера как отдельной практики — команды, ротация, куда что складывается — подробнее описан в статье про настройку auditd на сервере: AVC-события используют ту же инфраструктуру аудита ядра, что и остальные события безопасности, которые там разбираются.

Практика: как искать причину «необъяснимого» Permission denied

Когда приложение падает с Permission denied, а права и владелец на первый взгляд в порядке, стоит идти по шагам, а не гадать заново с chmod 777.

Шаг 1. Проверить, вообще ли включён и в каком режиме SELinux.

getenforce

Возможные ответы: Enforcing (блокирует), Permissive (только логирует, но не блокирует — тогда причина не в нём) или Disabled.

Шаг 2. Посмотреть последние отказы AVC для нужного процесса или временного окна.

Если стоит auditd:

ausearch -m avc -ts recent

Отфильтровать по имени процесса можно так:

ausearch -m avc -c nginx -ts today

Если auditd не установлен, а события идут через journald:

journalctl -t audit -g denied

Шаг 3. Если событий AVC вообще нет, а Permission denied есть — SELinux не виноват.

Это тоже полезный результат: значит, дело в обычных Unix-правах, ACL, или в ограничениях самого приложения (например, chroot или контейнерной изоляции), и дальше нужно смотреть уже классические ls -la, getfacl, права каталогов на всём пути до файла.

Шаг 4. Если AVC-события есть — понять их одним инструментом, а не вручную.

Пакет setroubleshoot (точнее, утилита sealert) умеет разбирать сырые записи AVC в человекочитаемое объяснение и — часто — готовую команду для исправления:

sealert -a /var/log/audit/audit.log

Шаг 5. Проверить фактическую метку файла и сравнить с ожидаемой.

ls -Z /path/to/file

Если тип не совпадает с тем, что требует tcontext в записи AVC — вероятно, файл был создан или перемещён так, что не унаследовал правильный контекст (частый случай — cp из другого каталога вместо mv внутри одного, или восстановление из бэкапа сторонним архиватором).

Шаг 6. Исправлять контекст, а не отключать SELinux.

Восстановить метку по умолчанию для типа файла:

restorecon -v /path/to/file

Если нужного правила в политике вообще нет (это не ошибка метки, а осознанно новый сценарий использования) — сгенерировать локальный модуль политики из уже накопленных AVC-событий:

audit2allow -a -M mymodule
semodule -i mymodule.pp

Это создаёт точечное разрешение под конкретную операцию, а не снимает защиту целиком.

Чего не стоит делать и почему это ловушка

Самый частый «быстрый» способ избавиться от Permission denied — выполнить setenforce 0 или вовсе отключить SELinux в /etc/selinux/config. Проблема действительно исчезает — потому что исчезает сама проверка. Но вместе с ней исчезает и весь слой защиты, который сдерживает конкретный класс атак: даже если процесс скомпрометирован (RCE в веб-приложении, уязвимая библиотека, случайно открытый upload), SELinux ограничивает, что скомпрометированный процесс физически может сделать в системе, независимо от прав, под которыми он запущен. Отключение SELinux превращает точечную задачу «разрешить nginx читать один файл» в системное решение «убрать барьер для всех процессов на сервере» — эта разница в масштабе последствий и делает такой подход антипаттерном, разбор которого целиком посвящена статья «отключить SELinux и забыть».

Отдельно стоит подсветить два ложных диагноза, которые полезно исключать быстрее, чем разбор AVC-логов:

  • Показалось, что дело в правах, хотя это SELinux. Классический сценарий: разработчик выкладывает новый бинарник или конфиг через scp или rsync с другого хоста, права выставляются верно, а деплой всё равно падает — потому что у файла на новом сервере не тот контекст. Похожая по духу проблема (но с другой первопричиной — переменными окружения и путями при sudo) разобрана в статье про Permission denied при деплое; полезно сравнить оба случая, чтобы быстрее отличать «это SELinux» от «это окружение и права».
  • Показалось, что дело в SELinux, хотя это обычные права. Бывает и наоборот: администратор помнит про SELinux, сразу лезет в ausearch, а AVC-события пустые — значит, проблема банальнее, и время нужно тратить на ls -la и getfacl, а не на политику.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Как быстро понять, виноват ли вообще SELinux, не разбираясь в деталях политики?

Проверьте getenforce. Если ответ Disabled или Permissive — SELinux точно ни при чём (в Permissive он логирует, но не блокирует). Если Enforcing — следующий шаг: посмотреть, есть ли свежие AVC-события через ausearch -m avc -ts recent или journalctl -t audit -g denied. Пустой результат означает, что и в режиме Enforcing конкретно этот отказ дал не SELinux.

Почему chmod 777 иногда «чинит» проблему, а иногда нет?

Потому что chmod меняет только DAC-права, а SELinux проверяет отдельно и до них. Если запрет стоит именно на уровне DAC — chmod 777 его снимает, и кажется, что помогло. Если запрет на уровне политики SELinux — операция как была запрещена, так и останется запрещена, сколько бы прав вы ни выставили. chmod 777 в любом случае — плохая практика: она размывает права для всех, вместо того чтобы решить конкретную задачу для конкретного домена.

Обязательно ли ставить auditd, если события AVC и так видны через journalctl?

Для разовой диагностики journald обычно достаточно, но auditd даёт более полный и структурированный журнал, который меньше теряет записи под нагрузкой и удобнее фильтровать инструментами вроде ausearch и aureport. На серверах, где SELinux используется всерьёз (не просто «стоит по умолчанию»), auditd стоит держать включённым постоянно, а не подключать только в момент проблемы.

Что делать, если sealert недоступен на минимальной установке сервера?

Пакет setroubleshoot-server не всегда стоит по умолчанию на облегчённых образах. Его можно установить отдельно (dnf install setroubleshoot-server на RHEL-совместимых системах), либо работать напрямую с сырыми AVC-записями через ausearch и вручную сопоставлять scontext/tcontext с ожидаемыми — это медленнее, но не требует дополнительного пакета.

SELinux мешает только файлам, или сетевым портам тоже?

Тоже. У портов есть свой тип в политике (например, http_port_t для 80/443), и если сервис слушает нестандартный порт, не привязанный к нужному типу, bind() тоже упадёт с Permission denied — и точно так же без объяснений в логе самого сервиса. Проверить и расширить список портов для типа можно командой semanage port -l и semanage port -a.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →