Webentwicklung

PHP-FPM-Unix-Socket-Berechtigungen - Nginx gezielt Zugriff auf PHP geben

PHP-FPM-Unix-Socket-Berechtigungen - Nginx gezielt Zugriff auf PHP geben

Nginx liefert eine 502-Antwort, und im Fehlerprotokoll wird ein von PHP-FPM verwendeter Unix-Socket erwähnt. Es liegt nahe, diesen Socket für alle Benutzer beschreibbar zu machen und weiterzumachen. Doch 502 ist nur das sichtbare Symptom an der Schnittstelle. Der Socket könnte fehlen, Nginx und PHP-FPM könnten unterschiedliche Pfade verwenden, ein übergeordnetes Verzeichnis könnte die Pfadauflösung verhindern oder dem Nginx-Worker könnte die Berechtigung zum Verbinden fehlen.

Eine sinnvolle Korrektur beginnt damit, den tatsächlich vorliegenden Zustand zu bestimmen. Dieser Artikel entwickelt eine Diagnose für Nginx und PHP-FPM auf demselben Linux-Host. Anschließend wird die Korrektur in der PHP-FPM-Pool-Konfiguration verankert, damit sie die Neuerstellung des Sockets übersteht, statt von einem einmaligen chmod abzuhängen.

Die Namen app, www-data und /run/php/app.sock sind im Folgenden Platzhalter. Aktive Benutzer, Gruppen, Dienstnamen, Binärdateien und Pfade unterscheiden sich je nach System und Paket; sie müssen vor jeder Änderung geprüft werden.

An der Schnittstelle beginnen, nicht bei der Browsermeldung

In dieser Anordnung ist PHP-FPM der Listener und Nginx der Client. Der PHP-FPM-Pool erstellt mit seiner listen-Direktive einen lokalen Endpunkt. Nginx sendet mit fastcgi_pass eine Anfrage an diesen Endpunkt. Das PHP-FPM-Konfigurationshandbuch akzeptiert für listen entweder eine IP-Adresse mit Port oder einen Unix-Socket-Pfad. Das FastCGI-Modul von Nginx akzeptiert ebenfalls einen Unix-Domain-Socket:

location ~ \.php$ {
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass unix:/run/php/app.sock;
}

Beide Seiten müssen denselben Endpunkt beschreiben. Ähnlich aussehende Namen sind dennoch verschiedene Pfade. Ein konfigurierter Pfad /run/php/app.sock erreicht nicht /run/php/app-fpm.sock. Ein laufender PHP-FPM-Dienst beweist außerdem nicht, dass gerade der von Nginx erwartete Pool oder Socket verfügbar ist.

Der erste Beleg sollte daher das Nginx-Fehlerprotokoll rund um eine kontrollierte Anfrage sein. Die genaue Operation, der Pfad und der Betriebssystemfehler sollten erhalten bleiben. „No such file or directory“, „connection refused“ und „permission denied“ führen zu unterschiedlichen Untersuchungen. Sie sollten nicht pauschal zu „PHP ist ausgefallen“ zusammengefasst werden.

Vor Änderungen an Berechtigungen die Existenz prüfen

Bevor Eigentümer oder Modus geändert werden, ist zu prüfen, ob der erwartete Socket vorhanden ist und ein Prozess lauscht. Diese Befehle untersuchen den Zustand, ohne ihn zu verändern:

systemctl list-units --type=service --all 'nginx.service' 'php*-fpm.service'
ss -xl
stat -Lc 'type=%F owner=%U group=%G mode=%a path=%n' /run/php/app.sock

Der Platzhalter hilft dabei, eine versionierte PHP-FPM-Unit zu finden, die Benennung hängt jedoch weiterhin vom Paket ab. Ein fehlgeschlagenes stat ist keine Berechtigungsdiagnose. Zunächst stellt sich die Frage, ob der Pfad falsch ist, der Pool nicht gestartet werden konnte, sein übergeordnetes Verzeichnis nicht verfügbar war oder eine andere Service-Unit den Listener verwaltet.

Zu vergleichen sind die aktive Nginx-Konfiguration und die aktive Pool-Konfiguration, nicht ein Tutorial oder eine ungenutzte Beispieldatei:

; PHP-FPM pool
listen = /run/php/app.sock
# Nginx PHP location
fastcgi_pass unix:/run/php/app.sock;

Nginx kann seine geladene Konfiguration mit nginx -T ausgeben. Diese Ausgabe kann jedoch andere vertrauliche Einstellungen enthalten und sollte nicht ungeprüft in ein öffentliches Ticket kopiert werden. Befehle zum Testen der PHP-FPM-Konfiguration und die Namen der Binärdateien variieren je nach Paket. Statt einen versionierten Befehl zu erraten, sollten die installierte Binärdatei und die Dokumentation des Pakets verwendet werden.

Die Prozesse mit notwendigem Zugriff bestimmen

Effektive Prozessidentitäten sind aussagekräftiger als Konfigurationsbezeichnungen. In einer typischen Master-Worker-Anordnung startet ein Nginx-Master möglicherweise mit erhöhten Rechten, während die anfrageverarbeitenden Worker unter einem unprivilegierten Konto laufen. Der Worker muss die Verbindung herstellen. Auch PHP-FPM trennt die Identität der Pool-Worker von den Metadaten seines Listening-Sockets.

