SSD Nodes Learn 🎉 VPS ab $5.50/Monat
Anleitungen Matt ConnorVon Matt Connor · Aktualisiert 2026-08-13

SELinux: nginx gibt trotz Rechten 403 zurück

nginx meldet 403 trotz korrekter Rechte? Lesen Sie die SELinux-Verweigerung, setzen Sie das Label mit semanage und restorecon und lassen Sie enforcing aktiv.

Warum nginx den Zugriff auf eine Datei mit korrekten Berechtigungen verweigert

Wenn nginx bei einer Datei mit korrekten Berechtigungsbits 403 zurückgibt, verweigert fast immer SELinux (Security-Enhanced Linux) den Lesezugriff. SELinux prüft nach den normalen Berechtigungen einen zweiten Regelsatz. Der Webserver darf nur Dateien lesen, die ein Label für Webinhalte tragen. Ihre Datei trägt ein anderes Label. Deshalb schlägt das Öffnen fehl, und nginx hat keine Daten, die es senden kann.

Prüfen Sie das Label und nicht nur den Modus:

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

Der Punkt nach drwxr-xr-x bedeutet, dass die Datei ein SELinux-Label trägt. default_t erhält ein Pfad, wenn die Policy ihn noch nicht kennt. Keine Regel für den Webserver erlaubt das Lesen dieses Typs. Das Fehlerprotokoll zeigt einen gewöhnlichen Unix-Fehler. Deshalb sieht das Problem wie ein Berechtigungsfehler aus:

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"

Der Kernel gibt 13: Permission denied für beide Arten der Verweigerung zurück: für die gewöhnliche und für die durch SELinux. Ermitteln Sie daher zuerst, welche Ebene den Zugriff verweigert hat. Beginnen Sie nicht mit setenforce 0.

Der Teil des Modells, den Sie benötigen

SELinux ist eine verpflichtende Zugriffskontrolle, meist als MAC bezeichnet. Jeder Prozess läuft in einer Domain, zum Beispiel httpd_t für den Webserver. Jede Datei und jeder Netzwerkport hat einen Typ, zum Beispiel httpd_sys_content_t. Die Policy ist eine Liste zulässiger Kombinationen aus Domain, Typ und Aktion. Alles, was nicht in dieser Liste steht, wird verweigert. Sie wird nach der klassischen Unix-Prüfung angewendet. Daher müssen die Berechtigungsbits in drwxr-xr-x den Zugriff zuerst ebenfalls erlauben. Beide Ebenen müssen den Zugriff zulassen.

Ein vollständiger Kontext hat vier durch Doppelpunkte getrennte Felder, zum Beispiel system_u:system_r:httpd_t:s0: den SELinux-Benutzer, die Rolle, den Typ und das Level. Auf einem Server arbeiten Sie fast ausschließlich mit dem dritten Feld, dem Typ. Zwei Befehle zeigen die aktuell verwendeten Werte:

ps -eZ | grep nginx
id -Z

Die nginx-Worker zeigen einen Kontext, der mit httpd_t endet. Ihre Login-Shell zeigt unconfined_u:unconfined_r:unconfined_t:s0, weil die standardmäßige targeted-Policy Dienste einschränkt und interaktive Benutzer nicht beschränkt. Das ist wichtig, weil SELinux den Betrieb von Diensten mit Benutzern nach dem Prinzip der geringsten Berechtigungen nicht ersetzt. SELinux begrenzt, auf welche Ressourcen ein Dienst zugreifen kann, nachdem jemand in ihn eingebrochen ist.

Die drei Modi und welche Images SELinux verwenden

sestatus
getenforce

Enforcing blockiert Zugriffe und protokolliert sie. Permissive lässt alles zu und protokolliert, was blockiert worden wäre. Disabled lädt überhaupt keine Richtlinie. getenforce gibt den aktuellen Modus aus. sestatus gibt zusätzlich den Modus aus /etc/selinux/config aus. Das ist der Modus, der nach einem Reboot wieder aktiviert wird.

Rocky Linux, AlmaLinux, Fedora und RHEL werden mit aktiviertem SELinux und der Richtlinie targeted ausgeliefert. Ubuntu und Debian verwenden stattdessen AppArmor. AppArmor erfüllt dieselbe Aufgabe mit einem anderen Mechanismus (der letzte Abschnitt behandelt ihn). Daher kann sich dieselbe Anwendung auf einem Ihrer Server problemlos installieren lassen und auf einem anderen den Status 403 zurückgeben.

