Привет, это Вася из Fastfox. Если вы администрируете AlmaLinux или Rocky Linux, рано или поздно встретите ситуацию: владелец файла правильный, права 0644 или 0755 выставлены, Nginx или Apache перезапущен, PHP-FPM жив, а сайт всё равно отдаёт 403, 404 на существующий файл, 502 или загадочное Permission denied. На Debian-подобных системах мы часто сразу смотрим Unix-права и логи веб-сервера. В RHEL-совместимых дистрибутивах есть ещё один важный слой — SELinux.
Главная мысль статьи: SELinux не нужно отключать при первой ошибке. В режиме Enforcing он действительно может блокировать доступы, которые выглядят нормальными с точки зрения классических прав Linux, но почти всегда это диагностируется через audit.log и исправляется точечно: правильным контекстом, boolean-переключателем или, в редких случаях, небольшим локальным модулем политики.
Если после
setenforce 0сайт внезапно заработал, это не решение, а только подтверждение диагноза. ВозвращаемEnforcingи разбираем конкретный запрет.
Как SELinux смотрит на веб-стек
SELinux работает не вместо обычных прав доступа, а вместе с ними. Сначала должны быть корректны владелец, группа, режимы доступа, ACL и права на каждый каталог по пути. Затем вступает в игру SELinux: у процесса есть домен, у файла, сокета или порта — тип, а политика решает, разрешено ли домену работать с этим типом.
Для веб-сервера это особенно заметно. Процессы Nginx и Apache обычно работают в домене, связанном с HTTP-сервисами. Контент сайта должен иметь тип вроде httpd_sys_content_t, директории для записи — httpd_sys_rw_content_t, runtime-сокеты PHP-FPM — тип, который веб-серверу разрешено использовать. Если вы положили сайт в нестандартный путь, например /srv/www/example.com, SELinux не обязан автоматически считать его веб-контентом.
На AlmaLinux и Rocky Linux стандартный веб-контент в /var/www обычно размечается корректно пакетами и базовой политикой. Проблемы чаще появляются после миграции проекта из архива, ручного rsync, переноса на новый диск, bind mount, кастомного пути в /srv или /opt, отдельного пула PHP-FPM с собственным сокетом, нестандартного порта backend-сервиса. Если разворачиваете такой стек на VDS, SELinux стоит включать в план настройки сразу, а не после первого инцидента.
Быстрая проверка состояния SELinux и auditd
Начните с базовой диагностики. Нам нужно понять, включён ли SELinux, в каком он режиме и пишет ли система события аудита.
getenforce
sestatus
systemctl status auditd
Типичные режимы:
Enforcing— политика применяется, запрещённые действия блокируются;Permissive— запреты только логируются, но не блокируют работу;Disabled— SELinux отключён на уровне системы.
Для нормальной эксплуатации веб-сервера на AlmaLinux и Rocky Linux я рекомендую держать Enforcing. Временный переход в Permissive допустим на короткое время для подтверждения причины, но после этого лучше вернуть защиту обратно.
setenforce 0
getenforce
setenforce 1
getenforce
Если команд audit2why, audit2allow или semanage нет, установите нужные пакеты. На RHEL-совместимых системах они обычно находятся в policycoreutils-python-utils и связанных пакетах.
dnf install audit policycoreutils policycoreutils-python-utils setroubleshoot-server
systemctl enable --now auditd
Пакет setroubleshoot-server не обязателен, но часто помогает получить более человекочитаемые подсказки. На минимальных VDS его иногда не ставят, чтобы не тащить лишние зависимости; тогда достаточно auditd, ausearch, audit2why и audit2allow.

