SSD Nodes Learn 🎉 VPS от $5.50/мес
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-13

Почему nginx выдает 403 при корректных правах доступа

Если nginx возвращает 403, а права доступа верны, проверьте логи SELinux. Узнайте, как исправить метки файлов с помощью semanage и restorecon, не отключая режим enforcing.

Почему nginx возвращает 403 для файла с корректными правами доступа

Если nginx возвращает ошибку 403 для файла, у которого установлены верные биты прав доступа, почти всегда это означает, что SELinux (Security-Enhanced Linux) блокирует операцию чтения. SELinux проверяет дополнительный набор правил после прохождения стандартной проверки прав, и веб-серверу разрешено читать только те файлы, которые имеют метку веб-контента. Ваш файл имеет другую метку, поэтому операция открытия завершается неудачей, и nginx не может отправить содержимое.

Проверьте метку, а не только режим доступа:

ls -ldZ /data/www /data/www/index.html
drwxr-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. В Ubuntu и Debian вместо него используется AppArmor, который выполняет те же функции, но с помощью другого механизма (подробности в последнем разделе). Поэтому одно и то же приложение может корректно установиться на одном сервере и выдавать ошибку 403 на другом.

Устанавливайте инструменты до того, как они понадобятся

sudo dnf install -y policycoreutils-python-utils setroubleshoot-server

semanage: command not found в минимальном образе означает, что отсутствует policycoreutils-python-utils: этот пакет содержит semanage и audit2allow. setroubleshoot-server добавляет sealert и записывает краткое описание каждого отказа в доступе в журнал на понятном языке. Установите оба пакета на свежий сервер, так как они понадобятся именно в тот момент, когда что-то уже сломалось.

Как читать отказы SELinux в журнале аудита

Каждый отказ записывается демоном аудита в виде сообщения AVC (access vector cache):

sudo ausearch -m AVC,USER_AVC,SELINUX_ERR -ts recent
type=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 ничего не выводит, возможно, демон аудита не запущен. В этом случае отказы попадают в кольцевой буфер ядра:

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.log

audit2why считывает те же записи и называет распознанную причину: выключенный булев параметр, метка, не соответствующая политике, или отсутствие правила. 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, обновление пакета или полная перемаркировка сбросят её, так как политика по-прежнему предписывает для этого пути другое значение. 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.

Когда нет логического параметра или метки для изменения

Это редко встречается на обычном сервере, и именно здесь пользователи наносят ущерб. 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 и запускает перемаркировку всех файловых систем при следующей загрузке. На дисках большого объема это занимает много времени, и консоль может выглядеть зависшей, поэтому выполняйте эту операцию, когда у вас есть запас времени. В Rocky Linux и AlmaLinux 9 конфигурационный файл больше не отключает часть ядра самостоятельно, и документированный способ полного отключения 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 на 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 (режим permissive для одного профиля) и 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.