Installieren Sie die Werkzeuge, bevor Sie sie benötigen

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

semanage: command not found auf einem Minimal-Image bedeutet, dass policycoreutils-python-utils fehlt: Dieses Paket enthält semanage und audit2allow. setroubleshoot-server ergänzt sealert und schreibt eine verständliche Zusammenfassung jeder verweigerten Aktion in das Journal. Installieren Sie beide Pakete auf einem neuen Server, denn sobald Sie sie benötigen, ist bereits etwas gestört.

So lesen Sie eine SELinux-Verweigerung im Audit-Log

Jede Verweigerung wird vom Audit-Daemon als AVC-Nachricht (Access Vector Cache) aufgezeichnet:

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 Felder enthalten die vollständigen Informationen. comm ist das blockierte Programm. scontext ist der Quellkontext, also die Domain, in der der Prozess ausgeführt wurde. tcontext ist der Zielkontext, also das Label des Objekts, auf das der Prozess zugreifen wollte. tclass bezeichnet den Objekttyp, hier eine Datei. Zusammengenommen bedeutet das: Der Prozess in httpd_t versuchte, eine mit default_t gelabelte Datei zu lesen, und permissive=0 zeigt, dass die Anfrage tatsächlich blockiert und nicht nur protokolliert wurde.

Wenn ausearch keine Ausgabe liefert, läuft der Audit-Daemon möglicherweise nicht. Die Verweigerungen landen dann stattdessen im Kernel-Ringpuffer:

sudo journalctl -k | grep -i avc

Formulieren Sie den Eintrag nun als deutschen Satz:

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

audit2why liest dieselben Einträge und nennt die erkannte Ursache: ein deaktiviertes Boolean, ein nicht zur Policy passendes Label oder eine vollständig fehlende Regel. sealert durchsucht das gesamte Log und gibt pro Verweigerung einen vorgeschlagenen Befehl aus. Betrachten Sie den Vorschlag als Hinweis. Die Formulierung ändert sich zwischen Releases, und sealert schlägt manchmal ein benutzerdefiniertes Policy-Modul vor, obwohl eine einzeilige Korrektur des Labels die richtige Lösung wäre.

Beachten Sie außerdem Folgendes. Die Policy enthält dontaudit-Regeln, die als harmlos eingestufte Verweigerungen ausblenden. Deshalb kann sich ein Programm fehlerhaft verhalten, während das Log leer bleibt. Blenden Sie diese Verweigerungen für die Dauer eines Tests ein:

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

Mit semanage fcontext und restorecon einen falsch gekennzeichneten Pfad korrigieren

Zwei Befehle, und die Reihenfolge ist wichtig. semanage fcontext -a speichert, welche Kennzeichnung ein Pfad haben sollte. restorecon wendet diesen gespeicherten Standard auf die Dateien auf dem Datenträger an.

sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"
sudo restorecon -Rv /data/www
ls -ldZ /data/www/index.html

Der Pfad ist ein regulärer Ausdruck. (/.*)? erfasst das Verzeichnis selbst und alles darunter. Das ist für ein Document Root erforderlich. Prüfen Sie vor der Änderung, was sich ändern würde: sudo restorecon -Rvn /data/www gibt die geplanten Neukennzeichnungen aus, weil -n keine Aktion bedeutet. Nach einem tatsächlichen restorecon lautet die Kennzeichnung httpd_sys_content_t, und der 403-Fehler ist ohne Neustart des Dienstes behoben.

Verwenden Sie chcon nur zum Testen. chcon -t httpd_sys_content_t index.html setzt die Kennzeichnung direkt. Der nächste restorecon, ein Paketupdate oder eine vollständige Neukennzeichnung setzt sie zurück, weil die Policy weiterhin vorgibt, dass der Pfad eine andere Kennzeichnung haben sollte. semanage fcontext ist die dauerhafte Variante. Mit sudo semanage fcontext -l | grep '^/data' listen Sie die gespeicherten Zuordnungen auf.

Für Inhalte, in die der Dienst schreiben muss, ist ein anderer Typ erforderlich. Verwenden Sie httpd_sys_rw_content_t für ein Upload-Verzeichnis oder einen Cache, und beschränken Sie das auf diese Pfade: Eine schreibgeschützte Website unter einem beschreibbaren Typ gewährt mehr Zugriff, als die Anwendung benötigt.

