SSD Nodes Learn Hosting plans →
Przewodniki Matt ConnorAutor: Matt Connor · Zaktualizowano 2026-09-04

SELinux i nginx: przyczyna błędu 403 mimo uprawnień

nginx zwraca 403 mimo prawidłowych uprawnień? Sprawdź odmowę SELinux, popraw etykietę przez semanage i restorecon, pozostawiając tryb enforcing.

Dlaczego nginx zwraca 403 dla pliku, którego uprawnienia są prawidłowe

Jeśli nginx zwraca 403 dla pliku, którego bity uprawnień są prawidłowe, niemal zawsze oznacza to, że SELinux (security-enhanced Linux) odrzuca odczyt. SELinux sprawdza drugi zestaw reguł po przejściu standardowej kontroli uprawnień. Serwer WWW może odczytywać tylko pliki opatrzone etykietą treści WWW. Plik ma inną etykietę, dlatego operacja otwarcia kończy się niepowodzeniem i nginx nie ma czego wysłać.

Należy sprawdzić etykietę, a nie tylko tryb uprawnień:

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

Kropka wyświetlana po drwxr-xr-x oznacza, że plik ma etykietę SELinux. default_t to wartość przypisywana ścieżce, o której zasadach nigdy nie było informacji. Żadna reguła serwera WWW nie zezwala na odczyt tego typu. Dziennik błędów pokazuje zwykły błąd systemu Unix, dlatego problem wygląda jak 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 zwraca 13: Permission denied w przypadku obu rodzajów odmowy: zwykłej oraz wynikającej z SELinux. Najpierw należy więc ustalić, która warstwa odrzuciła operację. Nie należy zaczynać od setenforce 0.

Część modelu, której potrzebujesz

SELinux to mechanizm obowiązkowej kontroli dostępu, zwykle określany skrótem MAC. Każdy proces działa w domenie, takiej jak httpd_t w przypadku serwera WWW. Każdy plik i każdy port sieciowy ma przypisany typ, taki jak httpd_sys_content_t. Polityka zawiera listę dozwolonych kombinacji domeny, typu i działania. Wszystko, czego nie ma na tej liście, jest odrzucane. Kontrola SELinux działa po klasycznej kontroli systemu Unix, dlatego bity uprawnień w drwxr-xr-x również muszą najpierw zezwalać na dostęp. Obie warstwy muszą zezwolić na operację.

Pełny kontekst ma cztery pola oddzielone dwukropkami, na przykład system_u:system_r:httpd_t:s0: użytkownika SELinux, rolę, typ i poziom. Na serwerze niemal zawsze analizuje się trzecie pole, czyli typ. Bieżące wartości można wyświetlić za pomocą dwóch poleceń:

ps -eZ | grep nginx
id -Z

Procesy robocze nginx mają kontekst zakończony wartością httpd_t. Powłoka po zalogowaniu ma wartość unconfined_u:unconfined_r:unconfined_t:s0, ponieważ domyślna polityka targeted ogranicza usługi, ale nie ogranicza interaktywnych użytkowników. Jest to istotne, ponieważ SELinux nie zastępuje uruchamiania usług z użyciem użytkowników o minimalnych uprawnieniach. Ogranicza zasoby dostępne dla usługi po jej przejęciu przez atakującego.

Trzy tryby oraz obrazy, w których jest SELinux

sestatus
getenforce

Tryb Enforcing blokuje operacje i zapisuje je w dzienniku. Tryb Permissive zezwala na wszystkie operacje i zapisuje informacje o tym, co zostałoby zablokowane. Tryb Disabled nie ładuje żadnej polityki. getenforce wyświetla bieżący tryb. sestatus wyświetla również tryb z /etc/selinux/config, czyli ten, który zostanie przywrócony po ponownym uruchomieniu.

Rocky Linux, AlmaLinux, Fedora i RHEL są dostarczane z SELinux w trybie Enforcing oraz z polityką targeted. Ten wspólny domyślny wybór wynika ze wspólnego pochodzenia tych systemów, a nie z przypadku, ponieważ wszystkie wywodzą się z tej samej linii Red Hat, która obejmowała CentOS, zanim pojawiły się Rocky Linux i AlmaLinux. Wybór jednego z tych dwóch systemów nie wpływa na informacje przedstawione na tej stronie, ponieważ dostarczają tę samą politykę i te same narzędzia. Dlatego wybór między Rocky Linux i AlmaLinux zależy od deklarowanej zgodności oraz obsługi starszych procesorów, a nie od domyślnych ustawień zabezpieczeń. Ubuntu i Debian są dostarczane z AppArmor, który pełni tę samą funkcję, ale wykorzystuje inny mechanizm. Omówiono go w ostatniej sekcji. Dlatego ta sama aplikacja może zainstalować się poprawnie na jednym serwerze, a na innym zwracać kod 403.