Где искать запреты: audit.log, ausearch и journalctl
Основной файл для расследования — /var/log/audit/audit.log. Не советую начинать с простого просмотра всего файла: он быстро разрастается, а строки AVC выглядят шумно. Удобнее фильтровать недавние события.
ausearch -m avc -ts recent
ausearch -m avc,user_avc,selinux_err -ts today
ausearch -m avc -ts today | audit2why
Если вы воспроизводите проблему прямо сейчас, полезный порядок такой: выполнить запрос к сайту, затем сразу посмотреть свежие AVC-события.
curl -I http://127.0.0.1/
ausearch -m avc -ts recent | audit2why
Иногда отказ виден и через journal, особенно если установлен setroubleshoot:
journalctl -t setroubleshoot --since today
journalctl -u nginx --since today
journalctl -u httpd --since today
journalctl -u php-fpm --since today
Важно сопоставлять время. Ошибка в error.log Nginx или Apache должна совпадать по времени с AVC-запретом. Если сайт отвечает 403, но в audit.log нет свежих событий, причина может быть не в SELinux: проверьте обычные права, root или alias в Nginx, Require all granted в Apache, правила .htaccess, владельца сокета PHP-FPM.
Как читать AVC-запрет без боли
В AVC-событии нас интересуют несколько полей: какой процесс пытался выполнить действие, над каким объектом, какой доступ был запрошен и какие SELinux-типы участвовали. В выводе audit2why часть этой логики уже разложена.
Пример команды для анализа свежих отказов:
ausearch -m avc -ts recent | audit2why
Если вывод говорит, что доступ был запрещён из-за неверного типа файла, не спешите генерировать модуль через audit2allow. В большинстве веб-кейсов правильнее назначить штатный тип через semanage fcontext и применить restorecon. Локальный модуль нужен тогда, когда у вас действительно нестандартное приложение и штатных типов или boolean-переключателей недостаточно.
Минимальный алгоритм чтения такой:
- Найдите процесс:
nginx,httpd,php-fpmили дочерний процесс приложения. - Посмотрите объект: файл, каталог, Unix-сокет, TCP-порт, сетевое соединение.
- Проверьте текущий контекст объекта через
ls -Z,ps -eZилиsemanage port -l. - Сравните с ожидаемым типом для веб-контента, writable-директории, сокета или порта.
- Исправьте причину штатным способом и снова воспроизведите запрос.
Сценарий 1: Nginx или Apache отдаёт 403 на файлы сайта
Классика: сайт лежит в /srv/www/example.com/public, права выглядят нормально, но веб-сервер пишет Permission denied. Сначала проверяем обычный доступ по цепочке каталогов:
namei -l /srv/www/example.com/public/index.php
ls -la /srv/www/example.com/public
Затем смотрим SELinux-контексты:
ls -Zd /srv /srv/www /srv/www/example.com /srv/www/example.com/public
ls -Z /srv/www/example.com/public
Если файлы имеют что-то вроде default_t, admin_home_t или другой неподходящий тип, веб-сервер не обязан их читать. Назначаем постоянное правило контекста и применяем его:
semanage fcontext -a -t httpd_sys_content_t '/srv/www/example.com/public(/.*)?'
restorecon -Rv /srv/www/example.com/public
ls -Z /srv/www/example.com/public
Почему именно semanage fcontext, а не chcon? chcon меняет метку прямо сейчас, но она может слететь при полном restorecon или переустановке контекстов. semanage fcontext добавляет локальное правило в политику, и после этого restorecon будет восстанавливать нужную метку.
Для нескольких сайтов лучше размечать общий шаблон:
semanage fcontext -a -t httpd_sys_content_t '/srv/www/[^/]+/public(/.*)?'
restorecon -Rv /srv/www
В этом примере регулярное выражение применяется к путям контекстов SELinux. Оно удобно, но не превращайте его в слишком широкое правило на весь /srv, если там лежат не только публичные файлы.
Сценарий 2: PHP-FPM не может писать в cache, sessions или uploads
Для чтения статических файлов достаточно httpd_sys_content_t. Но каталоги, куда приложение пишет во время работы, должны быть размечены иначе. Это касается var/cache, storage, runtime, uploads, временных каталогов фреймворков и директорий с пользовательскими файлами.
Например, для Laravel-подобной структуры:
semanage fcontext -a -t httpd_sys_rw_content_t '/srv/www/example.com/storage(/.*)?'
semanage fcontext -a -t httpd_sys_rw_content_t '/srv/www/example.com/bootstrap/cache(/.*)?'
restorecon -Rv /srv/www/example.com/storage
restorecon -Rv /srv/www/example.com/bootstrap/cache
Для WordPress обычно нужны права записи в wp-content/uploads, иногда в wp-content/cache, если используется плагин кеширования:
semanage fcontext -a -t httpd_sys_rw_content_t '/srv/www/example.com/public/wp-content/uploads(/.*)?'
semanage fcontext -a -t httpd_sys_rw_content_t '/srv/www/example.com/public/wp-content/cache(/.*)?'
restorecon -Rv /srv/www/example.com/public/wp-content
Не размечайте весь сайт как httpd_sys_rw_content_t ради удобства. Это расширяет последствия возможной уязвимости в приложении: процесс PHP получит право менять код, шаблоны и другие файлы, которые должны быть только для чтения. Безопаснее разделять read-only код и writable-данные.
Сценарий 3: 502 Bad Gateway из-за сокета PHP-FPM
При связке Nginx и PHP-FPM частая причина 502 — проблема с Unix-сокетом. Сначала исключаем обычные вещи: php-fpm запущен, путь к сокету совпадает в конфиге Nginx и пуле FPM, владелец и группа позволяют веб-серверу подключаться.
systemctl status php-fpm
systemctl status nginx
ss -xl | grep php-fpm
ls -lZ /run/php-fpm
Если сокет вынесен в нестандартный путь, например /srv/run/example.sock, SELinux может не разрешить Nginx подключение. Для runtime-сокетов веб-стека обычно используют тип httpd_var_run_t.
mkdir -p /srv/run
semanage fcontext -a -t httpd_var_run_t '/srv/run(/.*)?'
restorecon -Rv /srv/run
systemctl restart php-fpm
systemctl restart nginx
ls -lZ /srv/run
Если сокет создаётся заново при рестарте PHP-FPM, важно, чтобы каталог уже имел правильный контекст. Иначе файл сокета может появиться с неподходящей меткой, и проблема вернётся после перезагрузки.
Для Apache с mod_proxy_fcgi логика похожая: проверяем путь к сокету, контекст каталога, права на сам сокет и AVC-запреты в audit.log. Разница в конфигурации веб-сервера, а не в принципах SELinux. Если выбираете веб-сервер под проект, может пригодиться отдельный разбор Nginx и Apache в современных веб-стеках.
Сценарий 4: веб-приложение не может ходить в сеть
SELinux может запрещать HTTP-процессам исходящие сетевые соединения. Это всплывает, когда PHP-приложение обращается к внешнему API, локальному backend на TCP-порту, Redis, Memcached, SMTP или базе данных. В логах приложения вы увидите timeout, connection refused или permission denied, а в audit.log — AVC для сетевого доступа.
Проверьте boolean-переключатели:
getsebool -a | grep httpd_can_network
getsebool httpd_can_network_connect
getsebool httpd_can_network_connect_db
Для исходящих подключений к произвольным TCP-сервисам часто нужен httpd_can_network_connect:
setsebool -P httpd_can_network_connect on
Для подключения к базам данных может быть достаточно более узкого переключателя:
setsebool -P httpd_can_network_connect_db on
Здесь важно не включать всё подряд. Boolean — это штатный механизм политики, но он тоже расширяет возможности веб-процессов. Если приложению нужен только доступ к PostgreSQL или MySQL, проверьте, подходит ли специализированный boolean. Если нужен HTTP-запрос к API, тогда httpd_can_network_connect может быть оправдан.
Для отправки почты через локальный или внешний SMTP иногда требуется отдельный переключатель:
getsebool httpd_can_sendmail
setsebool -P httpd_can_sendmail on
Сценарий 5: нестандартные порты для Nginx, Apache или backend
SELinux контролирует не только файлы, но и порты. Если Apache или Nginx должен слушать нестандартный порт, одного параметра listen в конфиге недостаточно. Нужно, чтобы порт был помечен подходящим типом, например http_port_t.
semanage port -l | grep http_port_t
Если веб-сервер должен слушать TCP-порт 8080, а его нет в списке http_port_t, добавьте:
semanage port -a -t http_port_t -p tcp 8080
systemctl restart nginx
Если порт уже существует с другим типом, команда добавления завершится ошибкой. Тогда нужно не добавлять, а изменить тип:
semanage port -m -t http_port_t -p tcp 8080
Проверяйте это при переносе приложений с Debian/Ubuntu на AlmaLinux или Rocky Linux: сервис, который раньше спокойно слушал 8081 или 8443, на SELinux-системе может не стартовать именно из-за типа порта.

