Nginx 403 foutmelding door SELinux oplossen
Krijgt u een 403-foutmelding in Nginx terwijl de rechten correct lijken? Leer hoe u SELinux-weigeringen analyseert en labels herstelt met semanage en restorecon voor uw server.
Waarom nginx een 403-foutmelding geeft voor een bestand met correcte rechten
Wanneer nginx een 403-foutmelding geeft voor een bestand waarvan de rechten correct zijn ingesteld, is dit bijna altijd het gevolg van SELinux (Security-Enhanced Linux) dat de leestoegang blokkeert. SELinux controleert een tweede set regels nadat de standaard rechten zijn gecontroleerd. De webserver mag alleen bestanden lezen die voorzien zijn van een label voor webinhoud. Uw bestand heeft een ander label, waardoor het openen mislukt en nginx geen gegevens kan versturen.
Bekijk niet alleen de modus, maar ook het label:
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.htmlDe punt die na drwxr-xr-x wordt weergegeven, betekent dat het bestand een SELinux-label heeft. default_t is het label dat een pad krijgt wanneer het beleid dit niet herkent, en niets in de regels van de webserver staat toe om dat type te lezen. Het foutenlogboek toont een standaard Unix-fout, wat de reden is dat dit als een rechtenprobleem wordt geïnterpreteerd:
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"De kernel geeft 13: Permission denied terug voor beide soorten weigeringen, zowel de standaard Unix-weigering als de SELinux-weigering. De eerste stap is daarom om te achterhalen welke laag de toegang heeft geweigerd. Begin niet met setenforce 0.
Het deel van het model dat u nodig heeft
SELinux is een vorm van mandatory access control, doorgaans afgekort als MAC. Elk proces draait in een domein, zoals httpd_t voor de webserver. Elk bestand en elke netwerkpoort heeft een type, zoals httpd_sys_content_t. Het beleid is een lijst met toegestane combinaties van domein, type en actie; alles wat niet op die lijst staat, wordt geweigerd. Het wordt uitgevoerd na de klassieke Unix-controle, dus de permissiebits in drwxr-xr-x moeten de toegang eerst nog steeds toestaan. Beide lagen moeten akkoord gaan.
Een volledige context heeft vier velden gescheiden door dubbele punten, zoals system_u:system_r:httpd_t:s0: de SELinux-gebruiker, de rol, het type en het niveau. Op een server besteedt u bijna al uw tijd aan het derde veld, het type. Twee commando's tonen de actieve waarden:
ps -eZ | grep nginx
id -ZDe nginx-workers tonen een context die eindigt op httpd_t. Uw inlog-shell toont unconfined_u:unconfined_r:unconfined_t:s0, omdat het standaard targeted-beleid services beperkt en interactieve gebruikers ongemoeid laat. Dat is belangrijk om te weten, omdat SELinux niet in de plaats komt van het draaien van services onder gebruikers met minimale rechten. Het beperkt wat een service kan bereiken nadat iemand is ingebroken.
De drie modi en welke images SELinux bevatten
sestatus
getenforceEnforcing blokkeert acties en logt deze. Permissive staat alles toe en logt wat er anders geblokkeerd zou zijn. Disabled laadt geen enkel beleid. getenforce toont de huidige modus. sestatus toont ook de modus uit /etc/selinux/config, wat de modus is die na een herstart wordt toegepast.
Rocky Linux, AlmaLinux, Fedora en RHEL worden geleverd met SELinux in de modus enforcing met het targeted-beleid. Ubuntu en Debian gebruiken in plaats daarvan AppArmor, dat dezelfde taak uitvoert met een ander mechanisme (het laatste hoofdstuk behandelt dit). Hierdoor kan dezelfde applicatie probleemloos op de ene server installeren en op de andere een 403-foutmelding geven.
Installeer de tools voordat u ze nodig heeft
sudo dnf install -y policycoreutils-python-utils setroubleshoot-serversemanage: command not found op een minimale image betekent dat policycoreutils-python-utils ontbreekt: dat pakket bevat semanage en audit2allow. setroubleshoot-server voegt sealert toe en schrijft een begrijpelijke samenvatting van elke weigering naar de journal. Installeer beide op een nieuwe server, want het moment dat u ze nodig heeft, is het moment dat er al iets defect is.
Hoe u een SELinux-weigering in het audit-logboek leest
Elke weigering wordt door de audit daemon vastgelegd als een AVC-bericht (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=0Vier velden vertellen het volledige verhaal. comm is het programma dat werd geblokkeerd. scontext is de broncontext, het domein waarin het proces actief was. tcontext is de doelcontext, het label op het object dat het proces probeerde te benaderen. tclass is het type object, in dit geval een bestand. Gecombineerd leest u: het proces in httpd_t probeerde een bestand met het label default_t te lezen, en permissive=0 geeft aan dat het verzoek daadwerkelijk werd geblokkeerd in plaats van alleen gelogd.
Als ausearch niets weergeeft, draait de audit daemon mogelijk niet. Weigeringen komen dan terecht in de kernel ring buffer:
sudo journalctl -k | grep -i avcVertaal het record nu naar een begrijpelijke zin:
sudo ausearch -m AVC -ts recent | audit2why
sudo journalctl -t setroubleshoot --since today
sudo sealert -a /var/log/audit/audit.logaudit2why leest dezelfde records en benoemt de herkende oorzaak: een boolean die uitgeschakeld staat, een label dat niet overeenkomt met het beleid, of het ontbreken van een regel. sealert doorloopt het volledige logboek en print per weigering een voorgesteld commando. Beschouw dit voorstel als een aanwijzing. De formulering verschilt per release en sealert stelt soms een aangepaste policy-module voor, terwijl een correctie van het label met één regel de juiste oplossing is.
Nog één belangrijk punt. Het beleid bevat dontaudit-regels die weigeringen verbergen die als ongevaarlijk worden beschouwd, waardoor een programma zich onjuist kan gedragen terwijl het logboek leeg blijft. Maak deze tijdelijk zichtbaar voor de duur van één test:
sudo semodule -DB
# reproduce the problem, then read the log again
sudo semodule -BEen onjuist gelabeld pad herstellen met semanage fcontext en restorecon
Twee commando's, waarbij de volgorde van belang is. semanage fcontext -a legt vast wat het label voor een pad zou moeten zijn. restorecon past die geregistreerde standaard toe op de bestanden op de schijf.
sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"
sudo restorecon -Rv /data/www
ls -ldZ /data/www/index.htmlHet pad is een reguliere expressie. (/.*)? omvat de map zelf en alles wat daaronder valt, wat noodzakelijk is voor een document root. Bekijk wat er zou veranderen voordat u de wijziging doorvoert: sudo restorecon -Rvn /data/www toont de geplande herlabelingen, omdat -n betekent dat er geen actie wordt ondernomen. Na een daadwerkelijke restorecon is het label httpd_sys_content_t en is de 403-fout verdwenen zonder dat de service opnieuw hoeft te worden opgestart.
Gebruik chcon alleen als test. chcon -t httpd_sys_content_t index.html stelt het label direct in, maar de volgende restorecon, pakketupdate of volledige herlabeling zet dit terug, omdat het beleid nog steeds voorschrijft dat het pad een andere waarde moet hebben. semanage fcontext is de versie die behouden blijft. Bekijk wat u heeft vastgelegd met sudo semanage fcontext -l | grep '^/data'.
Inhoud waar de service naar moet schrijven, vereist een ander type. Gebruik httpd_sys_rw_content_t voor een uploadmap of een cache, en beperk dit tot die paden: een alleen-lezen website onder een schrijfbaar type verleent meer toegang dan de applicatie nodig heeft.
Waarom was het label überhaupt onjuist? Vrijwel altijd door de manier waarop de bestanden zijn aangekomen. mv behoudt het bestaande label van een bestand, waardoor een site die uit /root is verplaatst, aankomt met het label admin_home_t en dat behoudt. Een standaard cp geeft het nieuwe bestand het standaardlabel van de doelmap, wat meestal is wat u wilt, terwijl cp -a en rsync -X de bronlabels samen met het bestand kopiëren. Een git clone naar een nieuwe map op het hoogste niveau resulteert in default_t. Wanneer een pagina correct laadt vanuit /usr/share/nginx/html maar faalt vanuit uw eigen map, is dit de reden.
Een gedragsklasse corrigeren met een boolean
Sommige fouten zijn geen kwestie van labels. Een reverse proxy op een verse Rocky of AlmaLinux-server geeft een 502-foutmelding en het foutenlogboek vermeldt:
2026/08/11 10:02:55 [crit] 1183#1183: *3 connect() to 127.0.0.1:3000 failed (13: Permission denied) while connecting to upstreamUw upstream is in orde. Het httpd_t-domein mag standaard geen uitgaande netwerkverbindingen openen, waardoor de connect()-aanroep wordt geweigerd voordat deze de loopback-interface bereikt. Eén schakelaar beheert dit volledige gedrag:
getsebool -a | grep httpd_can_network
sudo setsebool -P httpd_can_network_connect on-P is de relevante vlag; deze schrijft de waarde naar de schijf. Zonder -P gaat de wijziging verloren bij de volgende herstart, wat resulteert in een service die werkt totdat de machine opnieuw opstart. Bevestig dit met semanage boolean -l | grep httpd_can_network_connect, die de actieve waarde naast de opgeslagen waarde weergeeft.
Geef de voorkeur aan een boolean boven een handgeschreven regel wanneer er een beschikbaar is. Booleans worden meegeleverd met het distributiebeleid, waardoor ze onderhouden en gedocumenteerd zijn en gemakkelijk vindbaar voor de volgende beheerder. getsebool -a somt elk exemplaar op het systeem op.
Een service laten luisteren op een niet-standaard poort
Poorten zijn ook voorzien van labels. Verplaats Nginx naar 8081 en de service weigert te starten:
nginx: [emerg] bind() to 0.0.0.0:8081 failed (13: Permission denied)httpd_t mag poorten binden die zijn voorzien van het label http_port_t, en 8081 hoort daar niet bij. Voeg deze toe:
sudo semanage port -l | grep -w http_port_t
sudo semanage port -a -t http_port_t -p tcp 8081Controleer eerst de lijst. Verschillende hoge poorten zijn al toegestaan, waaronder 8008 en 8443. Het dubbel toevoegen van een poort mislukt met ValueError: Port tcp/8081 already defined. Als de poort al bij een ander type hoort, wijzig deze dan met semanage port -m -t http_port_t -p tcp 8081 in plaats van deze toe te voegen.
Hetzelfde commando zorgt ervoor dat een verplaatste SSH-poort werkt. Bind to port 2222 on 0.0.0.0 failed: Permission denied in journalctl -u sshd betekent dat 2222 ontbreekt in ssh_port_t, dus voer sudo semanage port -a -t ssh_port_t -p tcp 2222 uit voordat u de daemon herstart en uw sessie sluit. Dit is de stap die mensen overslaan wanneer ze een algemene handleiding volgen voor het beveiligen van SSH op een VPS op een image uit de Red Hat-familie. SELinux is bovendien geen firewall, dus de poort moet nog steeds worden geopend: sudo firewall-cmd --permanent --add-port=8081/tcp && sudo firewall-cmd --reload in dit geval, of ufw op een Debian- of Ubuntu-image.
Wanneer er geen boolean of label is om aan te passen
Dit komt zelden voor op een standaardserver en is het punt waarop gebruikers schade aanrichten. audit2allow kan een policy-module bouwen op basis van de denials in het logbestand:
sudo ausearch -m AVC -ts recent -c nginx | audit2allow -M nginx_local
cat nginx_local.te
sudo semodule -i nginx_local.ppLees nginx_local.te voordat u deze installeert. Twee gewoontes houden dit proces veilig. Filter de invoer naar het specifieke programma dat u probeert te herstellen met -c, omdat het doorsluizen van een week aan ongerelateerde denials naar audit2allow deze allemaal tegelijk toestaat. Installeer bovendien nooit een module die is gebouwd op basis van een denial die u niet kunt verklaren: een regel die httpd_t toestaat om elk bestand op de server te lezen, is eenvoudig te genereren, maar maanden later lastig te detecteren. Verwijder een module met sudo semodule -r nginx_local.
Permissive is een diagnostische modus, geen oplossing
sudo setenforce 0
# reproduce the problem once, all the way through
sudo ausearch -m AVC -ts recent
sudo setenforce 1De permissive-modus staat toegang toe en logt deze. De werkelijke waarde hiervan is volledigheid. In de enforcing-modus stopt de service bij de eerste weigering; u lost deze op, herstart, en stuit vervolgens op de volgende. In de permissive-modus gaat de uitvoering door en verzamelt het logbestand elke weigering in één keer, waarna u terugschakelt en ze gezamenlijk verhelpt.
setenforce wijzigt /etc/selinux/config niet, dus een herstart brengt de server terug naar de enforcing-modus. Dit is een vangnet en tevens de reden waarom een "oplossing" die bestond uit setenforce 0 op het slechtst mogelijke moment weer ongedaan wordt gemaakt. Als een service ruimte nodig heeft terwijl u eraan werkt, markeer dan dat specifieke domein in plaats van de gehele machine: sudo semanage permissive -a httpd_t laat al het overige in de enforcing-modus, en sudo semanage permissive -d httpd_t draait dit terug.
Waarom het uitschakelen van SELinux meer kost dan het herstellen van de label
Het instellen van SELINUX=disabled in /etc/selinux/config ruilt een eenregelige correctie van een label in voor een permanent zwakkere server. Het verschil wordt duidelijk op de dag dat een webapplicatie wordt gecompromitteerd. In de modus enforcing draait de code van de aanvaller in httpd_t, waardoor deze mogelijk webinhoud kan lezen, terwijl het lezen van /etc/shadow of het schrijven van een systemd-unit door het beleid wordt geweigerd, ongeacht wat de Unix-gebruiker zou hebben toegestaan. Zonder geladen beleid krijgt diezelfde code alle rechten van het service-account.
Uitschakelen brengt ook kosten met zich mee die u later betaalt. Terwijl er geen beleid is geladen, worden nieuwe bestanden aangemaakt zonder label, waardoor het bestandssysteem uit de pas loopt met het beleid. Het opnieuw inschakelen van SELinux vereist dan een volledige relabel, anders falen meerdere services tegelijkertijd:
sudo fixfiles -F onboot
sudo rebootDit schrijft /.autorelabel en voorziet elk bestandssysteem van nieuwe labels tijdens de volgende boot. Op een grote schijf duurt dit lang en lijkt de console vastgelopen, dus start dit proces alleen wanneer u kunt wachten. Op Rocky Linux en AlmaLinux 9 schakelt het configuratiebestand het kernel-onderdeel niet langer zelfstandig uit, en de gedocumenteerde manier om SELinux volledig uit te schakelen is via een kernel-argument (sudo grubby --update-kernel ALL --args selinux=0). Het kennen van dat commando helpt wanneer u de server van iemand anders overneemt. Het is niet de oplossing voor een 403-fout.
Containers een extra label toevoegen
Op een host uit de Red Hat-familie draaien containerprocessen in container_t en mogen zij alleen bestanden lezen die zijn gelabeld als container_file_t. Een bind mount vanaf de host faalt met Permission denied binnen de container, terwijl ls -l op de host er volkomen normaal uitziet. Het :Z-achtervoegsel instrueert de runtime om de mount opnieuw te labelen:
docker run -d -v /data/appdata:/var/lib/app:Z myimage:Z labelt de map uitsluitend voor deze container. :z labelt deze voor gebruik door meerdere containers. Wijs :Z naar een map die andere services gebruiken en deze zal de map recursief opnieuw labelen, wat die services onbruikbaar maakt; geef containers daarom hun eigen paden. Alle overige aspecten van de configuratie komen overeen met elke andere image, zoals beschreven in Docker draaien op een VPS.
Ubuntu en Debian bieden AppArmor
Hetzelfde doel, een ander ontwerp. AppArmor beperkt een programma op basis van het pad naar het uitvoerbare bestand, waarbij gebruik wordt gemaakt van een profiel onder /etc/apparmor.d/, in plaats van bestanden op de schijf te labelen. Er hoeft niets opnieuw te worden gelabeld en er is geen restorecon. Begin hier:
sudo aa-status
sudo journalctl -k | grep -i apparmorEen weigering verschijnt als apparmor="DENIED" operation="open" profile="/usr/sbin/nginx" name="/data/www/index.html" requested_mask="r". De workflow heeft dezelfde structuur: lees de weigering, zoek het profiel en pas de regel aan. sudo apt install apparmor-utils biedt u aa-complain (permissive voor één profiel) en aa-enforce om dit terug te draaien. Ubuntu beperkt een geselecteerde set verpakte services en laat de rest onbeperkt; lees daarom aa-status om te zien wat er daadwerkelijk actief is in plaats van aannames te doen.
Eén gewoonte is op beide systemen van toepassing. Wanneer een service Permission denied rapporteert over iets dat correct lijkt, lees dan het beveiligingslogboek voordat u de rechten wijzigt. De bits zijn zelden twee keer het probleem.
FAQ
Waarom geeft Nginx een 403-foutmelding terwijl de bestandsrechten correct zijn?
Omdat SELinux de leestoegang blokkeert, niet de toegangsrechten. De webserver draait in het httpd_t-domein en mag alleen bestanden lezen die zijn gelabeld voor webinhoud. Een bestand met het label default_t of admin_home_t wordt geweigerd, waardoor Nginx niets kan serveren. Controleer dit met sudo ausearch -m AVC -ts recent; dit toont scontext eindigend op httpd_t, waarbij tcontext het onjuiste type bevat. Noteer vervolgens het juiste label en pas dit toe: sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?" gevolgd door sudo restorecon -Rv /data/www.
Is het veilig om setenforce 0 uit te voeren om een service werkend te krijgen?
setenforce 0 is een diagnostische stap, geen oplossing. Gebruik het om het probleem eenmalig te reproduceren zodat het logbestand alle weigeringen in één keer verzamelt, lees deze met sudo ausearch -m AVC -ts recent, voer daarna sudo setenforce 1 uit en herstel de oorzaken. Een server in de permissive-modus logt elke weigering maar blokkeert niets; u behoudt dus de ruis en verliest de beveiliging. Als één service ruimte nodig heeft terwijl u werkt, voer dan sudo semanage permissive -a httpd_t uit zodat de rest van de machine in de enforcing-modus blijft.
Hoe draai ik een service op een niet-standaard poort met ingeschakelde SELinux?
Voeg de poort toe aan het type waaraan de service mag binden. Voor een webserver op 8081: sudo semanage port -a -t http_port_t -p tcp 8081. Voor SSH op 2222: sudo semanage port -a -t ssh_port_t -p tcp 2222. Controleer eerst de huidige lijst met sudo semanage port -l | grep -w http_port_t, omdat een poort die al in de lijst staat een foutmelding geeft met ValueError: Port tcp/8081 already defined. Zonder deze stap sluit de daemon bij het opstarten af met bind() ... Permission denied, ook al houdt geen enkel ander proces de poort bezet.
Heeft Ubuntu SELinux?
Nee. Ubuntu en Debian gebruiken AppArmor, dat een profiel afdwingt dat gekoppeld is aan het pad van een uitvoerbaar bestand in plaats van aan labels op bestanden. Controleer dit met sudo aa-status en zoek naar apparmor="DENIED"-regels in sudo journalctl -k. Ubuntu beperkt een geselecteerde set verpakte services, waardoor veel programma's standaard ongehinderd draaien. Rocky Linux en AlmaLinux zijn de distributies waar u SELinux standaard ingeschakeld tegenkomt, evenals Fedora en RHEL.