Почему nginx выдает 403 при корректных правах доступа
Если права доступа верны, но nginx возвращает 403, причиной является SELinux. Узнайте, как прочитать логи отказа, исправить метки через semanage и restorecon без отключения защиты.
Почему nginx возвращает 403 для файла с корректными правами доступа
Если nginx возвращает ошибку 403 для файла, у которого установлены верные биты прав доступа, почти всегда причиной является SELinux (Security-Enhanced Linux), блокирующий чтение. SELinux проверяет второй набор правил после прохождения стандартной проверки прав, и веб-серверу разрешено читать только те файлы, которые имеют метку веб-контента. Ваш файл имеет другую метку, поэтому операция открытия завершается неудачей, и nginx не может отправить содержимое.
Проверьте метку, а не только режим доступа:
ls -ldZ /data/www /data/www/index.htmldrwxr-xr-x. root root unconfined_u:object_r:default_t:s0 /data/www
-rw-r--r--. root root unconfined_u:object_r:default_t:s0 /data/www/index.htmlТочка, отображаемая после drwxr-xr-x, означает, что файл имеет метку SELinux. default_t — это метка, которую получает путь, если политика безопасности не содержит информации о нем, и правила веб-сервера не разрешают чтение файлов такого типа. В журнале ошибок отображается обычная ошибка Unix, поэтому ситуация выглядит как проблема с правами доступа:
2026/08/11 09:14:02 [error] 1183#1183: *1 open() "/data/www/index.html" failed (13: Permission denied), client: 203.0.113.9, server: _, request: "GET / HTTP/1.1"Ядро возвращает 13: Permission denied для обоих типов отказа: как для обычного, так и для случая с SELinux. Поэтому ваша первая задача — выяснить, какой уровень системы отклонил запрос. Не начинайте с setenforce 0.
Часть модели, которая вам нужна
SELinux — это система принудительного контроля доступа, обычно обозначаемая как MAC. Каждый процесс выполняется в домене, например httpd_t для веб-сервера. Каждый файл и каждый сетевой порт имеют тип, например httpd_sys_content_t. Политика представляет собой список разрешенных комбинаций домена, типа и действия; всё, что не входит в этот список, запрещено. Она работает после классической проверки Unix, поэтому биты прав доступа в drwxr-xr-x сначала также должны разрешать доступ. Оба уровня должны дать положительный ответ.
Полный контекст состоит из четырех полей, разделенных двоеточиями, например system_u:system_r:httpd_t:s0: пользователь SELinux, роль, тип и уровень. На сервере вы будете проводить почти всё время, работая с третьим полем — типом. Две команды показывают текущие значения:
ps -eZ | grep nginx
id -ZРабочие процессы nginx показывают контекст, заканчивающийся на httpd_t. Ваша оболочка входа в систему показывает unconfined_u:unconfined_r:unconfined_t:s0, поскольку политика targeted по умолчанию ограничивает службы и не затрагивает интерактивных пользователей. Это важно знать, так как SELinux не заменяет запуск служб от имени пользователей с минимальными привилегиями. Он ограничивает то, к чему служба может получить доступ после того, как кто-то взломает её.
Три режима работы и наличие SELinux в образах
sestatus
getenforceРежим Enforcing блокирует действия и ведет логи. Режим Permissive разрешает все действия, но записывает в лог то, что было бы заблокировано. Режим Disabled полностью отключает загрузку политик. Команда getenforce выводит текущий режим. Команда sestatus также показывает режим, указанный в /etc/selinux/config — именно этот режим будет установлен после перезагрузки системы.
Дистрибутивы Rocky Linux, AlmaLinux, Fedora и RHEL поставляются с включенным SELinux в режиме Enforcing и политикой targeted. Этот общий стандарт не является случайностью, так как все четыре дистрибутива происходят из одной ветки Red Hat, к которой относился CentOS до появления Rocky Linux и AlmaLinux. Выбор между ними не влияет на содержание этой страницы, так как они используют одинаковые политики и инструменты, поэтому выбор между Rocky Linux и AlmaLinux зависит от обязательств по совместимости и поддержки старых процессоров, а не от настроек безопасности по умолчанию. В Ubuntu и Debian вместо этого используется AppArmor, который выполняет ту же задачу с помощью другого механизма (подробности в последнем разделе). В результате одно и то же приложение может корректно установиться на одном сервере и выдавать ошибку 403 на другом.
Устанавливайте инструменты до того, как они понадобятся
sudo dnf install -y policycoreutils-python-utils setroubleshoot-serversemanage: command not found в минимальном образе означает, что отсутствует policycoreutils-python-utils: этот пакет содержит semanage и audit2allow. setroubleshoot-server добавляет sealert и записывает краткое описание каждого отказа в доступе в журнал в понятном виде. Установите оба пакета на свежеразвернутый сервер, так как момент, когда они потребуются, совпадет с моментом возникновения неисправности.
Как читать сообщения об отказах SELinux в журнале audit
Каждый отказ записывается демоном audit в виде сообщения AVC (access vector cache):
sudo ausearch -m AVC,USER_AVC,SELINUX_ERR -ts recenttype=AVC msg=audit(1754896442.881:412): avc: denied { read } for pid=1183 comm="nginx" name="index.html" dev="vda1" ino=17203 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:default_t:s0 tclass=file permissive=0Четыре поля содержат всю необходимую информацию. comm — это программа, действие которой было заблокировано. scontext — исходный контекст, домен, в котором выполнялся процесс. tcontext — целевой контекст, метка объекта, к которому процесс пытался обратиться. tclass — тип объекта, в данном случае файл. Читаем вместе: процесс в httpd_t попытался прочитать файл с меткой default_t, а permissive=0 подтверждает, что запрос был действительно заблокирован, а не просто записан в журнал.
Если ausearch ничего не выводит, возможно, демон audit не запущен. В этом случае сообщения об отказах попадают в кольцевой буфер ядра:
sudo journalctl -k | grep -i avcТеперь преобразуем запись в понятное описание:
sudo ausearch -m AVC -ts recent | audit2why
sudo journalctl -t setroubleshoot --since today
sudo sealert -a /var/log/audit/audit.logaudit2why считывает те же записи и указывает распознанную причину: выключенный булев параметр, метка, не соответствующая политике, или полное отсутствие правила. sealert анализирует весь журнал и выводит предлагаемую команду для каждого отказа. Относитесь к этим предложениям как к подсказкам. Формулировки меняются от версии к версии, и sealert иногда предлагает создать пользовательский модуль политики, хотя правильным решением может быть простое исправление метки одной командой.
Еще один важный момент. Политика содержит правила dontaudit, которые скрывают отказы, считающиеся безвредными, поэтому программа может работать некорректно, а журнал останется пустым. Чтобы увидеть их, отключите фильтрацию на время проведения теста:
sudo semodule -DB
# reproduce the problem, then read the log again
sudo semodule -BИсправление неверного пути с помощью semanage fcontext и restorecon
Порядок выполнения этих двух команд имеет значение. semanage fcontext -a записывает, какой меткой должен обладать путь. restorecon применяет эту записанную метку по умолчанию к файлам на диске.
sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"
sudo restorecon -Rv /data/www
ls -ldZ /data/www/index.htmlПуть представляет собой регулярное выражение. (/.*)? охватывает сам каталог и всё его содержимое, что необходимо для корневого каталога веб-сервера. Перед внесением изменений проверьте, что именно будет изменено: sudo restorecon -Rvn /data/www выводит планируемые изменения меток, так как флаг -n отменяет выполнение действий. После реального запуска restorecon метка принимает значение httpd_sys_content_t, и ошибка 403 исчезает без перезапуска службы.
Используйте chcon только для тестирования. chcon -t httpd_sys_content_t index.html устанавливает метку напрямую, но следующее выполнение restorecon, обновление пакета или полная перемаркировка сбросят её, так как политика по-прежнему предписывает для этого пути другое значение. На сервере, где dnf-automatic применяет обновления безопасности по расписанию, этот сброс произойдёт автоматически в фоновом режиме, поэтому сайт перестанет работать спустя время после ваших действий. semanage fcontext — это вариант, который сохраняется после перезагрузки. Список записанных правил можно просмотреть с помощью sudo semanage fcontext -l | grep '^/data'.
Контент, в который служба должна записывать данные, требует другого типа метки. Используйте httpd_sys_rw_content_t для каталогов загрузки или кэша и ограничивайте применение только этими путями: сайт, доступный только для чтения, но имеющий метку для записи, предоставляет приложению избыточные права доступа.
Почему метка оказалась неверной? Почти всегда это связано со способом перемещения файлов. mv сохраняет существующую метку файла, поэтому сайт, перемещённый из /root, сохраняет метку admin_home_t. Обычная команда cp присваивает новому файлу метку по умолчанию, соответствующую целевому каталогу, что обычно и требуется, в то время как cp -a и rsync -X копируют метки источника вместе с файлом. Команда git clone в новый каталог верхнего уровня создаёт default_t. Если страница корректно загружается из /usr/share/nginx/html, но выдаёт ошибку из вашего собственного каталога, причина заключается именно в этом.
Исправление поведения с помощью логического переключателя
Некоторые сбои не связаны с метками безопасности. Reverse proxy на свежеустановленной системе Rocky или AlmaLinux возвращает ошибку 502, а в логе ошибок указано:
2026/08/11 10:02:55 [crit] 1183#1183: *3 connect() to 127.0.0.1:3000 failed (13: Permission denied) while connecting to upstreamВаш upstream исправен. В httpd_t по умолчанию запрещено открывать исходящие сетевые соединения, поэтому вызов connect() отклоняется до того, как он достигнет loopback-интерфейса. Один переключатель управляет всем этим поведением:
getsebool -a | grep httpd_can_network
sudo setsebool -P httpd_can_network_connect on-P — это важный флаг: он записывает значение на диск. Без -P изменение будет потеряно при следующей перезагрузке, из-за чего сервис будет работать только до перезапуска машины. Проверьте результат с помощью semanage boolean -l | grep httpd_can_network_connect, которая выводит текущее значение рядом с сохраненным.
Всегда отдавайте предпочтение логическим переключателям, если они существуют, вместо написания правил вручную. Эти переключатели поставляются вместе с политикой дистрибутива, поэтому они поддерживаются, задокументированы и их легко найти другому администратору. getsebool -a выводит список всех доступных переключателей в системе.
Настройка прослушивания сервисом нестандартного порта
Порты также имеют метки. Перенесите nginx на порт 8081, и он откажется запускаться:
nginx: [emerg] bind() to 0.0.0.0:8081 failed (13: Permission denied)httpd_t может привязываться только к портам с меткой http_port_t, а 8081 в их число не входит. Добавьте его:
sudo semanage port -l | grep -w http_port_t
sudo semanage port -a -t http_port_t -p tcp 8081Сначала проверьте список. Некоторые высокие порты уже разрешены, включая 8008 и 8443, а повторное добавление одного и того же порта приведет к ошибке ValueError: Port tcp/8081 already defined. Если порт уже относится к другому типу, используйте semanage port -m -t http_port_t -p tcp 8081 вместо добавления.
Эта же команда обеспечивает работу SSH на нестандартном порту. Bind to port 2222 on 0.0.0.0 failed: Permission denied в journalctl -u sshd означает, что 2222 отсутствует в ssh_port_t, поэтому выполните sudo semanage port -a -t ssh_port_t -p tcp 2222 перед перезапуском демона и закрытием сессии. Это шаг, который часто пропускают при выполнении общих руководств по укреплению безопасности SSH на VPS в дистрибутивах семейства Red Hat. SELinux — это не межсетевой экран, поэтому порт все равно нужно открыть: sudo firewall-cmd --permanent --add-port=8081/tcp && sudo firewall-cmd --reload здесь или ufw в дистрибутивах Debian или Ubuntu. Флаг --permanent содержит ту же ловушку при перезагрузке, что и -P для логических значений, а зоны, определяющие, к каким интерфейсам применяется правило, стоит изучить один раз в основах firewalld для VPS на базе Rocky или AlmaLinux.
Когда нет подходящего логического значения или метки для изменения
Это редкая ситуация на обычном сервере, и именно здесь пользователи часто совершают ошибки. audit2allow позволяет создать модуль политики на основе записей об отказах в логах:
sudo ausearch -m AVC -ts recent -c nginx | audit2allow -M nginx_local
cat nginx_local.te
sudo semodule -i nginx_local.ppПрочитайте nginx_local.te перед установкой модуля. Две привычки помогут сохранить безопасность. Фильтруйте входные данные для конкретной программы, которую вы настраиваете, с помощью -c, так как передача через конвейер всех отказов за неделю в audit2allow разрешит их все одновременно. Никогда не устанавливайте модуль, созданный на основе отказа, который вы не можете объяснить: правило, позволяющее httpd_t читать любой файл в системе, легко создать, но трудно обнаружить спустя месяцы. Удаляйте модуль с помощью sudo semodule -r nginx_local.
Permissive — это диагностический режим, а не решение проблемы
sudo setenforce 0
# reproduce the problem once, all the way through
sudo ausearch -m AVC -ts recent
sudo setenforce 1Режим permissive разрешает доступ и записывает его в журнал. Его главная ценность — полнота данных. В режиме enforcing сервис останавливается при первом же отказе в доступе: вы исправляете одну ошибку, перезапускаете сервис и сталкиваетесь со следующей. В режиме permissive выполнение продолжается, и журнал фиксирует все отказы за один проход, что позволяет проанализировать и устранить их все сразу перед возвратом в рабочий режим.
setenforce не затрагивает /etc/selinux/config, поэтому после перезагрузки система возвращается в режим enforcing. Это механизм безопасности, и именно поэтому «исправление», выполненное с помощью setenforce 0, может привести к сбою в самый неподходящий момент. Если сервису требуется больше прав на время отладки, измените настройки только для этого домена, а не для всей системы: sudo semanage permissive -a httpd_t сохраняет режим enforcing для остальных компонентов, а sudo semanage permissive -d httpd_t отменяет это действие.
Почему отключение SELinux обходится дороже, чем исправление меток
Установка SELINUX=disabled в /etc/selinux/config меняет разовое исправление метки на постоянное снижение безопасности сервера. Разница становится очевидной в день, когда веб-приложение оказывается скомпрометировано. В режиме enforcing код злоумышленника выполняется в контексте httpd_t, поэтому он может читать веб-контент, но попытки прочитать /etc/shadow или записать unit-файл systemd будут заблокированы политикой, независимо от прав, предоставленных пользователю Unix. Без загруженной политики тот же код получает все права, которыми обладает учетная запись сервиса.
Отключение также влечет за собой скрытые расходы. Пока политика не загружена, новые файлы создаются без меток, из-за чего файловая система перестает соответствовать требованиям безопасности. Последующее включение SELinux потребует полной перемаркировки, иначе множество сервисов одновременно перестанут работать:
sudo fixfiles -F onboot
sudo rebootЭта команда записывает /.autorelabel и запускает перемаркировку всех файловых систем при следующей загрузке. На дисках большого объема это занимает много времени, и консоль может выглядеть зависшей, поэтому выполняйте операцию, когда у вас есть запас времени. Поскольку сервер в любом случае уйдет на перезагрузку, стоит заранее проверить, какие еще процессы требуют перезапуска — именно это показывает утилита needs-restarting после того, как обновление dnf оставило в памяти старые версии ядер и библиотек. В Rocky Linux и AlmaLinux 9 конфигурационный файл больше не отключает работу SELinux на уровне ядра самостоятельно, и документированный способ полного отключения SELinux требует передачи аргумента ядру (sudo grubby --update-kernel ALL --args selinux=0). Знание этой команды полезно при получении доступа к чужому серверу. Это не является решением ошибки 403.
Добавление метки для контейнеров
На хостах семейства Red Hat процессы контейнеров выполняются в container_t и могут читать только файлы с меткой container_file_t. Привязка каталога (bind mount) с хоста завершается ошибкой Permission denied внутри контейнера, хотя на хосте ls -l выглядит корректно. Суффикс :Z указывает среде выполнения на необходимость перемаркировки точки монтирования:
docker run -d -v /data/appdata:/var/lib/app:Z myimage:Z помечает каталог только для этого контейнера. :z помечает его для совместного использования между контейнерами. Если указать :Z для каталога, который используют другие службы, произойдет рекурсивная перемаркировка, что приведет к сбою этих служб, поэтому выделяйте контейнерам отдельные пути. Если движок еще не установлен, учтите, что команда docker в этих дистрибутивах часто является псевдонимом podman — это нюанс, который инструкции по установке для Rocky и AlmaLinux проясняют до того, как вы с этим столкнетесь. Все остальные аспекты настройки соответствуют любому другому образу, что описано в разделе запуск Docker на VPS.
В Ubuntu и Debian используется AppArmor
Та же задача, другой подход. AppArmor ограничивает программу по пути к исполняемому файлу, используя профиль в /etc/apparmor.d/, вместо маркировки файлов на диске. Здесь нечего перемаркировывать и нет restorecon. Начните отсюда:
sudo aa-status
sudo journalctl -k | grep -i apparmorОтказ отображается как apparmor="DENIED" operation="open" profile="/usr/sbin/nginx" name="/data/www/index.html" requested_mask="r". Рабочий процесс выглядит так же: прочитать отказ, найти профиль, изменить правило. sudo apt install apparmor-utils предоставляет aa-complain (разрешительный режим для одного профиля) и aa-enforce для возврата в исходное состояние. Ubuntu ограничивает выбранный набор пакетных сервисов, оставляя остальные без ограничений, поэтому используйте aa-status, чтобы увидеть, что действительно активно, вместо того чтобы делать предположения.
Одна привычка полезна для обеих систем. Когда сервис сообщает Permission denied при обращении к чему-то, что выглядит корректно, прочитайте журнал безопасности, прежде чем менять права доступа. Проблема редко заключается в битах прав.
FAQ
Почему nginx возвращает 403, если права доступа к файлам установлены верно?
Потому что SELinux запретил чтение, а не права доступа в файловой системе. Веб-сервер работает в домене httpd_t и может читать только файлы, помеченные как веб-контент, поэтому в доступе к файлу с меткой default_t или admin_home_t будет отказано, и nginx не сможет отдать содержимое. Проверьте это с помощью sudo ausearch -m AVC -ts recent: команда покажет scontext, заканчивающийся на httpd_t, и tcontext с неверным типом. Затем определите правильную метку и примените её: sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?", а затем sudo restorecon -Rv /data/www.
Безопасно ли использовать setenforce 0 для запуска сервиса?
setenforce 0 — это диагностический шаг, а не исправление. Используйте его, чтобы воспроизвести проблему один раз и собрать все отказы в лог, изучите их с помощью sudo ausearch -m AVC -ts recent, затем выполните sudo setenforce 1 и устраните причины. Сервер в режиме permissive записывает все отказы, но не блокирует их, поэтому вы получаете лишний шум в логах и теряете защиту. Если одному сервису нужно больше прав на время работы, используйте sudo semanage permissive -a httpd_t, чтобы остальная часть системы оставалась в режиме enforcing.
Как запустить сервис на нестандартном порту при включенном SELinux?
Добавьте порт к типу, которому разрешено связываться с этим сервисом. Для веб-сервера на порту 8081: sudo semanage port -a -t http_port_t -p tcp 8081. Для SSH на порту 2222: sudo semanage port -a -t ssh_port_t -p tcp 2222. Сначала проверьте текущий список с помощью sudo semanage port -l | grep -w http_port_t, так как попытка добавить уже существующий порт приведет к ошибке ValueError: Port tcp/8081 already defined. Без этого шага демон завершит работу при запуске с ошибкой bind() ... Permission denied, даже если порт не занят другим процессом.
Есть ли в Ubuntu SELinux?
Нет. В Ubuntu и Debian используется AppArmor, который применяет профили, привязанные к пути к исполняемому файлу, а не к меткам на файлах. Проверьте его состояние с помощью sudo aa-status и поищите строки apparmor="DENIED" в sudo journalctl -k. Ubuntu ограничивает только определенный набор пакетных сервисов, поэтому многие программы по умолчанию работают без ограничений. SELinux в режиме enforcing по умолчанию используется в Rocky Linux, AlmaLinux, а также в Fedora и RHEL.