Eine umfassende Prozessliste kann diese Identitäten zeigen, ohne ihre Namen vorwegzunehmen:

ps -eo user,group,pid,ppid,comm,args

Die relevanten Nginx- und PHP-FPM-Zeilen sollten lokal gefiltert und gelesen werden. Zusätzlich sind die Nginx-Direktive user sowie die Pool-Direktiven user, group und listen.* zu prüfen. Der Prozessbenutzer des Pools beantwortet die Frage: „Welches Konto führt PHP-Code aus?“ Eigentümer, Gruppe und Modus des Sockets beantworten: „Welche lokalen Prozesse dürfen diesen Listener erreichen?“ Die Fragen hängen zusammen, sind aber nicht identisch.

Der gesamte Pfad muss durchsuchbar sein

Ein Pathname-Socket unterliegt den Berechtigungsprüfungen des Dateisystems. Laut dem Linux-Handbuch unix(7) benötigt das Erstellen eines Pathname-Sockets Schreib- und Suchberechtigung für sein Verzeichnis. Unter Linux erfordert die Verbindung zu einem Stream-Socket Schreibberechtigung für den Socket selbst. Der Client muss außerdem den dorthin führenden Pfad durchlaufen können.

Dieser letzte Punkt erklärt, warum ein Socket korrekt zugeordnet aussehen kann und die Verbindung dennoch fehlschlägt. Jede Komponente ist zu prüfen, nicht nur die letzte Zeile:

namei -l /run/php/app.sock
stat -Lc 'owner=%U group=%G mode=%a path=%n' /run /run/php /run/php/app.sock

namei ist möglicherweise nicht auf jedem System installiert. Die wiederholte Verwendung von stat ist für diesen kurzen Pfad eine hinreichend portable Alternative. Die Verzeichnisse entlang des Pfades benötigen Suchberechtigung für den Nginx-Worker. Einem übergeordneten Verzeichnis nur für die Pfadauflösung Schreibberechtigung zu geben, ist unnötig und erweitert den Zugriff über das erforderliche Maß hinaus.

Die dauerhafte Berechtigung im Pool festlegen

PHP-FPM stellt für einen Linux-Unix-Socket drei Direktiven bereit: listen.owner, listen.group und listen.mode. Das Handbuch dokumentiert den Standardmodus 0660; Eigentümer und Gruppe richten sich ansonsten nach dem ausführenden Benutzer. Paketkonfigurationen können abweichende explizite Werte setzen, daher bleibt die aktive Datei maßgeblich.

Angenommen, die Prüfung zeigt, dass der Anwendungspool als app und die Nginx-Worker mit der Gruppe www-data laufen. Eine eng begrenzte Pool-Konfiguration könnte so aussehen:

[app]
user = app
group = app

listen = /run/php/app.sock
listen.owner = app
listen.group = www-data
listen.mode = 0660

Dieses Beispiel gibt Eigentümer und Gruppe Lese- und Schreibrechte, anderen lokalen Konten jedoch keine Socket-Rechte. Unter Linux erlaubt das Schreibbit der Gruppe einem Worker in www-data, eine Verbindung herzustellen. Der genaue Eigentümer und die genaue Gruppe müssen den auf dem Zielhost ermittelten Identitäten folgen. Eine eigene FastCGI-Zugriffsgruppe kann enger sein, wenn mehrere Webserver oder Pools nebeneinander existieren. Ihre Einführung erfordert jedoch auch die Verwaltung ergänzender Gruppenmitgliedschaften und einen Neustart der betroffenen Prozesse, damit sie die neue Mitgliedschaft erhalten.

Die vorgelagerte PHP-FPM-Pool-Vorlage dokumentiert dieselbe Trennung zwischen der Identität der Pool-Worker und den Socket-Metadaten. Sie dokumentiert außerdem listen.acl_users und listen.acl_groups für Systeme mit POSIX-ACL-Unterstützung. Wenn diese ACL-Optionen gesetzt sind, werden listen.owner und listen.group ignoriert. ACLs können Zugriff ohne gemeinsame Gruppe ausdrücken. Werden jedoch beide Mechanismen vermischt, ohne diese Priorität zu beachten, wird die Diagnose schwieriger.

Warum ein schnelles chmod keine Lösung ist

Ein Befehl, der den aktiven Socket ändert, kann als vorübergehendes Experiment dienen: Funktioniert der Zugriff nach einer sorgfältig gewählten Metadatenänderung, ist das ein Hinweis auf die Fehlerursache. Eine dauerhafte Konfiguration ist es noch nicht. PHP-FPM erstellt den Pathname-Socket, wenn der Pool seinen Listener bindet. Wird dieser Endpunkt während des Dienstlebenszyklus entfernt und neu erstellt, können manuell gesetzte Metadaten mit dem alten Dateisystemobjekt verschwinden.

