SSD Nodes Learn Hosting plans →
Gidsen Matt ConnorDoor Matt Connor · Bijgewerkt 2026-09-04

Nginx 403 foutmelding door SELinux oplossen

Krijgt u een 403-foutmelding in nginx terwijl de bestandsrechten correct lijken? Leer hoe u SELinux-denials analyseert en labels herstelt met semanage en restorecon.

Waarom nginx een 403-fout geeft voor een bestand met correcte rechten

Wanneer nginx een 403-fout geeft voor een bestand waarvan de permissiebits correct zijn, is dit bijna altijd het gevolg van SELinux (Security-Enhanced Linux) dat de leestoegang weigert. SELinux controleert een tweede set regels nadat de normale permissies zijn goedgekeurd. De webserver mag alleen bestanden lezen die een label voor webinhoud dragen. 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.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

De 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 er is geen regel in de webserverconfiguratie die het lezen van dat type toestaat. Het foutenlogboek toont een standaard Unix-fout, wat de reden is dat dit als een permissiefout 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-rechten als de SELinux-beperkingen. De eerste stap is daarom om te achterhalen welke laag de toegang weigert. Begin niet met setenforce 0.

Het onderdeel van het model dat u nodig heeft

SELinux is een vorm van mandatory access control, meestal 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 -Z

De 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
getenforce

Enforcing 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 vanuit /etc/selinux/config, dit is de modus die na een herstart wordt toegepast.

Rocky Linux, AlmaLinux, Fedora en RHEL worden geleverd met SELinux in de modus enforcing met het targeted-beleid. Deze gedeelde standaard is geen toeval, aangezien alle vier voortkomen uit dezelfde Red Hat-lijn die via CentOS liep voordat Rocky Linux en AlmaLinux verschenen. Welke van de twee u gebruikt, maakt voor de inhoud van deze pagina geen verschil, aangezien ze hetzelfde beleid en dezelfde tools gebruiken. Daarom hangt de keuze tussen Rocky Linux en AlmaLinux af van hun compatibiliteitsbeloften en ondersteuning voor oudere CPU's, in plaats van van de standaardbeveiligingsinstellingen. Ubuntu en Debian worden geleverd met AppArmor, dat dezelfde taak uitvoert met een ander mechanisme (het laatste gedeelte 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-server

semanage: 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 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

Vier 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 in de kernel ring buffer terecht:

sudo journalctl -k | grep -i avc

Vertaal 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.log

audit2why leest dezelfde records en benoemt de herkende oorzaak: een boolean die is uitgeschakeld, 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 beleidsmodule 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 onschadelijk worden beschouwd, waardoor een programma zich misdraagt 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 -B

Een 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 vastgelegde 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.html

Het pad is een reguliere expressie. (/.*)? omvat de map zelf en alles wat daaronder valt; dit is wat een document root vereist. 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, en de volgende restorecon, pakketupdate of volledige herlabeling zet dit weer terug, omdat het beleid nog steeds voorschrijft dat het pad een andere waarde moet hebben. Op een systeem waar dnf-automatic beveiligingsupdates op een timer toepast, vindt die reset volgens zijn eigen planning plaats in plaats van terwijl u voor de machine zit, waardoor de site uren na uw laatste handeling uitvalt. semanage fcontext is de versie die behouden blijft. Bekijk wat u heeft vastgelegd met sudo semanage fcontext -l | grep '^/data'.

Inhoud waarnaar de service 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 site 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, gelabeld blijft als admin_home_t. Een standaard cp geeft het nieuwe bestand het standaardlabel van de doelmap, wat meestal de gewenste situatie is, 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 klasse van gedrag corrigeren met een boolean

Sommige fouten zijn geen labelprobleem. Een reverse proxy op een nieuwe 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 upstream

Uw upstream functioneert naar behoren. De httpd_t-domeininstelling staat standaard geen uitgaande netwerkverbindingen toe, 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. Controleer 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 worden onderhouden, gedocumenteerd en eenvoudig vindbaar zijn voor de volgende beheerder. getsebool -a somt alle beschikbare booleans 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 alleen 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 8081

Controleer eerst de lijst. Verschillende hoge poorten zijn al toegestaan, waaronder 8008 en 8443. Het dubbel toevoegen van een poort resulteert in 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 de poort 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 zij 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. De vlag --permanent bevat dezelfde valkuil bij een reboot als -P bij een boolean, en het is de moeite waard om de zones die bepalen op welke interfaces een regel van toepassing is, eens door te nemen in de basis van firewalld voor een Rocky of AlmaLinux VPS.

Wanneer er geen boolean of label is om aan te passen

Dit komt zelden voor op een standaard server, en dit is waar gebruikers schade aanrichten. audit2allow kan een policy-module opbouwen 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.pp

Lees nginx_local.te voordat u deze installeert. Twee gewoontes houden dit veilig. Filter de invoer naar het specifieke programma dat u repareert 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 en 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 1

De 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 het proces door en verzamelt het logbestand elke weigering in één keer, waarna u terugschakelt en ze gezamenlijk verhelpt.

setenforce heeft geen invloed op /etc/selinux/config, dus een herstart brengt de server terug naar de enforcing-modus. Dit is een vangnet, en het is ook de reden waarom een "oplossing" die bestond uit setenforce 0 op het slechtst mogelijke moment weer opduikt. 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 duurder is dan het corrigeren van de label

Het instellen van SELINUX=disabled in /etc/selinux/config ruilt een labelcorrectie van één regel 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 er direct een hoop services:

sudo fixfiles -F onboot
sudo reboot

Dit 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 wanneer u kunt wachten. Aangezien de machine toch opnieuw opstart, is het de moeite waard om eerst te bekijken wat er nog meer in de wachtrij staat voor een herstart; dit is wat needs-restarting rapporteert nadat een dnf-update oude kernels en bibliotheken in het geheugen heeft achtergelaten. 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 gedeeld gebruik tussen containers. Wijs :Z naar een map die andere services gebruiken en deze labelt die map recursief opnieuw, wat die services onbruikbaar maakt; geef containers daarom hun eigen paden. Als de engine nog niet op de server staat, merk dan op dat het docker-commando op deze distributies vaak podman is die reageert op de naam, een detail dat de installatiestappen voor Rocky en AlmaLinux oplost voordat u hiermee te maken krijgt. Al het overige met betrekking tot de configuratie komt 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 gelabeld te worden en er is geen restorecon. Begin hier:

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

Een 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 wijzig de regel. 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 aanpast. De bits zijn zelden twee keer het probleem.

FAQ

Waarom geeft Nginx een 403-foutmelding terwijl de bestandsrechten correct zijn?

Dit komt doordat SELinux de leestoegang blokkeert, niet de toegangsrechten van het bestandssysteem. De webserver draait in het httpd_t-domein en mag alleen bestanden lezen die zijn gelabeld als webcontent. 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 uit met sudo ausearch -m AVC -ts recent, en voer daarna sudo setenforce 1 uit om de oorzaken te herstellen. Een server die in de permissive-modus blijft staan, 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 ValueError: Port tcp/8081 already defined geeft. Zonder deze stap stopt de daemon bij het opstarten 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 pakketservices, waardoor veel programma's standaard ongehinderd draaien. Rocky Linux en AlmaLinux zijn de distributies waar u standaard SELinux in de enforcing-modus tegenkomt, evenals Fedora en RHEL.