Warum war die Kennzeichnung überhaupt falsch? Fast immer liegt es daran, wie die Dateien dorthin gelangt sind. mv behält die vorhandene Kennzeichnung einer Datei bei. Eine aus /root verschobene Website kommt daher mit der Kennzeichnung admin_home_t an und behält sie bei. Ein einfaches cp weist der neuen Datei die Standardkennzeichnung des Zielverzeichnisses zu. Das ist normalerweise das gewünschte Verhalten. cp -a und rsync -X kopieren dagegen die Kennzeichnungen der Quelldateien zusammen mit der Datei. Ein git clone in ein neues Verzeichnis der obersten Ebene erzeugt default_t. Wenn eine Seite aus /usr/share/nginx/html problemlos geladen wird, aber aus Ihrem eigenen Verzeichnis fehlschlägt, ist das der Grund.

Eine Verhaltensklasse mit einem Boolean beheben

Nicht jeder Fehler ist ein Label-Problem. Ein Reverse Proxy auf einem frisch installierten Rocky- oder AlmaLinux-System gibt 502 zurück. Im Fehlerprotokoll steht:

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

Ihr Upstream funktioniert. Die httpd_t-Domain darf standardmäßig keine ausgehenden Netzwerkverbindungen öffnen. Daher wird der connect()-Aufruf abgewiesen, bevor er die Loopback-Schnittstelle erreicht. Ein Schalter steuert dieses gesamte Verhalten:

getsebool -a | grep httpd_can_network
sudo setsebool -P httpd_can_network_connect on

-P ist die relevante Option. Sie schreibt den Wert auf die Festplatte. Ohne -P geht die Änderung beim nächsten Reboot verloren. Der Dienst funktioniert dann nur, bis der Rechner neu gestartet wird. Prüfen Sie die Einstellung mit semanage boolean -l | grep httpd_can_network_connect. Der Befehl gibt den aktuell laufenden Wert neben dem gespeicherten Wert aus.

Verwenden Sie einen Boolean statt einer selbst geschriebenen Regel, wenn ein solcher Boolean vorhanden ist. Booleans sind Bestandteil der Distributionsrichtlinie. Sie werden daher gepflegt, dokumentiert und sind für die nächste zuständige Person leicht auffindbar. getsebool -a listet alle Booleans auf dem System auf.

Einen Dienst auf einem nicht standardmäßigen Port lauschen lassen

Ports sind ebenfalls gekennzeichnet. Verschieben Sie nginx auf 8081. Danach verweigert der Dienst den Start:

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

httpd_t darf Ports mit der Kennzeichnung http_port_t binden. 8081 gehört nicht dazu. Fügen Sie die Kennzeichnung hinzu:

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

Prüfen Sie die Liste zuerst. Mehrere Ports im hohen Portbereich sind bereits erlaubt, darunter 8008 und 8443. Eine doppelte Ergänzung schlägt mit ValueError: Port tcp/8081 already defined fehl. Gehört der Port bereits zu einem anderen Typ, ändern Sie die Zuordnung mit semanage port -m -t http_port_t -p tcp 8081, statt sie hinzuzufügen.

Mit demselben Befehl bringen Sie auch einen verschobenen SSH-Port zum Laufen. Bind to port 2222 on 0.0.0.0 failed: Permission denied in journalctl -u sshd bedeutet, dass 2222 in ssh_port_t fehlt. Führen Sie daher sudo semanage port -a -t ssh_port_t -p tcp 2222 aus, bevor Sie den Daemon neu starten und Ihre Sitzung schließen. Diesen Schritt überspringen viele, wenn sie einer allgemeinen Anleitung zum Härten von SSH auf einem VPS auf einem Red-Hat-Familien-Image folgen. SELinux ist außerdem keine Firewall. Der Port muss daher weiterhin geöffnet sein: hier mit sudo firewall-cmd --permanent --add-port=8081/tcp && sudo firewall-cmd --reload oder mit ufw auf einem Debian- oder Ubuntu-Image.

Wenn es weder einen Boolean noch ein Label zum Ändern gibt

Das ist auf einem normalen Server selten der Fall. Genau hier verursachen Administratoren häufig Schäden. audit2allow kann aus den Verweigerungen im Log ein Policy-Modul erstellen:

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

Lesen Sie nginx_local.te, bevor Sie das Modul installieren. Zwei Vorgehensweisen sorgen für Sicherheit. Filtern Sie die Eingabe mit -c auf das eine Programm, das Sie korrigieren, weil eine Woche nicht zusammengehöriger Verweigerungen per Pipe an audit2allow sämtliche Regeln auf einmal gewährt. Installieren Sie niemals ein Modul, das aus einer Verweigerung erstellt wurde, die Sie nicht erklären können: Eine Regel, mit der httpd_t jede Datei auf dem Server lesen darf, lässt sich leicht erzeugen und wird Monate später nur schwer erkannt. Entfernen Sie ein Modul mit sudo semodule -r nginx_local.