Zainstaluj narzędzia, zanim będą potrzebne

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

semanage: command not found na obrazie minimalnym oznacza brak pakietu policycoreutils-python-utils: ten pakiet zawiera semanage i audit2allow. setroubleshoot-server dodaje sealert i zapisuje w dzienniku zrozumiałe podsumowanie każdego odrzucenia. Zainstaluj oba pakiety na nowym serwerze, ponieważ gdy stają się potrzebne, coś jest już zepsute.

Jak odczytać odmowę SELinux w dzienniku audit

Każda odmowa jest zapisywana przez demona audit jako komunikat 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

Całą informację zawierają cztery pola. comm to program, którego działanie zablokowano. 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 określa typ obiektu, w tym przypadku plik. Odczytane razem oznaczają, że proces w httpd_t próbował odczytać plik oznaczony etykietą default_t, a permissive=0 wskazuje, że żądanie rzeczywiście zablokowano, a nie tylko zapisano w dzienniku.

Jeśli ausearch nic nie wyświetla, demon audit może nie działać. Odmowy trafiają wtedy do bufora pierścieniowego jądra:

sudo journalctl -k | grep -i avc

Teraz przekształć rekord w zdanie:

sudo ausearch -m AVC -ts recent | audit2why
sudo journalctl -t setroubleshoot --since today
sudo sealert -a /var/log/audit/audit.log

audit2why odczytuje te same rekordy i wskazuje rozpoznaną przyczynę: wyłączony boolean, etykietę niezgodną z polityką albo całkowity brak reguły. sealert przegląda cały dziennik i wyświetla sugerowane polecenie dla każdej odmowy. Traktuj sugestię jako wskazówkę. Treść komunikatów zmienia się między wydaniami, a sealert czasami proponuje niestandardowy moduł polityki, mimo że prawidłowym rozwiązaniem jest jednoliniowa poprawka etykiety.

Należy znać jeszcze jedną rzecz. Polityka zawiera reguły dontaudit, które ukrywają odmowy uznane za nieszkodliwe. Program może więc działać nieprawidłowo, mimo że dziennik pozostaje pusty. Odkryj te odmowy na czas jednego testu:

sudo semodule -DB
# reproduce the problem, then read the log again
sudo semodule -B

Naprawa nieprawidłowej etykiety ścieżki za pomocą semanage fcontext i restorecon

Należy użyć dwóch poleceń, a kolejność ma znaczenie. semanage fcontext -a rejestruje, jaka powinna być etykieta ścieżki. restorecon stosuje zarejestrowaną etykietę domyślną 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 katalogu głównego dokumentów. Przed wprowadzeniem zmiany należy sprawdzić jej przewidywany zakres: sudo restorecon -Rvn /data/www wyświetla planowane zmiany etykiet, ponieważ -n oznacza brak wykonywania operacji. Po rzeczywistym wykonaniu restorecon etykieta ma wartość httpd_sys_content_t, a błąd 403 znika bez restartowania usługi.

chcon należy używać wyłącznie do testów. chcon -t httpd_sys_content_t index.html ustawia etykietę bezpośrednio, a następne restorecon, aktualizacja pakietów lub pełne ponowne etykietowanie ją resetuje, ponieważ polityka nadal określa inną etykietę dla tej ścieżki. Na serwerze, na którym dnf-automatic stosuje aktualizacje zabezpieczeń zgodnie z harmonogramem, reset nastąpi zgodnie z własnym harmonogramem, a nie wtedy, gdy użytkownik pracuje bezpośrednio na maszynie. W rezultacie witryna może przestać działać kilka godzin po ostatniej wykonanej zmianie. semanage fcontext to wersja, która pozostaje zachowana. Zarejestrowane ustawienia można wyświetlić za pomocą sudo semanage fcontext -l | grep '^/data'.

Dla treści, którą usługa musi zapisywać, wymagany jest inny typ. Należy użyć httpd_sys_rw_content_t dla katalogu przesyłania lub pamięci podręcznej i ograniczyć ten typ do tych ścieżek. Witryna tylko do odczytu z typem umożliwiającym zapis zapewnia aplikacji większy zakres dostępu, niż jest jej potrzebny.

