Ситуация, знакомая каждому администратору Fedora, Rocky Linux или AlmaLinux. Права на каталог образцовые, владелец apache, режим 755, конфигурация выверена построчно, а страница отвечает 403 и пишет в лог Permission denied. Root читает тот же файл без вопросов, обычные пользователи тоже. Логично лишь одно: в этих дистрибутивах работает второй, независимый слой контроля, и его жалобы лежат не в error_log, а в журнале аудита ядра. Дальше по порядку: устройство меток SELinux, перевод системы в разрешающий режим для диагностики, чтение отказов и восстановление правильных контекстов командой restorecon.
Контекст как второй слой прав поверх привычных rwx
Традиционные права Unix называют дискреционными, DAC. Владелец сам решает, кто может читать и писать его файлы, поэтому любая программа, запущенная от его имени, автоматически получает доступ ко всему его хозяйству. SELinux добавляет мандатный контроль, MAC, где решение принимает не владелец, а политика, загруженная в ядро при старте системы. Обойти её сменой владельца или chmod невозможно, ведь редактирует её только администратор.
Каждому процессу и каждому объекту, файлу, каталогу, сокету, порту, присвоена метка, SELinux-контекст. Внутри четыре поля через двоеточия, например unconfined_u:unconfined_r:httpd_t:s0. Это пользователь SELinux, роль, тип и уровень MLS. Значимо почти только третье поле, тип: именно он участвует в большинстве правил и заканчивается суффиксом _t. Процесс веб-сервера живёт в домене httpd_t, содержимое /var/www/html размечено типом httpd_sys_content_t, временные файлы несут тип tmp_t.
Философия политики сурова. Запрещено всё, что не разрешено явно. Нет allow-правила для пары домен и тип, значит ядро вернёт EACCES, приложение покажет Permission denied, а журнал получит запись. Решения при этом кэшируются в AVC, Access Vector Cache, чтобы не пересчитывать правила на каждый чих. Аналогия напрашивается сама, это пропускной режим в офисном здании: дверь кабинета может быть не заперта, но без отметки в бейдже охрана внутрь не пустит. Так httpd_t спокойно читает httpd_sys_content_t и не читает ни домашние каталоги, ни файлы базы данных MariaDB, даже если те лежат в папках, открытых на чтение всем. Взломанный веб-сервер остаётся заперт в собственном коридоре.
Ещё одна деталь, о которой забывают. SELinux проверяет доступ после обычных прав Unix: если доступ закрыл chmod, до политики дело не дойдёт, и записи об отказе в аудите не появится, поэтому диагностика идёт по порядку, сначала rwx и владельцы, потом метки. По умолчанию в Fedora, RHEL и производных стоит политика selinux-policy-targeted, что подтверждает параметр SELINUXTYPE=targeted в /etc/selinux/config. Под ограничение попадают сетевые службы, а пользовательские сеансы живут в свободном домене unconfined_t, поэтому рабочий стол и терминал себя обычно никак не проявляют.
Метки файлов и процессов через ls -Z, ps -Z и sestatus
Просмотр контекстов встроен в стандартные утилиты, достаточно флага -Z. Команда ls -Z /var/www/html/ покажет напротив каждого файла полную метку вроде unconfined_u:object_r:httpd_sys_content_t:s0, ls -lZ совместит её с привычным длинным списком, ps -Z покажет метки процессов, а id -Z собственный контекст сессии. Пользователь, вошедший по SSH обычным способом, увидит unconfined_u:unconfined_r:unconfined_t:s0, а под ограниченным входом guest_u набор полей совсем другой, вплоть до запрета на запуск большинства программ.
Полную картину состояния собирает sestatus, выполняется он от root:
$ sestatus
SELinux status: enabled
SELinuxfs mount: /sys/fs/selinux
SELinux root directory: /etc/selinux
Loaded policy name: targeted
Current mode: enforcing
Mode from config file: enforcing
Policy MLS status: enabled
Policy deny_unknown status: allowed
Memory protection checking: actual (secure)
Max kernel policy version: 31
Из вывода видно три вещи: подсистема включена, система в принуждающем режиме enforcing, конфигурационный файл согласен с текущим состоянием. Разница между Current mode и Mode from config file сама подсказка: если в живой системе permissive, а в конфиге enforcing, значит кто-то дал setenforce 0 и забыл вернуть. Строка Max kernel policy version 31 сообщает максимальную версию формата политики, которую поддерживает ядро.
Отдельная жемчужина диагностики, утилита matchpathcon. Она сравнивает фактическую метку с положенной по правилам политики, а с флагом -V печатает только проблемные файлы:
$ matchpathcon -V /var/www/html/*
/var/www/html/index.html has context unconfined_u:object_r:user_home_t:s0, should be system_u:object_r:httpd_sys_content_t:s0
Файл лежит в каталоге веб-сервера, а метка у него от домашнего каталога, тип user_home_t там жить не должен. Классика жанра, когда содержимое переносили командой mv из домашней папки. mv сохраняет исходную метку, cp создаёт файл заново по правилам целевого каталога, и одно это различие объясняет, почему скопированный сайт работает, а перемещённый нет.
Разрешающий режим Permissive для диагностики вместо полного отключения
У SELinux три состояния: enforcing, permissive и disabled. Enforcing принуждает политику. Permissive ничего не блокирует, но пишет в журнал, что было бы запрещено, причём каждый уникальный отказ фиксируется один раз. Для диагностики permissive идеален: сервис ведёт себя как в обычной работе, а журнал тем временем собирает полный список претензий, который изучают в удобный момент.
Быстрое переключение без перезагрузки делает setenforce, изменения действуют до перезагрузки:
# setenforce 0
# getenforce
Permissive
# setenforce 1
# getenforce
Enforcing
Чтобы режим пережил перезагрузку, в /etc/selinux/config строку SELINUX=enforcing меняют на SELINUX=permissive и перезапускают машину. Там же стоит SELINUXTYPE=targeted, выбор между targeted-политикой и mls, его трогать не нужно.
Точечный приём: ослабить один домен, не трогая всю систему. Команда semanage permissive -a httpd_t переводит в permissive только веб-сервер, остальная система продолжает принуждать политику в полную силу. Список таких доменов покажет semanage permissive -l, возврат в строгий режим сделает semanage permissive -d httpd_t. Когда из-за политики машина вообще не загружается, в меню GRUB нажимают клавишу e на выбранной строке, дописывают в конец строки, начинающейся с linux, параметр enforcing=0 и запускают систему через Ctrl+X. Для постоянной правки параметров ядра есть grubby: sudo grubby --update-kernel ALL --args enforcing=0, откат выполняется через --remove-args.
И совет, противоположный вредной привычке. Не отключайте SELinux совсем. Строка SELINUX=disabled, а тем более параметр ядра selinux=0, лишает файлы меток: всё созданное при выключенной подсистеме остаётся без контекста, и при обратном включении частично размеченная файловая система способна не дать системе загрузиться. Лечение одно, полная переразметка командой fixfiles -F onboot либо созданием файла /.autorelabel: при следующей загрузке все файлы получат метки заново, на терабайтном диске это десятки минут, поэтому планируют такое заранее.
Где искать отказы SELinux и как читать записи аудита
Когда сервис не может прочитать файл, ответ лежит в /var/log/audit/audit.log, туда служба auditd складывает события ядра. Сырой файл тяжело читать, поэтому запросы строят через ausearch по типам сообщений:
# ausearch -m AVC,USER_AVC,SELINUX_ERR,USER_SELINUX_ERR -ts recent
Значение recent означает последние десять минут, today берёт весь текущий день. Типичный отказ выглядит так:
type=AVC msg=audit(1395177286.929:1638): avc: denied { read } for pid=6591 comm="httpd" name="webpages" dev="0:37" ino=2112 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:nfs_t:s0 tclass=dir
Читается запись проще, чем кажется. avc denied это сам факт отказа, в фигурных скобках запрещённая операция read. comm="httpd" называет программу, pid=6591 номер её процесса. Пара scontext и tcontext, source context и target context, главные действующие лица: процесс домена httpd_t пытался прочитать каталог с типом nfs_t, tclass=dir сообщает класс объекта. В переводе на человеческий язык SELinux запретил веб-серверу читать каталог на сетевой файловой системе NFS. Рядом с read часто мелькают getattr, опрос атрибутов файла перед чтением, и write на запись.
Вместо самостоятельного перевода можно поставить пакет setroubleshoot-server: после установки journalctl -t setroubleshoot покажет расшифровку с готовыми командами, а sealert -l "*" выведет разбор недавних отказов с оценкой уверенности каждого варианта. Если служба auditd не запущена, выручает sudo dmesg | grep -i -e type=1300 -e type=1400, эти номера соответствуют событиям AVC и USER_AVC. Любопытно, что для случая с NFS в политике заранее существует переключатель httpd_use_nfs, и одна строка setsebool закрывает вопрос.
Тонкий момент, способный свести с ума: часть отказов политика прячет правилами dontaudit, чтобы журнал не тонул в повторяющемся шуме. Если проблема точно воспроизводится, а журнал пуст, временно отключают эти правила командой semodule -DB, воспроизводят сценарий, собирают записи и возвращают всё командой semodule -B.
restorecon и semanage fcontext возвращают метки на место
Пусть веб-сервер должен отдавать файлы из /srv/myweb, а не из стандартного /var/www/html. Каталог /srv в политике размечен типом var_t, созданные внутри файлы наследуют его, а новые каталоги верхнего уровня вроде /myserver получают default_t. Оба типа для домена httpd_t чужие, и на попытке чтения журнал получает новый отказ. Решение состоит из двух шагов, и их порядок принципиален. Сначала регистрируют правило разметки, оно переживёт и перезагрузку, и любую будущую переразметку:
# semanage fcontext -a -t httpd_sys_content_t "/srv/myweb(/.*)?"
Регулярное выражение /srv/myweb(/.*)? охватывает сам каталог и всё содержимое на любую глубину. Затем метки применяют к уже существующим файлам:
# restorecon -R -v /srv/myweb
restorecon сверяет фактические метки с правилами политики и меняет только расходящиеся. Флаг -R спускается в подкаталоги, -v печатает каждое изменение, -n включает сухой прогон без записи на диск, -F сбрасывает не только тип, но и пользователя, роль и уровень метки:
restorecon reset /srv/myweb/index.html context unconfined_u:object_r:var_t:s0->system_u:object_r:httpd_sys_content_t:s0
Разница между инструментами принципиальна. semanage fcontext записывает правило в базу, но ни одну метку не трогает. restorecon меняет метки здесь и сейчас, но без зарегистрированного правила результат проживёт до ближайшей переразметки. Утилита chcon -t httpd_sys_content_t file.html тоже меняет метку мгновенно, однако это ручное касание, которое слетит при любой переразметке, поэтому chcon оставляют для экспериментов, а для постоянных решений берут связку semanage fcontext плюс restorecon. Прочие операции с правилами: semanage fcontext -l перечисляет правила, ключ -C оставляет только локальные правки администратора, -m меняет правило, -d удаляет его.
Типовой алгоритм диагностики любого отказа SELinux укладывается в четыре шага:
- воспроизвести проблему в permissive режиме и убедиться, что без SELinux всё работает нормально;
- собрать записи об отказах через ausearch или sealert и найти пару scontext и tcontext;
- проверить метку цели командой matchpathcon -V и, если она расходится с ожидаемой, поправить контекст через semanage fcontext и restorecon;
- если метка верна, поискать подходящий переключатель setsebool, затем порт через semanage port и лишь в крайнем случае добавить собственное правило через audit2allow.
Булевы переключатели и нестандартные порты для служб
Далеко не всякая задача требует правки политики. Разработчики заранее заложили кнопки под типичные пожелания администраторов, это булевы переключатели, каждый включает или выключает целую ветку правил на лету, без перезагрузки и пересборки. Список получает команда getsebool -a, он длинный, поэтому обычно фильтруют: getsebool -a | grep ftp выдаст строки вроде ftpd_full_access --> off. Включение с сохранением после перезагрузки делает setsebool с флагом -P:
# setsebool -P httpd_can_network_connect_db on
Без -P значение протянет только до перезагрузки, и это частая причина повторяющихся чудес: настроили, проверили, перезагрузились, снова не работает. Для веб-сервера существуют десятки подобных переключателей, например httpd_enable_homedirs разрешает отдавать пользовательские каталоги, httpd_read_user_content даёт чтение файлов из домашних папок, а описание каждого покажет semanage boolean -l из пакета selinux-policy-devel.
Вторая типовая причина отказов, нестандартный порт. Попытка посадить Apache на порт 9876 оборачивается отказом старта службы, а в журнале появляется avc denied с операцией name_bind и классом tcp_socket. Портам тоже назначены типы, и домену httpd_t разрешено слушать только порты своего типа. Проверка списка выполняется от root:
# semanage port -l | grep http
http_cache_port_t tcp 3128, 8080, 8118
http_cache_port_t udp 3130
http_port_t tcp 80, 443, 488, 8008, 8009, 8443
pegasus_http_port_t tcp 5988
pegasus_https_port_t tcp 5989
Шесть TCP-портов для веб-сервера, 80, 443, 488, 8008, 8009 и 8443. Новый порт добавляют в тип одной командой, флаг -a добавляет запись, -t задаёт тип, -p определяет протокол:
# semanage port -a -t http_port_t -p tcp 9876
После этого systemctl start httpd.service запускается без сюрпризов. Ключ -m меняет существующую запись, -d удаляет её. Тот же механизм обслуживает Samba, PostgreSQL, Redis и любую службу со своим порт-типом в политике.
Свои правила через audit2allow и границы осторожности
Если метки верны, переключатели изучены, порты проверены, а отказ всё равно повторяется, значит в политике нет нужного правила. Так бывает с новыми версиями программ, нестандартными сборками и самописными службами. Для таких случаев существует генератор локальных модулей audit2allow:
# audit2allow -w -a
# ausearch -x /usr/bin/passwd --raw | audit2allow -D -M my-passwd
# semodule -X 300 -i my-passwd.pp
Флаг -x ограничивает выборку одним исполняемым файлом, -M компилирует модуль my-passwd.pp, ключ -X 300 кладёт локальные правила в отдельный слот политики. Список загруженных модулей покажет semodule -l, удалит модуль semodule -r my-passwd, а заглянуть в правила до установки позволяет короткий текстовый файл my-passwd.te.
Здесь нужна дисциплина. audit2allow превращает в разрешение всё, что попало в выборку журнала, включая странные запросы от взломанной программы. Опытные администраторы применяют его последним шагом и всегда читают сгенерированный файл .te перед установкой: там видно, какие allow-правила получит домен. Расплывчатое разрешение на всё для одного домена означает, что безопасность для этой службы просто выключили и назвали это настройкой.
SELinux не антивирус и не замена паролям и брандмауэру, он усиливает существующие слои защиты и держит взломанный сервис в узком коридоре. Пара привычек превращает его из раздражителя в рабочий инструмент: смотреть метки флагом -Z при переносе файлов и заглядывать в ausearch до того, как рука потянется отключить всё навсегда. Она не запрещает хаотично: в каждой строке журнала отчётливо названы домен процесса и тип объекта, а между этими двумя точками всегда лежит конкретная, проверяемая причина.