Permissive ist ein Diagnosemodus, keine Fehlerbehebung

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

Der Permissive-Modus erlaubt den Zugriff und protokolliert ihn. Sein eigentlicher Wert liegt in der Vollständigkeit. Im Enforcing-Modus beendet der Dienst den Vorgang bei der ersten Ablehnung. Sie beheben diese Ablehnung, starten den Dienst neu und stoßen dann auf die zweite. Im Permissive-Modus läuft der Vorgang weiter. Das Log erfasst alle Ablehnungen in einem einzigen Durchlauf. Anschließend wechseln Sie zurück und beheben sie gemeinsam.

setenforce ändert /etc/selinux/config nicht. Nach einem Reboot befindet sich das System daher wieder im Enforcing-Modus. Das ist ein Sicherheitsnetz. Deshalb tritt auch ein vermeintlicher „Fix“, der lediglich aus setenforce 0 bestand, im ungünstigsten Moment wieder auf. Wenn ein Dienst während der Fehlersuche mehr Spielraum benötigt, markieren Sie statt des gesamten Systems nur diese Domain: sudo semanage permissive -a httpd_t lässt alle anderen Domains im Enforcing-Modus, und sudo semanage permissive -d httpd_t macht die Änderung rückgängig.

Warum das Deaktivieren von SELinux mehr kostet als die Korrektur des Labels

SELINUX=disabled in /etc/selinux/config zu setzen, tauscht eine Korrektur in einer Zeile gegen einen dauerhaft weniger geschützten Server ein. Der Unterschied zeigt sich an dem Tag, an dem eine Webanwendung kompromittiert wird. Im Enforcing-Modus läuft der Code des Angreifers in httpd_t. Er kann dadurch Webinhalte lesen. Das Lesen von /etc/shadow oder das Schreiben einer systemd-Unit wird von der Policy jedoch verweigert, unabhängig davon, was der Unix-Benutzer erlauben würde. Ohne geladene Policy erhält derselbe Code alle Berechtigungen des Dienstkontos.

Auch das Deaktivieren verursacht späteren Aufwand. Solange keine Policy geladen ist, werden neue Dateien ohne Label erstellt. Dadurch weicht das Dateisystem zunehmend von der Policy ab. Wenn SELinux wieder aktiviert wird, ist daher eine vollständige Neulabelung erforderlich. Andernfalls schlagen mehrere Dienste gleichzeitig fehl:

sudo fixfiles -F onboot
sudo reboot

Dieser Befehl schreibt /.autorelabel und versieht beim nächsten Boot jedes Dateisystem erneut mit Labels. Auf einer großen Festplatte dauert das lange. Die Konsole wirkt dabei möglicherweise eingefroren. Starten Sie den Vorgang daher nur, wenn Sie warten können. Unter Rocky Linux und AlmaLinux 9 schaltet die Konfigurationsdatei den Kernel-Teil nicht mehr automatisch ab. Die dokumentierte Methode, SELinux vollständig zu deaktivieren, ist ein Kernel-Argument (sudo grubby --update-kernel ALL --args selinux=0). Dieser Befehl ist hilfreich, wenn Sie den Server einer anderen Person übernehmen. Er behebt jedoch keinen 403-Fehler.

Container erhalten ein zusätzliches Label

Auf einem Host der Red-Hat-Familie laufen Containerprozesse in container_t und dürfen nur Dateien mit dem Label container_file_t lesen. Ein Bind-Mount vom Host schlägt im Container mit Permission denied fehl, obwohl ls -l auf dem Host völlig normal aussieht. Das Suffix :Z weist die Laufzeitumgebung an, den Mount neu zu labeln:

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

:Z versieht das Verzeichnis nur für diesen Container mit dem passenden Label. :z versieht es mit einem Label für die gemeinsame Nutzung durch mehrere Container. Wenn Sie :Z auf ein Verzeichnis setzen, das andere Dienste verwenden, wird dieses Verzeichnis rekursiv neu gelabelt. Dadurch fallen diese Dienste aus. Verwenden Sie deshalb eigene Pfade für Container. Alles andere an der Einrichtung entspricht der Einrichtung mit jedem anderen Image, wie unter Docker auf einem VPS ausführen beschrieben.

Ubuntu und Debian liefern AppArmor