Dlaczego etykieta była nieprawidłowa? Niemal zawsze wynika to ze sposobu dostarczenia plików. mv zachowuje istniejącą etykietę pliku, dlatego witryna przeniesiona z /root otrzymuje etykietę admin_home_t i pozostaje z nią. Zwykłe cp nadaje nowemu plikowi domyślną etykietę katalogu docelowego, co zazwyczaj jest właściwym rozwiązaniem, natomiast cp -a i rsync -X kopiują etykiety źródłowe razem z plikiem. git clone do nowego katalogu najwyższego poziomu powoduje powstanie default_t. Gdy strona ładuje się poprawnie z /usr/share/nginx/html, ale nie działa z własnego katalogu, przyczyną jest właśnie to.

Naprawianie określonej klasy problemów za pomocą wartości logicznej

Niektóre awarie nie wynikają z problemu z etykietą. Reverse proxy na świeżo zainstalowanym systemie Rocky lub AlmaLinux zwraca kod 502, a dziennik błędów zawiera wpis:

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

Backend działa prawidłowo. Domena httpd_t domyślnie nie może otwierać wychodzących połączeń sieciowych, dlatego wywołanie connect() zostaje odrzucone, 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

Istotną flagą jest -P. Zapisuje ona wartość na dysku. Bez -P zmiana zostanie utracona przy następnym restarcie, co powoduje, że usługa działa tylko do czasu ponownego uruchomienia komputera. Potwierdzenie umożliwia semanage boolean -l | grep httpd_can_network_connect, które wyświetla bieżącą wartość obok wartości zapisanej.

Jeżeli istnieje odpowiednia wartość logiczna, należy użyć jej zamiast ręcznie tworzonej reguły. Wartości logiczne są dostarczane wraz z polityką dystrybucji, dlatego są utrzymywane, udokumentowane i łatwe do znalezienia przez kolejną osobę. getsebool -a wyświetla wszystkie wartości logiczne dostępne w systemie.

Usługa nasłuchująca na niestandardowym porcie

Porty również mają przypisane etykiety. Przenieś Nginx na port 8081. Uruchomienie usługi zakończy się niepowodzeniem:

nginx: [emerg] bind() to 0.0.0.0:8081 failed (13: Permission denied)

httpd_t może wiązać porty oznaczone jako http_port_t, a port 8081 nie znajduje się na tej liście. Dodaj go:

sudo semanage port -l | grep -w http_port_t
sudo semanage port -a -t http_port_t -p tcp 8081

Najpierw sprawdź listę. Kilka portów o wysokich numerach jest już dozwolonych, w tym 8008 i 8443. Ponowne dodanie portu kończy się błędem ValueError: Port tcp/8081 already defined. Jeśli port należy już do innego typu, zmień jego typ za pomocą semanage port -m -t http_port_t -p tcp 8081, zamiast go dodawać.

To samo polecenie umożliwia działanie SSH po przeniesieniu go na inny port. 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. Wykonaj sudo semanage port -a -t ssh_port_t -p tcp 2222 przed ponownym uruchomieniem demona i zamknięciem sesji. Jest to krok pomijany podczas korzystania z ogólnych poradników zabezpieczania SSH na VPS z obrazem należącym do rodziny Red Hat. SELinux nie jest zaporą sieciową, dlatego port nadal musi być otwarty: tutaj za pomocą sudo firewall-cmd --permanent --add-port=8081/tcp && sudo firewall-cmd --reload albo przez ufw na obrazie Debiana lub Ubuntu. Flaga --permanent powoduje tę samą pułapkę związaną z ponownym uruchomieniem systemu co -P użyta dla wartości logicznej, a strefy określające interfejsy, do których odnosi się reguła, warto raz sprawdzić w podstawach firewalld dla VPS z Rocky lub AlmaLinux.

Gdy nie ma ustawienia boolean ani etykiety do zmiany

Na zwykłym serwerze zdarza się to rzadko. W tym miejscu najłatwiej spowodować szkody. audit2allow może zbudować moduł zasad na podstawie odmów dostępu zapisanych w dzienniku:

sudo ausearch -m AVC -ts recent -c nginx | audit2allow -M nginx_local
cat nginx_local.te
sudo semodule -i nginx_local.pp

Przed instalacją modułu należy przeczytać nginx_local.te. Bezpieczne wykonanie tej operacji wymaga dwóch zasad. Za pomocą -c należy ograniczyć dane wejściowe do jednego programu, który jest naprawiany. Przekazanie przez potok tygodnia niezwiązanych odmów dostępu do audit2allow spowoduje jednoczesne przyznanie wszystkich tych uprawnień. Nie wolno też instalować modułu zbudowanego na podstawie odmowy dostępu, której nie można wyjaśnić. Regułę zezwalającą httpd_t na odczyt każdego pliku w systemie łatwo wygenerować, ale trudno zauważyć po kilku miesiącach. Moduł usuwa się za pomocą sudo semodule -r nginx_local.