Когда использовать audit2allow, а когда не стоит
audit2allow умеет превращать AVC-запреты в локальный модуль политики. Это мощный инструмент, но опасный при механическом применении. Он не понимает вашу архитектуру и не отличает правильное нестандартное поведение от ошибки разметки файлов.
Плохой подход выглядит так: собрать весь audit.log за неделю, сгенерировать модуль и установить его. Так можно разрешить старые, случайные и уже неактуальные запреты.
Более аккуратный подход:
- Воспроизвести одну конкретную проблему.
- Собрать только свежие AVC-события.
- Сначала проверить контексты, boolean и порты.
- Если штатного решения нет, сгенерировать минимальный локальный модуль.
- Сохранить исходный
.teфайл в репозитории инфраструктуры или хотя бы в документации сервера.
ausearch -m avc -ts recent --raw | audit2allow -M webapp_local
semodule -i webapp_local.pp
semodule -l | grep webapp_local
Перед установкой обязательно прочитайте сгенерированный файл webapp_local.te. Если там разрешения на широкие типы, доступы к домашним каталогам, системным файлам или неожиданным процессам, остановитесь и вернитесь к диагностике. Очень часто после внимательного просмотра оказывается, что нужен не модуль, а один restorecon.
Удалить локальный модуль можно так:
semodule -r webapp_local
Полезные команды для ежедневной диагностики
Ниже — небольшой набор команд, который я обычно держу под рукой на веб-серверах с SELinux.
getenforce
sestatus
ps -eZ | grep -E 'nginx|httpd|php-fpm'
ls -Z /var/www
ausearch -m avc -ts recent
ausearch -m avc -ts recent | audit2why
semanage fcontext -l | grep httpd
semanage port -l | grep http_port_t
getsebool -a | grep httpd
Если нужно быстро проверить конкретный путь:
matchpathcon /srv/www/example.com/public/index.php
ls -Z /srv/www/example.com/public/index.php
matchpathcon показывает, какой контекст должен быть у пути согласно правилам. Если фактический ls -Z отличается, применяйте restorecon.
restorecon -Rv /srv/www/example.com
Практический runbook: 403, 502 или Permission denied
Когда инцидент уже случился, удобно идти по короткому плану, а не прыгать между конфигами.
Если Nginx или Apache отдаёт 403
- Проверьте путь к файлу и обычные права:
namei -l,ls -la. - Проверьте контексты:
ls -Zдля файла и всех важных каталогов. - Посмотрите свежий
audit.log:ausearch -m avc -ts recent | audit2why. - Для публичных файлов назначьте
httpd_sys_content_t. - Для каталогов записи используйте
httpd_sys_rw_content_t, но только там, где запись действительно нужна.
Если Nginx показывает 502 на PHP
- Проверьте статус
php-fpmи веб-сервера. - Сверьте путь к Unix-сокету или TCP-адресу.
- Проверьте владельца, группу и контекст сокета через
ls -lZ. - Для нестандартного каталога сокета задайте
httpd_var_run_t. - Если PHP подключается к сети, проверьте boolean-переключатели.
Если приложение не подключается к API или базе
- Проверьте обычную сетевую доступность: порт слушает, firewall не блокирует, DNS работает.
- Посмотрите AVC-события для момента ошибки.
- Проверьте
httpd_can_network_connectиhttpd_can_network_connect_db. - Включайте только тот boolean, который соответствует реальной задаче приложения.
Типовые ошибки при работе с SELinux
Первая ошибка — отключать SELinux навсегда. Это действительно быстро убирает симптом, но вместе с ним убирает важный слой защиты. На публичном веб-сервере, особенно с CMS и PHP-приложениями, этот слой часто ограничивает ущерб от уязвимости в коде или плагине.
Вторая ошибка — использовать chmod 777. Если запрет идёт от SELinux, расширение Unix-прав не поможет, зато ухудшит модель безопасности. Более того, после таких прав приложение может начать писать туда, куда не должно, и диагностика станет ещё сложнее.
Третья ошибка — применять chcon без постоянного правила. Для быстрого теста это допустимо, но в рабочей конфигурации лучше фиксировать правило через semanage fcontext. Тогда после восстановления контекстов сервер останется в ожидаемом состоянии.
Четвёртая ошибка — генерировать модуль audit2allow на весь накопленный audit.log. В журнале могут быть события от старых экспериментов, сканеров, неудачных деплоев и уже исправленных проблем. Локальная политика должна быть минимальной и понятной.
SELinux — только один слой защиты. После базовой настройки полезно отдельно проверить TLS, заголовки браузерной безопасности и поведение прокси. Для веб-сервера пригодится практическая памятка по HTTP security headers в Nginx и Apache.
Нюансы для AlmaLinux и Rocky Linux разных версий
AlmaLinux и Rocky Linux следуют RHEL-экосистеме, но версии политик, пакетов Nginx, Apache и PHP-FPM могут отличаться между 8-й, 9-й и будущими ветками. На практике это значит: не копируйте команды без проверки текущих типов и boolean. Сначала смотрите, что есть в системе.
cat /etc/os-release
rpm -q selinux-policy selinux-policy-targeted
rpm -q nginx httpd php-fpm
Если Nginx установлен из стороннего репозитория, а не из базовых репозиториев ОС, пути к логам, runtime-каталогам и unit-файлам могут отличаться. SELinux при этом продолжает оценивать реальные процессы, файлы и сокеты, поэтому диагностика через audit.log остаётся тем же источником правды.
С Apache обычно больше готовых правил из коробки, потому что исторически RHEL-экосистема тесно интегрирована с httpd. Но это не означает, что Nginx плохо работает с SELinux. Просто для нестандартных раскладок Nginx плюс PHP-FPM чаще приходится явно размечать пути и сокеты.
Как безопасно документировать изменения
SELinux-настройки легко забыть, особенно если сервер живёт годами. Хорошая практика — хранить рядом с проектом короткий файл с описанием локальных правил: какие пути размечены, какие boolean включены, какие порты добавлены, есть ли локальный модуль политики.
Снять текущую картину можно так:
semanage fcontext -l -C
semanage port -l -C
semanage boolean -l -C
semodule -l | grep local
Ключ -C показывает локальные изменения относительно базовой политики. Это удобно при миграции на новый VDS, восстановлении из бэкапа или расследовании, почему два почти одинаковых сервера ведут себя по-разному.
Для инфраструктуры, управляемой Ansible, Salt или другим CM-инструментом, лучше описывать SELinux-правила декларативно. Тогда новый сервер получает не только Nginx, Apache и PHP-FPM, но и корректную SELinux-разметку с первого деплоя.
Итоги
SELinux в AlmaLinux и Rocky Linux — не враг веб-администратора, а строгий контролёр границ. Он мешает только тогда, когда система не знает, что ваш нестандартный путь, сокет, порт или сетевое подключение являются нормальной частью веб-приложения. Вместо отключения защиты лучше читать audit.log, прогонять события через audit2why, проверять контексты и исправлять причину штатными инструментами.
Для Nginx, Apache и PHP-FPM чаще всего хватает четырёх приёмов: semanage fcontext плюс restorecon для файлов, httpd_sys_rw_content_t только для каталогов записи, httpd_var_run_t для нестандартных сокетов и аккуратного включения boolean-переключателей для сети. audit2allow оставляйте как последний инструмент, когда вы точно понимаете, какое поведение разрешаете.
Если вы привыкнете начинать расследование с ausearch -m avc -ts recent | audit2why, SELinux перестанет быть магией. Он станет ещё одним понятным слоем диагностики — таким же привычным, как error.log, systemctl status и проверка прав на файлы.