Gleiche Aufgabe, anderes Design. AppArmor beschränkt ein Programm anhand des Pfads zu seiner ausführbaren Datei. Dafür verwendet es ein Profil unter /etc/apparmor.d/, anstatt Dateien auf dem Datenträger zu kennzeichnen. Es gibt nichts neu zu kennzeichnen und kein restorecon. Beginnen Sie hier:

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

Eine Verweigerung erscheint als apparmor="DENIED" operation="open" profile="/usr/sbin/nginx" name="/data/www/index.html" requested_mask="r". Der Ablauf ist derselbe: Lesen Sie die Verweigerung, ermitteln Sie das Profil und ändern Sie die Regel. sudo apt install apparmor-utils aktiviert aa-complain (permissive für ein Profil), und mit aa-enforce setzen Sie den Zustand zurück. Ubuntu beschränkt eine ausgewählte Gruppe paketierter Dienste und lässt die übrigen Dienste unbeschränkt. Lesen Sie daher aa-status, um zu sehen, was tatsächlich aktiv ist, statt dies anzunehmen.

Eine Vorgehensweise gilt für beide Systeme. Wenn ein Dienst für etwas, das korrekt aussieht, Permission denied meldet, lesen Sie das Security-Log, bevor Sie die Berechtigungen ändern. Die Berechtigungsbits sind nur selten zweimal das Problem.

FAQ

Warum liefert nginx den Status 403, obwohl die Dateiberechtigungen korrekt sind?

Weil SELinux den Lesezugriff verweigert hat, nicht die Berechtigungsbits. Der Webserver läuft in der Domäne httpd_t und darf nur Dateien lesen, die für Webinhalte gekennzeichnet sind. Eine Datei mit der Kennzeichnung default_t oder admin_home_t wird daher abgewiesen, und nginx hat keinen Inhalt auszuliefern. Prüfen Sie dies mit sudo ausearch -m AVC -ts recent. Der Befehl zeigt scontext mit dem Ende httpd_t und tcontext mit dem falschen Typ. Übernehmen Sie anschließend die korrekte Kennzeichnung und wenden Sie sie an: sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?" gefolgt von sudo restorecon -Rv /data/www.

Ist es sicher, setenforce 0 auszuführen, damit ein Dienst funktioniert?

setenforce 0 ist ein Diagnoseschritt, keine Lösung. Verwenden Sie den Befehl einmal, um das Problem zu reproduzieren. Dadurch protokolliert das Log alle Verweigerungen in einem Durchlauf. Lesen Sie sie mit sudo ausearch -m AVC -ts recent. Führen Sie anschließend sudo setenforce 1 aus und beheben Sie die Ursachen. Ein Server im permissive-Modus protokolliert jede Verweigerung, blockiert aber keine. Dadurch bleibt die Fehlermeldung bestehen, während der Schutz entfällt. Wenn ein einzelner Dienst während der Arbeiten mehr Spielraum benötigt, führen Sie sudo semanage permissive -a httpd_t aus. Der restliche Rechner bleibt dann im enforcing-Modus.

Wie betreibe ich einen Dienst mit aktiviertem SELinux auf einem nicht standardmäßigen Port?

Fügen Sie den Port dem Typ hinzu, an den der Dienst binden darf. Für einen Webserver auf 8081: sudo semanage port -a -t http_port_t -p tcp 8081. Für SSH auf 2222: sudo semanage port -a -t ssh_port_t -p tcp 2222. Prüfen Sie die aktuelle Liste zuerst mit sudo semanage port -l | grep -w http_port_t. Wenn ein Port bereits aufgeführt ist, schlägt der Befehl mit ValueError: Port tcp/8081 already defined fehl. Ohne diesen Schritt beendet sich der Daemon beim Start mit bind() ... Permission denied, obwohl kein anderer Prozess den Port verwendet.

Gibt es SELinux auf Ubuntu?

Nein. Ubuntu und Debian liefern AppArmor aus. AppArmor erzwingt ein Profil, das an den Pfad einer ausführbaren Datei gebunden ist, statt an Kennzeichnungen von Dateien. Prüfen Sie den Status mit sudo aa-status und suchen Sie in sudo journalctl -k nach Zeilen mit apparmor="DENIED". Ubuntu schränkt eine ausgewählte Gruppe paketierter Dienste ein. Viele Programme laufen daher standardmäßig ohne Einschränkung. Auf Rocky Linux und AlmaLinux ist SELinux standardmäßig im enforcing-Modus aktiviert. Das gilt auch für Fedora und RHEL.