Tryb permissive służy do diagnostyki, a nie do naprawy

sudo setenforce 0
# reproduce the problem once, all the way through
sudo ausearch -m AVC -ts recent
sudo setenforce 1

Tryb permissive zezwala na dostęp i zapisuje informację o nim w dzienniku. Jego rzeczywistą wartością jest kompletność. W trybie enforcing usługa zatrzymuje się przy pierwszym odmówionym dostępie. Po naprawieniu tego problemu i ponownym uruchomieniu pojawia się kolejny. W trybie permissive działanie jest kontynuowane, a dziennik zbiera wszystkie odmowy w jednym przebiegu. Następnie można ponownie włączyć tryb enforcing i naprawić je razem.

setenforce nie zmienia /etc/selinux/config, dlatego po ponownym uruchomieniu system wraca do trybu enforcing. Jest to zabezpieczenie. Z tego samego powodu „naprawa” polegająca na użyciu setenforce 0 pojawia się ponownie w najmniej odpowiednim momencie. Jeśli podczas pracy tylko jedna usługa potrzebuje tymczasowo szerszych uprawnień, należy oznaczyć jej domenę zamiast całego systemu: sudo semanage permissive -a httpd_t pozostawia pozostałe domeny w trybie enforcing, a sudo semanage permissive -d httpd_t cofa tę zmianę.

Dlaczego wyłączenie SELinux kosztuje więcej niż naprawa etykiety

Ustawienie SELINUX=disabled w /etc/selinux/config zastępuje jednowierszową naprawę etykiety trwale słabszą ochroną serwera. Różnica staje się widoczna w dniu, w którym aplikacja internetowa zostanie przejęta. W trybie enforcing kod atakującego działa w httpd_t, więc może odczytywać zawartość serwisu, natomiast odczyt /etc/shadow lub zapis jednostki systemd zostanie odrzucony przez politykę niezależnie od uprawnień, jakie zezwalałby użytkownik Unix. Bez załadowanej polityki ten sam kod uzyskuje wszystko, do czego ma dostęp konto usługi.

Wyłączenie SELinux powoduje również późniejsze koszty. Gdy żadna polityka nie jest załadowana, nowe pliki są tworzone bez etykiety, przez co stan systemu plików przestaje odpowiadać polityce. Ponowne włączenie SELinux wymaga wtedy pełnego ponownego etykietowania albo prowadzi do jednoczesnego niepowodzenia wielu usług:

sudo fixfiles -F onboot
sudo reboot

Polecenie zapisuje /.autorelabel i podczas następnego uruchomienia ponownie etykietuje każdy system plików. Na dużym dysku trwa to długo, a konsola może wyglądać na zawieszoną. Należy więc rozpocząć tę operację wtedy, gdy można poczekać. Skoro komputer i tak zostanie wyłączony, warto najpierw sprawdzić, jakie inne komponenty oczekują na ponowne uruchomienie. Informacje te przedstawia needs-restarting po tym, jak aktualizacja dnf pozostawiła stare jądra i biblioteki w pamięci. W Rocky Linux i AlmaLinux 9 plik konfiguracyjny nie wyłącza już samodzielnie mechanizmu SELinux w jądrze. Udokumentowany sposób pełnego wyłączenia SELinux polega na użyciu argumentu jądra (sudo grubby --update-kernel ALL --args selinux=0). Znajomość tego polecenia jest przydatna podczas przejmowania cudzego serwera. Nie jest to jednak sposób naprawy błędu 403.

Kontenery wymagają dodatkowej etykiety

Na hoście z rodziny Red Hat procesy kontenerów działają w container_t i mogą odczytywać wyłącznie pliki oznaczone etykietą container_file_t. Dowiązanie bind mount z hosta kończy się w kontenerze błędem Permission denied, mimo że ls -l na hoście wygląda prawidłowo. Sufiks :Z informuje silnik kontenerów, aby zmienił etykietę punktu montowania:

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

:Z oznacza katalog wyłącznie dla tego kontenera. :z oznacza go jako współdzielony między kontenerami. Wskazanie :Z na katalog używany przez inne usługi powoduje rekurencyjną zmianę etykiet w tym katalogu, co zakłóca działanie tych usług. Kontenerom należy więc przypisywać własne ścieżki. Jeśli silnik nie jest jeszcze zainstalowany na hoście, należy pamiętać, że polecenie docker w tych dystrybucjach często wskazuje na podprogram podman. Jest to szczegół, który kroki instalacji w Rocky i AlmaLinux uwzględniają, zanim pojawi się ten problem. Pozostała konfiguracja przebiega tak samo jak w przypadku każdego innego obrazu. Opisano ją w uruchamianiu Docker na VPS.

