SSD Nodes Learn Hosting plans →
Посібники Matt ConnorВід Matt Connor · Оновлено 2026-09-04

SELinux: чому nginx повертає 403 і як це виправити

nginx повертає 403, хоча права правильні? Перевірте SELinux denial, виправте мітку через 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 отримує шлях, про який policy ще нічого не знає. Жодне правило для вебсервера не дозволяє читати файли такого типу. У журналі помилок відображається звичайна помилка 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. Політика містить список дозволених комбінацій домену, типу та дії. Усе, чого немає в цьому списку, заборонено. SELinux працює після класичної перевірки 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 залежить від їхніх гарантій сумісності й підтримки старіших CPU, а не від налаштувань безпеки за замовчуванням. 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 у журналі аудиту

Кожна відмова реєструється audit daemon як повідомлення 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 нічого не виводить, audit daemon може не працювати. Тоді заборони потрапляють до кільцевого буфера ядра:

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 читає ті самі записи та називає розпізнану причину: вимкнений boolean, мітка, що не відповідає політиці, або повна відсутність правила. sealert переглядає весь журнал і виводить рекомендовану команду для кожної заборони. Розглядайте цю рекомендацію лише як підказку. Формулювання відрізняються в різних випусках, а sealert іноді пропонує custom policy module, хоча правильним рішенням є виправлення мітки в одному рядку.

Потрібно знати ще одну особливість. Політика містить 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, але не завантажується з вашого каталогу, причина саме в цьому.

Виправлення класу поведінки за допомогою boolean

Не всі збої пов’язані з проблемою мітки. 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. Команда виводить поточне значення поруч зі збереженим.

Якщо існує boolean, надавайте перевагу йому, а не правилу, написаному вручну. Booleans постачаються разом із політикою дистрибутива, тому їх підтримують, документують, і наступній людині легко їх знайти. getsebool -a виводить усі booleans у системі.

Сервіс прослуховує нестандартний порт

Порти також мають мітки. Перемістіть 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, перш ніж перезапускати daemon і завершувати сесію. Саме цей крок часто пропускають, коли дотримуються загального посібника з захисту SSH на VPS на образі сімейства Red Hat. SELinux також не є firewall, тому порт усе одно має бути відкритий: тут — за допомогою sudo firewall-cmd --permanent --add-port=8081/tcp && sudo firewall-cmd --reload або ufw на образі Debian або Ubuntu. Прапорець --permanent має ту саму пастку з перезавантаженням, що й -P для boolean, а зони, які визначають, до яких інтерфейсів застосовується правило, варто один раз переглянути в матеріалі основи firewalld для VPS на Rocky або AlmaLinux.

Коли немає ні boolean, ні label, який можна змінити

На звичайному сервері таке трапляється рідко. Саме в таких випадках найчастіше завдають шкоди. audit2allow може створити policy module на основі заборон у журналі:

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 виконання триває, а журнал за один прохід збирає всі заборони. Після цього ви повертаєтеся до режиму enforcing і виправляєте їх разом.

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 також створює проблему, яку доведеться вирішувати пізніше. Поки політику не завантажено, нові файли створюються без міток, тому стан файлової системи розходиться з політикою. Після повторного ввімкнення SELinux потрібне повне перемаркування, інакше одразу може відмовити багато сервісів:

sudo fixfiles -F onboot
sudo reboot

Це записує /.autorelabel і перемарковує всі файлові системи під час наступного завантаження. На великому диску це займає багато часу, а консоль може виглядати так, ніби зависла, тому запускайте операцію тоді, коли можете зачекати. Оскільки машину все одно буде перезавантажено, спочатку варто перевірити, що ще заплановано для перезапуску. Саме це показує needs-restarting після того, як dnf update залишив у пам’яті старі ядра та бібліотеки. У 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 вказує runtime перемаркувати mount:

docker run -d -v /data/appdata:/var/lib/app:Z myimage

:Z маркує каталог лише для цього контейнера. :z маркує його для спільного використання між контейнерами. Якщо вказати :Z на каталог, який використовують інші сервіси, runtime рекурсивно перемаркує цей каталог, через що робота сервісів буде порушена. Тому надавайте контейнерам окремі шляхи. Якщо engine ще не встановлено на хості, зверніть увагу: команда docker у цих дистрибутивах часто фактично запускає podman, який відповідає на це ім’я. Це особливість, яку кроки встановлення Rocky та AlmaLinux допомагають врахувати ще до виникнення проблеми. Усе інше в налаштуванні таке саме, як для будь-якого іншого image; це описано в розділі запуск 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 mode записує кожну заборону, але не блокує жодної дії. У результаті ви отримуєте зайві повідомлення та втрачаєте захист. Якщо під час роботи потрібно тимчасово послабити політику для одного сервісу, виконайте sudo semanage permissive -a httpd_t, щоб решта системи залишалася в enforcing mode.

Як запустити сервіс на нестандартному порту, коли SELinux працює в enforcing mode?

Додайте порт до типу, якому цьому сервісу дозволено прив’язуватися. Для вебсервера на 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. Без цього кроку daemon завершує роботу під час запуску з bind() ... Permission denied, навіть якщо жоден інший процес не використовує цей порт.

Чи має Ubuntu SELinux?

Ні. Ubuntu і Debian постачаються з AppArmor, який застосовує профіль, прив’язаний до шляху виконуваного файлу, а не до міток файлів. Перевірте його за допомогою sudo aa-status і знайдіть рядки apparmor="DENIED" у sudo journalctl -k. Ubuntu обмежує вибраний набір пакетних сервісів, тому багато програм типово працюють без обмежень. У Rocky Linux і AlmaLinux SELinux зазвичай працює в enforcing mode одразу після встановлення, так само як у Fedora та RHEL.