SELinux blokuje nginx 403: jak naprawić uprawnienia
Nginx zwraca błąd 403 mimo poprawnych uprawnień systemu plików. Dowiedz się, jak odczytać logi SELinux, użyć semanage oraz restorecon i poprawnie skonfigurować etykiety plików.
Dlaczego nginx zwraca błąd 403 dla pliku z poprawnymi uprawnieniami
Jeśli nginx zwraca błąd 403 dla pliku, którego bity uprawnień są poprawne, niemal zawsze przyczyną jest SELinux (Security-Enhanced Linux), który blokuje operację odczytu. SELinux sprawdza dodatkowy zestaw reguł po przejściu standardowej weryfikacji uprawnień. Serwer WWW ma uprawnienia do odczytu tylko tych plików, które posiadają etykietę zawartości sieciowej. Plik posiada inną etykietę, więc operacja otwarcia kończy się niepowodzeniem, a nginx nie ma danych do wysłania.
Należy sprawdzić etykietę, a nie tylko tryb dostępu:
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.htmlKropka wyświetlona po drwxr-xr-x oznacza, że plik posiada etykietę SELinux. default_t to etykieta przypisywana ścieżce, która nie jest zdefiniowana w polityce, a reguły serwera WWW nie zezwalają na odczyt tego typu. Dziennik błędów wskazuje na zwykły błąd systemu Unix, dlatego problem jest często błędnie interpretowany jako błąd uprawnień:
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"Jądro systemu zwraca 13: Permission denied dla obu rodzajów odmowy dostępu, zarówno standardowej, jak i wynikającej z SELinux. Pierwszym krokiem jest zatem ustalenie, która warstwa zablokowała dostęp. Nie należy zaczynać od setenforce 0.
Wymagany model działania
SELinux to system obowiązkowej kontroli dostępu, znany jako MAC. Każdy proces działa w domenie, takiej jak httpd_t dla serwera WWW. Każdy plik i każdy port sieciowy posiada typ, na przykład httpd_sys_content_t. Polityka stanowi listę dozwolonych kombinacji domeny, typu i akcji; wszystko, czego nie ma na tej liście, jest blokowane. Mechanizm ten działa po klasycznej weryfikacji uprawnień systemu Unix, więc bity uprawnień w formacie drwxr-xr-x muszą najpierw zezwalać na dostęp. Obie warstwy muszą wyrazić zgodę.
Pełny kontekst składa się z czterech pól oddzielonych dwukropkami, jak system_u:system_r:httpd_t:s0: użytkownik SELinux, rola, typ oraz poziom. Na serwerze większość czasu poświęca się na pracę z trzecim polem, czyli typem. Dwa polecenia wyświetlają aktualne wartości:
ps -eZ | grep nginx
id -ZProcesy robocze nginx wykazują kontekst kończący się na httpd_t. Powłoka użytkownika wykazuje unconfined_u:unconfined_r:unconfined_t:s0, ponieważ domyślna polityka targeted ogranicza usługi, pozostawiając użytkowników interaktywnych bez restrykcji. Warto o tym pamiętać, ponieważ SELinux nie zastępuje uruchamiania usług na kontach o ograniczonych uprawnieniach. Ogranicza on zakres działań usługi w przypadku przejęcia jej przez osobę niepowołaną.
Trzy tryby pracy oraz dystrybucje z włączonym SELinux
sestatus
getenforceTryb Enforcing blokuje działania niezgodne z polityką i zapisuje je w logach. Tryb Permissive zezwala na wszystkie operacje, rejestrując w logach zdarzenia, które zostałyby zablokowane. Tryb Disabled całkowicie wyłącza ładowanie polityk. Polecenie getenforce wyświetla aktualny tryb pracy. Polecenie sestatus wyświetla również tryb zdefiniowany w pliku /etc/selinux/config, który jest przywracany po ponownym uruchomieniu systemu.
Systemy Rocky Linux, AlmaLinux, Fedora oraz RHEL są domyślnie dostarczane z aktywnym SELinux w trybie Enforcing oraz polityką targeted. Systemy Ubuntu oraz Debian zamiast tego korzystają z AppArmor, który realizuje te same zadania przy użyciu innego mechanizmu (szczegóły w ostatniej sekcji). W rezultacie ta sama aplikacja może zainstalować się poprawnie na jednym serwerze, podczas gdy na innym zwróci błąd 403.
Zainstaluj narzędzia, zanim będą potrzebne
sudo dnf install -y policycoreutils-python-utils setroubleshoot-serversemanage: command not found w minimalnym obrazie systemu oznacza brak policycoreutils-python-utils: ten pakiet zawiera semanage oraz audit2allow. setroubleshoot-server dodaje sealert i zapisuje w dzienniku podsumowanie każdej odmowy dostępu w zrozumiałym języku. Zainstaluj oba pakiety na świeżym serwerze, ponieważ moment, w którym będą potrzebne, jest momentem, w którym coś już uległo awarii.
Jak odczytywać odmowy SELinux w dzienniku audit
Każda odmowa jest rejestrowana przez demona audit jako komunikat 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=0Cztery pola zawierają pełną informację. comm to program, który został zablokowany. scontext to kontekst źródłowy, czyli domena, w której działał proces. tcontext to kontekst docelowy, czyli etykieta obiektu, do którego proces próbował uzyskać dostęp. tclass to rodzaj obiektu, w tym przypadku plik. Interpretacja: proces w httpd_t próbował odczytać plik z etykietą default_t, a permissive=0 wskazuje, że żądanie zostało faktycznie zablokowane, a nie tylko odnotowane.
Jeśli ausearch nie zwraca żadnych wyników, demon audit może nie być uruchomiony. Odmowy trafiają wtedy do bufora pierścieniowego jądra:
sudo journalctl -k | grep -i avcTeraz należy przekształcić ten zapis na zrozumiały komunikat:
sudo ausearch -m AVC -ts recent | audit2why
sudo journalctl -t setroubleshoot --since today
sudo sealert -a /var/log/audit/audit.logaudit2why odczytuje te same rekordy i wskazuje rozpoznaną przyczynę: wyłączoną zmienną boolean, etykietę niezgodną z polityką lub brak odpowiedniej reguły. sealert analizuje cały dziennik i dla każdej odmowy drukuje sugerowane polecenie. Sugestię należy traktować jako wskazówkę. Sformułowania zmieniają się między wydaniami, a sealert czasami proponuje niestandardowy moduł polityki, podczas gdy poprawnym rozwiązaniem jest jednowierszowa poprawka etykiety.
Warto wiedzieć o jeszcze jednej rzeczy. Polityka zawiera reguły dontaudit, które ukrywają odmowy uznane za niegroźne, przez co program może działać nieprawidłowo, podczas gdy dziennik pozostaje pusty. Można je odkryć na czas trwania testu:
sudo semodule -DB
# reproduce the problem, then read the log again
sudo semodule -BNaprawa błędnie oznaczonej ścieżki za pomocą semanage fcontext oraz restorecon
Dwa polecenia, przy czym kolejność ma znaczenie. semanage fcontext -a rejestruje, jakie powinno być oznaczenie dla danej ścieżki. restorecon stosuje to zarejestrowane ustawienie domyślne do plików na dysku.
sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"
sudo restorecon -Rv /data/www
ls -ldZ /data/www/index.htmlŚcieżka jest wyrażeniem regularnym. (/.*)? obejmuje sam katalog oraz całą jego zawartość, co jest wymagane w przypadku głównego katalogu dokumentów. Przed wprowadzeniem zmian warto sprawdzić, co zostanie zmodyfikowane: sudo restorecon -Rvn /data/www wyświetla planowane zmiany oznaczeń, ponieważ flaga -n oznacza brak akcji. Po wykonaniu właściwego polecenia restorecon oznaczenie przyjmuje wartość httpd_sys_content_t, a błąd 403 znika bez konieczności restartu usługi.
Polecenia chcon należy używać wyłącznie w celach testowych. chcon -t httpd_sys_content_t index.html ustawia oznaczenie bezpośrednio, jednak kolejne polecenie restorecon, aktualizacja pakietu lub pełne przywrócenie oznaczeń zresetuje to ustawienie, ponieważ polityka nadal wskazuje, że ścieżka powinna mieć inną wartość. semanage fcontext to wersja, która jest trwała. Listę zarejestrowanych ustawień można wyświetlić za pomocą sudo semanage fcontext -l | grep '^/data'.
Treści, do których usługa musi mieć uprawnienia zapisu, wymagają innego typu. Należy użyć httpd_sys_rw_content_t dla katalogu przesyłania plików lub pamięci podręcznej i ograniczyć to tylko do tych ścieżek: witryna przeznaczona tylko do odczytu z przypisanym typem umożliwiającym zapis zapewnia szerszy dostęp, niż wymaga tego aplikacja.
Dlaczego oznaczenie było błędne? Prawie zawsze wynika to ze sposobu przeniesienia plików. mv zachowuje istniejące oznaczenie pliku, więc witryna przeniesiona z /root dociera z oznaczeniem admin_home_t i tak pozostaje. Zwykłe polecenie cp nadaje nowemu plikowi domyślne oznaczenie katalogu docelowego, co zazwyczaj jest pożądanym zachowaniem, podczas gdy cp -a oraz rsync -X kopiują oznaczenia źródłowe wraz z plikiem. Polecenie git clone do nowego katalogu najwyższego poziomu skutkuje oznaczeniem default_t. Jeśli strona ładuje się poprawnie z /usr/share/nginx/html, a nie działa z własnego katalogu, jest to właśnie przyczyna.
Naprawa klasy zachowań za pomocą wartości logicznej
Niektóre awarie nie wynikają z problemów z etykietami. Reverse proxy na świeżo zainstalowanym systemie Rocky lub AlmaLinux zwraca błąd 502, a dziennik błędów zawiera:
2026/08/11 10:02:55 [crit] 1183#1183: *3 connect() to 127.0.0.1:3000 failed (13: Permission denied) while connecting to upstreamBackend działa poprawnie. Domyślna konfiguracja httpd_t nie zezwala na nawiązywanie wychodzących połączeń sieciowych, dlatego wywołanie connect() jest odrzucane, zanim dotrze do interfejsu loopback. Całym tym zachowaniem steruje jeden przełącznik:
getsebool -a | grep httpd_can_network
sudo setsebool -P httpd_can_network_connect on-P to kluczowa flaga: zapisuje ona wartość na dysku. Bez użycia -P zmiana zostanie utracona przy następnym restarcie, co spowoduje, że usługa będzie działać tylko do momentu ponownego uruchomienia maszyny. Potwierdź zmianę za pomocą semanage boolean -l | grep httpd_can_network_connect, które wyświetla bieżącą wartość obok wartości zapisanej.
Zawsze, gdy istnieje taka możliwość, należy preferować wartości logiczne zamiast ręcznie pisanych reguł. Wartości logiczne są dostarczane wraz z polityką dystrybucji, dzięki czemu są utrzymywane, udokumentowane i łatwe do odnalezienia dla kolejnych administratorów. getsebool -a wyświetla listę wszystkich takich ustawień w systemie.
Konfiguracja nasłuchiwania usługi na niestandardowym porcie
Porty również podlegają etykietowaniu. Przeniesienie nginx na port 8081 powoduje błąd uruchomienia:
nginx: [emerg] bind() to 0.0.0.0:8081 failed (13: Permission denied)httpd_t może wiązać porty oznaczone etykietą http_port_t, a 8081 nie znajduje się w tym zbiorze. Należy go dodać:
sudo semanage port -l | grep -w http_port_t
sudo semanage port -a -t http_port_t -p tcp 8081Najpierw należy sprawdzić listę. Wiele portów o wysokich numerach jest już dozwolonych, w tym 8008 oraz 8443, a próba dwukrotnego dodania tego samego portu kończy się błędem ValueError: Port tcp/8081 already defined. Jeśli port jest już przypisany do innego typu, należy użyć semanage port -m -t http_port_t -p tcp 8081 zamiast dodawania.
To samo polecenie umożliwia działanie SSH na zmienionym porcie. Błąd Bind to port 2222 on 0.0.0.0 failed: Permission denied w journalctl -u sshd oznacza, że port 2222 nie znajduje się w ssh_port_t, dlatego przed restartem demona i zamknięciem sesji należy wykonać sudo semanage port -a -t ssh_port_t -p tcp 2222. Jest to krok pomijany przez użytkowników korzystających z ogólnych poradników dotyczących utwardzania SSH na VPS w systemach z rodziny Red Hat. SELinux nie pełni funkcji firewalla, więc port musi zostać otwarty: sudo firewall-cmd --permanent --add-port=8081/tcp && sudo firewall-cmd --reload w tym przypadku lub ufw w obrazach Debian lub Ubuntu.
Gdy brak wartości logicznej lub etykiety do zmiany
Sytuacja ta rzadko występuje na standardowym serwerze i to właśnie wtedy użytkownicy wyrządzają szkody. audit2allow pozwala zbudować moduł polityki na podstawie odmów dostępu zarejestrowanych w logach:
sudo ausearch -m AVC -ts recent -c nginx | audit2allow -M nginx_local
cat nginx_local.te
sudo semodule -i nginx_local.ppPrzed instalacją należy przeczytać nginx_local.te. Dwa nawyki pozwalają zachować bezpieczeństwo. Należy przefiltrować dane wejściowe tylko dla programu, który jest naprawiany, za pomocą -c, ponieważ przekierowanie tygodniowego zestawu niepowiązanych odmów do audit2allow spowoduje przyznanie wszystkich uprawnień jednocześnie. Nigdy nie należy instalować modułu zbudowanego na podstawie odmowy, której nie można wyjaśnić: regułę pozwalającą httpd_t na odczyt każdego pliku w systemie łatwo wygenerować, lecz trudno wykryć po kilku miesiącach. Moduł usuwa się za pomocą sudo semodule -r nginx_local.
Tryb Permissive jest narzędziem diagnostycznym, a nie rozwiązaniem
sudo setenforce 0
# reproduce the problem once, all the way through
sudo ausearch -m AVC -ts recent
sudo setenforce 1Tryb Permissive zezwala na dostęp i rejestruje go w logach. Jego główną zaletą jest kompletność danych. W trybie Enforcing usługa zatrzymuje się przy pierwszej odmowie dostępu, co wymusza naprawę jednego błędu, restart i napotkanie kolejnego. W trybie Permissive proces jest kontynuowany, a logi gromadzą wszystkie odmowy w jednym przebiegu, co pozwala na ich jednoczesną analizę i naprawę po powrocie do trybu Enforcing.
setenforce nie modyfikuje /etc/selinux/config, dlatego restart przywraca system do trybu Enforcing. Jest to mechanizm zabezpieczający, który wyjaśnia, dlaczego "poprawka" polegająca na setenforce 0 przestaje działać w najmniej odpowiednim momencie. Jeśli podczas prac serwisowych wymagane jest poluzowanie restrykcji dla konkretnej usługi, należy oznaczyć odpowiednią domenę zamiast całego systemu: sudo semanage permissive -a httpd_t pozostawia resztę systemu w trybie Enforcing, a sudo semanage permissive -d httpd_t cofa tę zmianę.
Dlaczego wyłączenie SELinux kosztuje więcej niż poprawienie etykiety
Ustawienie SELINUX=disabled w /etc/selinux/config zamienia jednowierszową poprawkę etykiety na trwale osłabiony serwer. Różnica ujawnia się w dniu, w którym aplikacja internetowa zostaje przejęta. W trybie enforcing kod atakującego działa w httpd_t, więc może odczytywać zawartość stron, podczas gdy odczyt /etc/shadow lub zapis jednostki systemd jest odrzucany przez politykę, niezależnie od uprawnień użytkownika Unix. Bez załadowanej polityki ten sam kod otrzymuje wszystkie uprawnienia konta usługi.
Wyłączenie systemu wiąże się również z późniejszymi kosztami. Gdy polityka nie jest załadowana, nowe pliki tworzone są bez etykiet, przez co system plików przestaje być zgodny z polityką. Ponowne włączenie SELinux wymaga wtedy pełnego przeetykietowania, w przeciwnym razie wiele usług przestanie działać jednocześnie:
sudo fixfiles -F onboot
sudo rebootTo polecenie tworzy /.autorelabel i przeetykietowuje cały system plików podczas następnego rozruchu. Na dużym dysku proces ten trwa długo, a konsola może wyglądać na zawieszoną, dlatego należy go uruchomić, gdy czas oczekiwania nie stanowi problemu. W systemach Rocky Linux i AlmaLinux 9 plik konfiguracyjny nie wyłącza już samodzielnie części jądra, a udokumentowanym sposobem na całkowite wyłączenie SELinux jest argument jądra (sudo grubby --update-kernel ALL --args selinux=0). Znajomość tego polecenia pomaga w przypadku przejęcia serwera po kimś innym. Nie jest to jednak rozwiązanie błędu 403.
Dodawanie etykiety dla kontenerów
Na hoście z rodziny Red Hat procesy kontenerów działają w container_t i mogą odczytywać wyłącznie pliki z etykietą container_file_t. Montowanie typu bind mount z hosta kończy się błędem Permission denied wewnątrz kontenera, mimo że ls -l na hoście wygląda poprawnie. Sufiks :Z informuje środowisko uruchomieniowe o konieczności zmiany etykiety punktu montowania:
docker run -d -v /data/appdata:/var/lib/app:Z myimage:Z nadaje etykietę katalogu wyłącznie dla tego kontenera. :z nadaje etykietę umożliwiającą współdzielenie między kontenerami. Wskazanie :Z na katalog używany przez inne usługi spowoduje rekurencyjną zmianę etykiet, co doprowadzi do awarii tych usług, dlatego należy przypisywać kontenerom osobne ścieżki. Pozostałe aspekty konfiguracji są identyczne jak w przypadku innych obrazów, co opisano w uruchamianie Docker na VPS.
Ubuntu i Debian oferują AppArmor
To samo zadanie, inna konstrukcja. AppArmor ogranicza program na podstawie ścieżki do pliku wykonywalnego, używając profilu w /etc/apparmor.d/, zamiast etykietowania plików na dysku. Nie ma tu potrzeby zmiany etykiet ani używania restorecon. Rozpocznij tutaj:
sudo aa-status
sudo journalctl -k | grep -i apparmorOdmowa dostępu pojawia się jako apparmor="DENIED" operation="open" profile="/usr/sbin/nginx" name="/data/www/index.html" requested_mask="r". Schemat pracy jest identyczny: odczytaj odmowę, znajdź profil, zmień regułę. sudo apt install apparmor-utils udostępnia aa-complain (tryb permissive dla jednego profilu) oraz aa-enforce, aby przywrócić pierwotny stan. Ubuntu ogranicza wybrany zestaw usług pakietowych, pozostawiając resztę bez ograniczeń, dlatego sprawdź aa-status, aby zobaczyć, co faktycznie jest aktywne, zamiast zakładać to z góry.
Jeden nawyk sprawdza się w obu systemach. Gdy usługa zgłasza Permission denied przy operacji, która wydaje się poprawna, przejrzyj dziennik bezpieczeństwa przed zmianą uprawnień. Rzadko kiedy problemem są bity uprawnień.
FAQ
Dlaczego nginx zwraca błąd 403, mimo że uprawnienia do pliku są poprawne?
Ponieważ SELinux zablokował odczyt, a nie bity uprawnień systemu plików. Serwer WWW działa w domenie httpd_t i może odczytywać wyłącznie pliki oznaczone jako zawartość WWW, dlatego plik z etykietą default_t lub admin_home_t jest odrzucany, a nginx nie ma czego serwować. Potwierdź to za pomocą sudo ausearch -m AVC -ts recent, co pokaże scontext kończące się na httpd_t oraz tcontext posiadające błędny typ. Następnie ustal poprawną etykietę i zastosuj ją: sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?", a po nim sudo restorecon -Rv /data/www.
Czy uruchomienie setenforce 0 w celu naprawy usługi jest bezpieczne?
setenforce 0 to krok diagnostyczny, a nie rozwiązanie. Użyj go, aby jednorazowo odtworzyć problem, tak aby dziennik zebrał wszystkie odmowy w jednym przebiegu, odczytaj je za pomocą sudo ausearch -m AVC -ts recent, a następnie uruchom sudo setenforce 1 i usuń przyczyny. Serwer pozostawiony w trybie permissive rejestruje każdą odmowę, ale żadnej nie blokuje, więc generuje szum i traci ochronę. Jeśli jedna usługa wymaga swobody podczas prac, uruchom sudo semanage permissive -a httpd_t, aby reszta maszyny pozostała w trybie enforcing.
Jak uruchomić usługę na niestandardowym porcie przy włączonym SELinux?
Dodaj port do typu, do którego usługa ma prawo się przypisać. Dla serwera WWW na porcie 8081: sudo semanage port -a -t http_port_t -p tcp 8081. Dla SSH na porcie 2222: sudo semanage port -a -t ssh_port_t -p tcp 2222. Najpierw sprawdź bieżącą listę za pomocą sudo semanage port -l | grep -w http_port_t, ponieważ port, który jest już na liście, spowoduje błąd ValueError: Port tcp/8081 already defined. Bez tego kroku demon zakończy działanie podczas startu z błędem bind() ... Permission denied, mimo że żaden inny proces nie zajmuje tego portu.
Czy Ubuntu posiada SELinux?
Nie. Ubuntu i Debian dostarczane są z AppArmor, który wymusza profil powiązany ze ścieżką pliku wykonywalnego, a nie z etykietami na plikach. Sprawdź to za pomocą sudo aa-status i szukaj linii apparmor="DENIED" w sudo journalctl -k. Ubuntu ogranicza wybraną grupę usług pakietowych, więc wiele programów domyślnie działa bez ograniczeń. Rocky Linux i AlmaLinux to systemy, w których SELinux w trybie enforcing jest domyślnie aktywny, podobnie jak w Fedora i RHEL.