Ubuntu i Debian udostępniają AppArmor

To samo zadanie, inny model działania. AppArmor ogranicza program na podstawie ścieżki do jego pliku wykonywalnego, korzystając z profilu w /etc/apparmor.d/, zamiast oznaczać pliki na dysku. Nie ma potrzeby ponownego oznaczania plików ani używania restorecon. Rozpocznij od:

sudo aa-status
sudo journalctl -k | grep -i apparmor

Odmowa jest rejestrowana jako apparmor="DENIED" operation="open" profile="/usr/sbin/nginx" name="/data/www/index.html" requested_mask="r". Schemat postępowania jest taki sam: odczytaj odmowę, znajdź profil i zmień regułę. sudo apt install apparmor-utils udostępnia aa-complain (tryb permissive dla jednego profilu) oraz aa-enforce, aby przywrócić poprzedni tryb. Ubuntu ogranicza wybrany zestaw usług dostarczanych w pakietach, a pozostałe pozostawia bez ograniczeń. Odczytaj więc aa-status, aby sprawdzić, co rzeczywiście jest aktywne, zamiast przyjmować to z góry.

Jedna zasada obowiązuje w obu systemach. Gdy usługa zgłasza Permission denied dla zasobu, którego konfiguracja wygląda poprawnie, przed zmianą uprawnień odczytaj dziennik bezpieczeństwa. Uprawnienia rzadko są przyczyną dwóch kolejnych problemów.

FAQ

Dlaczego nginx zwraca błąd 403, gdy uprawnienia pliku są prawidłowe?

Ponieważ SELinux zablokował odczyt, a nie bity uprawnień. Serwer WWW działa w domenie httpd_t i może odczytywać tylko pliki oznaczone etykietą przeznaczoną dla treści WWW. Plik oznaczony jako default_t lub admin_home_t zostaje odrzucony, więc nginx nie ma treści do udostępnienia. Potwierdza to polecenie sudo ausearch -m AVC -ts recent. Pokazuje ono scontext zakończone na httpd_t oraz tcontext zawierające nieprawidłowy typ. Następnie należy zapisać prawidłową etykietę i zastosować ją: sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?", a następnie sudo restorecon -Rv /data/www.

Czy uruchomienie setenforce 0 w celu uruchomienia usługi jest bezpieczne?

setenforce 0 jest czynnością diagnostyczną, a nie rozwiązaniem problemu. Należy użyć go jednorazowo, aby odtworzyć problem i zebrać w dzienniku wszystkie odmowy podczas jednego przebiegu, odczytać je za pomocą sudo ausearch -m AVC -ts recent, a następnie uruchomić sudo setenforce 1 i usunąć przyczyny. Serwer pozostawiony w trybie permissive rejestruje każdą odmowę, ale nie blokuje żadnej operacji. Pozostają więc zbędne komunikaty, a ochrona przestaje działać. Jeśli jedna usługa potrzebuje takiego trybu na czas prac, należy uruchomić sudo semanage permissive -a httpd_t, aby pozostała część systemu nadal działała w trybie enforcing.

Jak uruchomić usługę na niestandardowym porcie przy włączonym SELinux?

Należy dodać port do typu, z którym dana usługa może powiązać port. 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 należy sprawdzić bieżącą listę za pomocą sudo semanage port -l | grep -w http_port_t, ponieważ próba dodania portu, który już na niej figuruje, kończy się błędem ValueError: Port tcp/8081 already defined. Bez tego kroku demon kończy działanie podczas uruchamiania z powodu bind() ... Permission denied, nawet jeśli żaden inny proces nie zajmuje tego portu.

Czy Ubuntu używa SELinux?

Nie. Ubuntu i Debian dostarczają AppArmor, który wymusza profil powiązany ze ścieżką pliku wykonywalnego, a nie z etykietami plików. Jego stan można sprawdzić za pomocą sudo aa-status, a w sudo journalctl -k należy wyszukać wiersze apparmor="DENIED". Ubuntu ogranicza wybrany zestaw usług dostarczanych w pakietach, dlatego wiele programów domyślnie działa bez profilu ograniczającego. Z SELinux w trybie enforcing domyślnie można spotkać się w Rocky Linux i AlmaLinux, a także w Fedora i RHEL.