Антипаттерн: отключить SELinux и забыть
Nginx не может достучаться до приложения на соседнем порту, в логах — глухое Permission denied, файрвол открыт, права на файлы правильные, а соединение всё равно падает. Через двадцать минут гугления кто-то в панике набирает setenforce 0, всё оживает — и на этом расследование заканчивается навсегда. Спойлер: проблема не решена, она просто перестала быть видна, а вместе с ней исчез и целый слой защиты сервера.
Содержание
Как это выглядит на практике
Сценарий типовой для CentOS, RHEL и AlmaLinux: разворачиваете приложение на нестандартном порту (скажем, Node.js или Gunicorn на 8080), настраиваете nginx как reverse proxy — и получаете 502 Bad Gateway, хотя curl 127.0.0.1:8080 с самого сервера работает без проблем. Файрвол ни при чём — порт локальный, наружу не торчит. Конфиг nginx синтаксически верный. Приложение живо и слушает порт. Но именно nginx достучаться до него не может.
Дальше события развиваются по одному из двух сценариев.
Сценарий А (правильный). Администратор смотрит getenforce, видит Enforcing, вспоминает про SELinux, идёт смотреть, какая политика блокирует соединение, и чинит именно её.
Сценарий Б (антипаттерн). Администратор либо не знает про SELinux вообще, либо знает, но воспринимает его как "эту штуку, которая вечно мешает". Гуглит "nginx 502 selinux", находит на первом же форуме совет:
setenforce 0
Nginx тут же начинает проксировать, проблема "решена" за пять секунд. Довольный собой администратор либо оставляет всё как есть до следующей перезагрузки (после которой SELinux снова включится в Enforcing — и все обрадуются возврату старой ошибки), либо, чтобы "больше не мучиться", делает это постоянным:
vi /etc/selinux/config
# было: SELINUX=enforcing
# стало: SELINUX=disabled
Перезагрузка — и SELinux выключен навсегда, до переустановки системы. В коммите или тикете при этом обычно пишут что-то вроде "исправлена ошибка проксирования nginx" — ни слова о том, что реально была отключена вся система мандатного контроля доступа.
Что на самом деле выключает эта команда
Важно понимать разницу между тем, что кажется, будто вы отключаете, и тем, что вы отключаете на самом деле.
Кажется: "я отключил одно назойливое правило про порты".
Происходит на самом деле: вы отключаете Mandatory Access Control (MAC) — второй, независимый от прав доступа Unix (rwx, owner/group) слой контроля, который действует для *всех* процессов и *всех* файлов в системе, а не только для nginx и порта 8080.
Обычные права доступа в Linux — это Discretionary Access Control (DAC): владелец файла или root решает, кто может читать, писать и исполнять. Если процесс работает от имени root или получил права через уязвимость, DAC ему уже не помеха — он и так может почти всё. SELinux добавляет второй, независимый уровень проверки: у каждого процесса есть свой контекст (домен), у каждого файла, порта и сокета — свой тип, и политика описывает, какому домену какие типы разрешено использовать и как. Даже процесс, запущенный от root, но живущий в домене httpd_t, не может просто взять и открыть файл с типом, скажем, postgresql_var_lib_t, если это явно не разрешено, — SELinux вмешается раньше, чем вопрос вообще дойдёт до DAC.
Именно поэтому в контексте вашей задачи блокировка выглядит так странно: файловые права у файла нормальные, а доступа всё равно нет — потому что упёрлись не в rwx, а в контекст SELinux.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать VPSПочему полное отключение — это сдача обороны, а не устранение помехи
Смысл SELinux не в том, чтобы не пускать вас в систему, которую вы и так администрируете. Смысл — в том, что происходит после того, как систему уже взломали через вашу же собственную уязвимость: через дырявую версию библиотеки в приложении, через RCE в CMS, через небезопасную десериализацию в API. В момент эксплуатации атакующий получает выполнение кода в контексте вашего процесса — то есть ровно в том домене, в котором работает скомпрометированное приложение (например, httpd_t для веб-сервера).
Без SELinux (или с отключённым SELinux) единственная граница, в которую он тут же упрётся, — это DAC: права пользователя, от которого запущен процесс. Если процесс запущен не от root (что правильно) — атакующий ограничен правами этого пользователя, но это довольно широкая свобода: он может читать любые файлы, доступные на чтение этому пользователю или "всем" (o+r), пробовать открывать произвольные исходящие соединения, писать во всё, что доступно на запись.
С включённым и корректно настроенным SELinux тот же скомпрометированный процесс упирается в политику до DAC: домен httpd_t может писать только в типы файлов, явно размеченные под веб-сервер (httpd_sys_rw_content_t и подобные), может открывать исходящие соединения только если явно разрешено булевым флагом (httpd_can_network_connect), не может трогать файлы других сервисов даже при технически совпадающих Unix-правах, не может слушать порты, для которых не назначен подходящий тип. Это и называется defense in depth — второй независимый рубеж, который не отменяет первый, а стоит за ним на случай, если первый пробьют.
Отключая SELinux целиком ради одной блокировки по порту, вы не убираете "мешающее правило" — вы убираете весь этот рубеж сразу для всех сервисов на сервере: почты, базы данных, панели управления, если она там есть, любого будущего приложения, которое вы поставите через полгода и забудете, что SELinux вообще существовал. Это тот же класс ошибки, что и держать всё под root вместо непривилегированных пользователей: вместо того чтобы точечно ограничить конкретный процесс, убирают ограничение целиком — и получают систему, где скомпрометировать один сервис означает скомпрометировать все остальные разом. Конкретная проблема была ровно одна строчка политики про один порт. Решение "выключить всё" закрывает эту одну строчку ценой сотен других, о которых вы даже не думали.
Как разобраться, что именно заблокировано
Прежде чем что-либо отключать, стоит потратить три минуты на то, чтобы увидеть *конкретный* отказ. SELinux логирует каждое блокированное действие в audit-лог, и это самое надёжное место для диагностики:
grep denied /var/log/audit/audit.log | tail -20
Для нашего сценария (nginx не может подключиться к бэкенду на нестандартном порту) типичная запись выглядит примерно так:
type=AVC msg=audit(1735900000.123:456): avc: denied { name_connect } for
pid=1842 comm="nginx" dest=8080 scontext=system_u:system_r:httpd_t:s0
tcontext=system_u:object_r:unreserved_port_t:s0 tclass=tcp_socket
Ключевые поля здесь: denied { name_connect } — то самое действие, которое запретили; comm="nginx" — кто пытался; dest=8080 — какой порт; scontext=...httpd_t — домен-источник; tcontext=...unreserved_port_t — тип цели, то есть порт 8080 размечен как обычный "непривилегированный" порт, а не как порт, к которому явно разрешено обращаться веб-серверу.
Если логов пока пусто, а SELinux уже переключён в Permissive (режим "логировать, но не блокировать") — это тоже нормальная диагностическая техника, только временная: setenforce 0 из командной строки как раз и переводит систему в Permissive на текущую сессию до перезагрузки (это не то же самое, что SELINUX=disabled в конфиге — важное различие, о котором ниже). В Permissive вы увидите в логе все отказы, которые *были бы* заблокированы, но система при этом продолжает нормально работать, и вы спокойно собираете полную картину, прежде чем писать правило.
Для перевода AVC-записей в готовое правило есть утилита audit2allow (пакет policycoreutils-python-utils):
grep nginx /var/log/audit/audit.log | audit2allow -M nginx_local
Она сгенерирует модуль политики и подскажет команду для его загрузки. Но для типовых, часто встречающихся ситуаций — вроде подключения по нестандартному порту — обычно даже не нужен audit2allow: готовое решение через semanage/setsebool короче и понятнее, чем сгенерированный модуль.
Точечная настройка вместо отключения
Для конкретного случая "nginx не может проксировать на порт приложения" есть ровно два инструмента, и выбор между ними зависит от того, что означает блокировка.
Если дело в самом порту (SELinux не знает, что на 8080 разрешено ходить веб-серверу) — добавьте порт в список разрешённых для нужного типа:
# посмотреть, какие порты уже разрешены для httpd
semanage port -l | grep http_port_t
# добавить порт 8080 в список разрешённых для http-соединений
semanage port -a -t http_port_t -p tcp 8080
Если порт уже числится за каким-то другим типом (бывает с популярными портами вроде 8080, который часто занят под Tomcat как http_cache_port_t), semanage -a откажет с ошибкой — тогда используется -m (modify) вместо -a:
semanage port -m -t http_port_t -p tcp 8080
Если дело в самом факте исходящего сетевого соединения (nginx как прокси в принципе не имеет права инициировать TCP-соединения к бэкендам — а это отдельное от портов ограничение) — включите нужный булев флаг:
setsebool -P httpd_can_network_connect on
Флаг -P здесь обязателен — без него изменение живёт только до перезагрузки, ровно как временный setenforce 0. С -P изменение попадает в постоянную политику и переживает reboot.
Полезные булевы флаги для похожих ситуаций с nginx/Apache:
| Флаг | Что разрешает |
|---|---|
httpd_can_network_connect | Исходящие соединения веб-сервера к произвольным адресам (нужно для reverse proxy) |
httpd_can_network_connect_db | Соединения веб-сервера к базам данных по сети |
httpd_can_network_relay | Работа веб-сервера как прокси/релея |
httpd_enable_homedirs | Раздача контента из домашних директорий пользователей |
httpd_unified | Ослабление различий между типами контента (обычно лучше не включать без нужды) |
После правки стоит убедиться, что правило действительно применилось и новых отказов в логе больше не появляется:
getsebool httpd_can_network_connect
semanage port -l | grep 8080
tail -f /var/log/audit/audit.log | grep denied
Если после этого прокси заработал и в логе тишина — значит, вы закрыли ровно ту дыру, которая была нужна, не открыв заодно все остальные.
Отдельно стоит проверять контексты файлов, если проблема не в сети, а в доступе к файлам приложения (частый случай при переносе файлов приложения через scp/rsync в нестандартную директорию):
ls -Z /var/www/app/
restorecon -Rv /var/www/app/
restorecon возвращает файлам контекст, ожидаемый политикой для их расположения — часто этого одного достаточно, если файлы просто "потеряли" правильную разметку при копировании.
Почему "временно для отладки" остаётся навсегда
У этого антипаттерна есть предсказуемый жизненный цикл, который стоит проговорить отдельно, потому что именно он превращает разовую заглушку в постоянную дыру.
Шаг первый: возникает срочная проблема (прод лежит, дедлайн, кто-то ждёт). Шаг второй: setenforce 0 мгновенно снимает симптом. Шаг третий: задача помечена как решённая, все расходятся — потому что с точки зрения бизнеса *ничего не сломано*, сайт работает. Шаг четвёртый: настоящая причина (какой конкретно тип порта или булев флаг требовался) никогда не была установлена и уж тем более не задокументирована — она попросту не всплывала, потому что симптом исчез раньше, чем кто-то успел его расследовать. Шаг пятый: через полгода новый администратор (или тот же самый, но забывший) видит Disabled в getenforce, не понимает, зачем это было сделано, боится включать обратно "а вдруг опять что-то сломается" — и так это состояние консервируется на годы.
Отдельная ловушка — путаница между setenforce 0 и SELINUX=disabled в /etc/selinux/config. Первое — это переключение в Permissive на лету, временное до перезагрузки, и оно оставляет SELinux формально активным: политика продолжает загружаться, метки на файлах продолжают поддерживаться, ядро продолжает логировать (но не блокировать) отказы. Второе — это полное выключение подсистемы при следующей загрузке: метки на файлах постепенно "протухают" (новые файлы создаются вообще без контекста), и обратное включение (enforcing в конфиге + reboot) после долгого простоя в disabled почти гарантированно ломает всё разом, потому что политике приходится применяться к файловой системе, которая годами жила без разметки. Тогда restorecon -R / на всю систему после большого простоя в disabled — это отдельная, часто болезненная операция, которую тоже нередко пропускают, из-за чего первая же попытка вернуть enforcing превращается в цепочку новых отказов и желание снова всё выключить.
Правильная гигиена здесь простая: если нужно временно снять подозрение на SELinux при отладке — setenforce 0, воспроизводим проблему, смотрим audit.log через ausearch -m avc -ts recent, находим конкретный отказ, сразу же возвращаем setenforce 1, пишем точечное правило, проверяем, что оно решает проблему уже в Enforcing. Между "временно выключил" и "написал правило" не должно проходить больше нескольких минут — иначе это тот самый сценарий, который заканчивается навсегда выключенной защитой.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать VPSНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Как быстро проверить, действительно ли причина именно в SELinux, а не в чём-то ещё?
Временно переключите в setenforce 0 и повторите операцию. Если заработало — причина в SELinux, возвращайте setenforce 1 и ищите конкретное правило через audit.log. Если не заработало — дело в чём-то другом (файрвол, права DAC, конфигурация приложения), и выключать SELinux вообще не было смысла.
SELinux правда сильно нагружает систему по сравнению с выключенным?
На современном железе накладные расходы на проверку политики пренебрежимо малы для подавляющего большинства нагрузок — это не тот случай, когда стоит жертвовать безопасностью ради производительности. Если сомневаетесь для вашей конкретной нагрузки — измерьте на своих данных, а не полагайтесь на чужие цифры из интернета.
Что делать, если после долгого простоя в disabled нужно вернуть enforcing?
Поставьте SELINUX=enforcing в конфиге, но перед перезагрузкой создайте файл /.autorelabel (touch /.autorelabel) — при следующей загрузке система переразметит все файлы контекстами заново. Это может занять заметное время на больших дисках и стоит планировать в окно обслуживания, а не на живом проде.
Обязательно ли разбираться с audit2allow вручную, или можно просто копировать команды из интернета?
Команду semanage/setsebool для типовой ситуации скопировать можно, но перед этим стоит хотя бы посмотреть в audit.log, что именно там заблокировано — иначе рискуете скопировать правило, разрешающее не то, что нужно, и либо не решить проблему, либо разрешить лишнее.
А если приложение вообще не поддерживает работу с SELinux и постоянно генерирует отказы, которые не получается закрыть точечно?
Такое бывает с плохо написанным ПО, которое лезет куда попало. В крайнем случае можно написать под конкретное приложение собственный локальный модуль политики (audit2allow -M) вместо отключения SELinux целиком — это всё ещё точечное решение, просто более объёмное, и оно не открывает доступ остальным сервисам на сервере.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →