Skip to main content

NGINX Proxy Manager härten - SNI und Hostnamen aneinander binden

Einleitung

Bei einem ersten eigenen PenTest meines Reverse Proxys ist eine kleine, aber interessante Eigenheit aufgefallen: Im TLS-Handshake konnte ich einen Hostnamen angeben und im anschließenden HTTP-Request einen anderen. Nginx Proxy Manager hat die Anfrage trotzdem angenommen und anhand des HTTP-Host-Headers an den zweiten Proxy Host weitergeleitet.

Das ist erst einmal kein Fehler in Nginx. Der SNI-Name wird schon beim Aufbau der verschlüsselten Verbindung benötigt, damit Nginx das passende Zertifikat auswählen kann. Den HTTP-Host sieht Nginx dagegen erst innerhalb der danach entschlüsselten Anfrage. Von Haus aus verlangt Nginx nicht, dass beide Angaben identisch sind.

Solange alle Proxy Hosts exakt dieselben Zugriffsregeln haben, ist das häufig nur unschön. Spannend wird es aber, sobald ein Host später eine eigene IP-Freigabe, mTLS oder eine andere Sonderbehandlung bekommt. Dann könnte eine Anfrage über den Namen eines erlaubten Hosts aufgebaut und anschließend zu einem anderen Host gelenkt werden. Deshalb habe ich SNI und HTTP-Host vorsorglich fest aneinander gebunden.

Was nach der Änderung passieren soll

Bei einer normalen HTTPS-Anfrage sind SNI und Host identisch und alles läuft weiter wie bisher. Passen die Namen nicht zusammen, beendet Nginx ab sofort die Anfrage mit:

421 Misdirected Request

Der Statuscode passt hier ziemlich gut: Die Verbindung an sich funktioniert, aber sie wurde nicht für den im HTTP-Request verlangten Host aufgebaut.

Reine HTTP-Aufrufe dürfen von der Prüfung nicht betroffen sein. Das ist unter anderem für Weiterleitungen auf HTTPS und gegebenenfalls für ACME-Challenges wichtig (sollte man noch HTTP-01 verwenden).

Das verwendete Nginx-Snippet

Die naheliegende Ein-Zeilen-Prüfung ist für einen ausschließlich mit TLS betriebenen Serverblock ausreichend:

if ($ssl_server_name != $host) {
    return 421;
}

Nginx Proxy Manager kann eigene Einstellungen jedoch auch in Serverblöcke einbauen, die HTTP verarbeiten. Dort ist $ssl_server_name leer. Die kurze Variante würde dann selbst einen normalen HTTP-Aufruf mit 421 ablehnen.

Deshalb alles etwas komplizierter und eine Aktivierung ausschließlich bei HTTPS:

set $sni_host_mismatch 0;

if ($https = on) {
    set $sni_host_mismatch 1;
}

if ($ssl_server_name = $host) {
    set $sni_host_mismatch 0;
}

if ($sni_host_mismatch = 1) {
    return 421;
}

Die drei if-Blöcke schreiben keine URL um und enthalten keine Proxy-Anweisungen. Sie setzen nur eine Variable oder beenden die Anfrage mit return. Das bleibt überschaubar und vermeidet die üblichen Probleme, die komplexe Nginx-Konfigurationen innerhalb von if verursachen können.

Einbau in den NPM

Möglichkeit 1: direkt am Proxy Host in der Oberfläche

Bei wenigen Hosts lässt sich das Snippet direkt in Nginx Proxy Manager eintragen:

  1. den gewünschten Proxy Host öffnen;
  2. das kleine Rad Settings anklicken;
  3. das vollständige Snippet in Custom Nginx Configuration einfügen;
  4. speichern und den Host anschließend testen;
  5. die Änderung bei allen weiteren öffentlichen Proxy Hosts wiederholen.

NPM-001.png

Das ist die angenehmste Variante zum Ausprobieren. Ich sehe sofort, welcher Host die zusätzliche Prüfung besitzt, und kann zunächst nur einen einzelnen Host umstellen.

Der Nachteil zeigt sich später: Das Snippet wird mehrfach gepflegt und bei einem neu angelegten Proxy Host leicht vergessen. Für eine kleine, stabile Umgebung ist die Lösung trotzdem völlig in Ordnung.

Möglichkeit 2: zentral über die Custom-Konfiguration

Soll die Prüfung automatisch für alle vorhandenen und zukünftigen Proxy Hosts gelten, kann sie zentral in das persistente NPM-Datenverzeichnis gelegt werden. Die von NPM generierten Dateien unter /data/nginx/proxy_host/ fasse ich dabei nicht an – sie werden bei Änderungen über die Oberfläche neu erzeugt.

Das Snippet kommt stattdessen zum Beispiel in:

/data/nginx/custom/sni-host-validation.conf

In /data/nginx/custom/server_proxy.conf wird es anschließend eingebunden:

include /data/nginx/custom/sni-host-validation.conf;

server_proxy.conf wird von Nginx Proxy Manager in die erzeugten Proxy-Serverblöcke aufgenommen. Existiert dort bereits eine eigene Konfiguration, sichere und prüfe ich sie vor der Änderung, statt die Datei einfach zu überschreiben.

Vor dem Neuladen teste ich immer erst die komplette Nginx-Konfiguration:

docker exec <NPM-CONTAINER> nginx -t

Nur wenn der Test erfolgreich ist, wird sie neu geladen:

docker exec <NPM-CONTAINER> nginx -s reload

Die zentrale Variante ist weniger fehleranfällig, weil auch ein später angelegter Proxy Host automatisch geschützt ist. Dafür wirkt eine fehlerhafte Änderung sofort auf alle Hosts. Ein vorheriger Syntaxcheck und ein schneller Funktionstest der wichtigsten Dienste gehören deshalb unbedingt dazu.

Beide Varianten gleichzeitig zu verwenden bringt keinen Vorteil. Ich entscheide mich entweder für die Einträge in der Oberfläche oder für die zentrale Custom-Konfiguration.

Testen, ob die Regel wirklich greift

Zuerst rufe ich alle Dienste ganz normal im Browser auf. Anmeldung, Weiterleitungen und WebSockets sollen unverändert funktionieren. Danach folgt ein absichtlich falsch zusammengesetzter Request:

curl --http1.1 --silent --show-error --output /dev/null \
  --write-out '%{http_code}\n' \
  --resolve git.example.net:443:203.0.113.10 \
  https://git.example.net/ \
  -H 'Host: home.example.net'

--resolve verbindet den Test gezielt mit der angegebenen Adresse und sendet im TLS-Handshake den Namen git.example.net. Der zusätzliche Header verlangt innerhalb dieser Verbindung dagegen home.example.net. Vor der Härtung kann dadurch die zweite Anwendung antworten. Danach ist ausschließlich 421 das gewünschte Ergebnis.

Bei mehreren öffentlichen Hosts teste ich idealerweise alle Kombinationen. Ein einzelnes erfolgreiches Beispiel beweist nur, dass genau dieses Paar abgewiesen wird.

Kleine Stolperfallen

  • Gewollte Aliasnamen wie www.example.net und example.net sind technisch verschiedene Namen. Entweder bekommen sie eigene passende Proxy Hosts oder werden vor dem globalen Rollout bewusst getestet.
  • Sehr alte Clients ohne SNI können mit der strikten Zuordnung nicht sinnvoll arbeiten. Für normale aktuelle Browser und Apps spielt das praktisch keine Rolle mehr.
  • HTTP/2 darf Verbindungen unter bestimmten Bedingungen wiederverwenden. Ein sauberer Client kann dabei einmal 421 erhalten und baut anschließend eine passende Verbindung auf.
  • Nach einem NPM-Update kontrolliere ich, ob die zentrale Custom-Datei weiterhin an derselben Stelle eingebunden wird.

Fazit

Die Änderung ist klein, schließt aber eine Lücke zwischen TLS- und HTTP-Routing. In meiner Umgebung ist sie bei einem aktiven Test aufgefallen, bevor unterschiedliche Schutzregeln der Proxy Hosts daraus ein echtes Problem machen konnten.

Zuletzt aktualisiert am 05.09.2026 · 1 Aufrufe