Die dauerhafte Quelle der Konfiguration ist daher die Pool-Konfiguration zusammen mit dem Mechanismus, den Betriebssystem oder Paket zum Erstellen und Schützen des übergeordneten Verzeichnisses verwenden. Letzteres variiert: Es sollte nicht angenommen werden, dass ein manuell unter /run erstelltes Verzeichnis einen Neustart übersteht.

Der Modus 0666 ist als Standardantwort besonders schwach. Er gewährt jedem lokalen Konto Schreibzugriff auf den Endpunkt, obwohl nur eine Webserver-Identität als Client bekannt ist. Damit kann der Fehler verschwinden, während die durch Socket-Berechtigungen beabsichtigte Grenze aufgegeben wird. Umgekehrt funktioniert 0600 nur, wenn der verbindende Worker als Eigentümer gilt. „Restriktiver“ ist nicht hilfreich, wenn dadurch der beabsichtigte Client ausgeschlossen wird.

Schrittweise validieren, anwenden und testen

Nach der Bearbeitung ist die Konfiguration jedes Daemons mit dem vom installierten Paket bereitgestellten Befehl zu validieren. Beide Dienste sollten nicht blind neu gestartet werden: Schlägt ein Test fehl, bleibt der funktionierende Dienst bestehen, während zunächst die Syntax korrigiert wird.

Nach erfolgreicher Validierung wird die PHP-FPM-Änderung angewendet, damit der Socket neu erstellt wird. Nginx wird nur neu geladen oder gestartet, wenn sich seine Konfiguration geändert hat. Anschließend folgt dieselbe Prüfung erneut:

stat -Lc 'type=%F owner=%U group=%G mode=%a path=%n' /run/php/app.sock
namei -l /run/php/app.sock

Zum Abschluss wird eine harmlose PHP-Route über Nginx aufgerufen und sowohl das HTTP-Ergebnis als auch die aktuellen Protokolle werden geprüft. Dieser End-to-End-Test ist wichtig, weil plausible Metadaten nicht beweisen, dass Nginx den erwarteten Server-Block, PHP-FPM den erwarteten Pool oder eine Mandatory-Access-Control-Richtlinie die Verbindung zugelassen hat.

Eine kompakte Reihenfolge für die Fehlersuche lautet:

  1. Den genauen Nginx-Fehler und Socket-Pfad aus einer Anfrage erfassen.
  2. Bestätigen, dass der Listener existiert und der PHP-FPM-Pool läuft.
  3. Bestätigen, dass Nginx und PHP-FPM denselben Endpunkt nennen.
  4. Effektiven Benutzer und Gruppen des Nginx-Workers bestimmen.
  5. Den Socket und jedes übergeordnete Verzeichnis prüfen.
  6. Die listen.*-Einstellungen des Pools ändern, nicht nur den aktiven Socket.
  7. Konfiguration validieren, anwenden, Metadaten erneut prüfen und über Nginx testen.

Die Grenzen dieser Grundlage kennen

Diskretionäre Dateisystemberechtigungen sind nur eine Ebene. SELinux, AppArmor, eine systemd-Sandbox, ein chroot oder Containergrenzen können eine Verbindung verweigern, obwohl die numerischen Modusbits ausreichend erscheinen. Dieser Artikel empfiehlt nicht, solche Kontrollen zu deaktivieren. Sind Pfad und Identitäten korrekt, der Zugriff wird aber weiterhin verweigert, sollten die relevanten Richtlinienprotokolle und die Dienstbeschränkungen geprüft werden, statt den Socket-Modus wiederholt zu erweitern.

Das Linux-Verhalten ist außerdem keine allgemeine Unix-Regel. Sowohl die PHP-Dokumentation als auch unix(7) weisen darauf hin, dass einige von BSD abgeleitete Systeme die Berechtigungen von Pathname-Sockets anders behandeln. Getrennte PHP-FPM-Pools können schließlich die betriebliche Trennung verbessern, doch das PHP-Handbuch warnt ausdrücklich davor, Pools als vollständigen Sicherheitsmechanismus zu betrachten; Ressourcen wie eine einzelne OPcache-Instanz können weiterhin gemeinsam genutzt werden.

Den Vertrag zwischen den Prozessen korrigieren

Ein PHP-FPM-Socket ist ein kleiner Vertrag zwischen zwei Prozessen. PHP-FPM bestimmt, wo der Endpunkt existiert und welche Metadaten er erhält. Nginx muss denselben Endpunkt nennen und sich mit einer vom Pfad zugelassenen Identität verbinden. Auch das übergeordnete Verzeichnis und jede zusätzliche Beschränkungsebene wirken an dieser Entscheidung mit.

Dieses Modell ist nützlicher, als jede 502-Antwort als Aufforderung zu breiteren Berechtigungen zu behandeln. Zuerst wird festgestellt, ob der Endpunkt existiert, dann werden beide Konfigurationen, die tatsächlichen Identitäten und der vollständige Pfad geprüft. Anschließend wird der kleinste funktionierende Zugriff im Pool festgelegt. Das Ziel ist nicht nur, dass die Seite einmal lädt. Die beabsichtigte Verbindung soll verständlich sein und sich reproduzieren lassen, wenn der Socket erneut erstellt wird.

References