SELinux для сервера: чому nginx повертає 403
nginx повертає 403, хоча права правильні? Перевірте denial SELinux, виправте мітку через semanage і restorecon, не вимикаючи enforcing.
Чому 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 — це mandatory access control, зазвичай скорочено 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. Контекст вашої login shell — 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-serversemanage: 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 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 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.logaudit2why читає ті самі записи та називає розпізнану причину: вимкнений 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, оновлення пакета або повне перепризначення міток скине її, оскільки політика й надалі визначає для цього шляху інше значення. Стійким є варіант 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, використовуйте його замість правила, написаного вручну. Boolean входять до політики дистрибутива. Тому їх супроводжують, документують, і наступній людині легко їх знайти. getsebool -a виводить усі boolean у системі.
Дозвольте сервісу прослуховувати нестандартний порт
Порти також мають мітки. Перенаправте 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.
Коли немає ні boolean, ні мітки, яку можна змінити
На звичайному сервері таке трапляється рідко. Саме в цій ситуації найчастіше спричиняють пошкодження. 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 потрібне повне перемаркування, інакше одразу можуть перестати працювати багато сервісів:
sudo fixfiles -F onboot
sudo rebootЦе записує /.autorelabel і перемарковує всі файлові системи під час наступного завантаження. На великому диску це займає багато часу, а консоль може виглядати так, ніби зависла, тому запускайте операцію тоді, коли зможете зачекати. У 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 рекурсивно перемаркує цей каталог і порушить роботу цих сервісів, тому виділяйте для контейнерів окремі шляхи. В усіх інших аспектах налаштування відповідає будь-якому іншому образу, як описано в запуску 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 працює в enforcing-режимі?
Додайте порт до типу, якому дозволено виконувати прив’язування для цього сервісу. Для вебсервера на 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, навіть якщо інший процес не використовує цей порт.
Чи є SELinux в Ubuntu?
Ні. Ubuntu і Debian постачають AppArmor, який застосовує профіль, прив’язаний до шляху виконуваного файлу, а не до міток файлів. Перевірте його за допомогою sudo aa-status і шукайте рядки apparmor="DENIED" у sudo journalctl -k. Ubuntu обмежує вибраний набір пакетних сервісів, тому багато програм за замовчуванням працюють без обмежень. У Rocky Linux і AlmaLinux SELinux зазвичай працює в enforcing-режимі одразу після встановлення, як і у Fedora та RHEL.