Hinter nginx Basic Auth liefert ein totes Backend 401
Ein Endpunkt auf unserem eigenen Server antwortete monatelang mit HTTP 401 — ohne laufenden Dienst dahinter. Warum Auth vor einem Proxy ein totes Backend verbirgt.
Bei einer reinen Lese-Inventur unseres Produktionsservers am 26. September 2026 fanden wir einen öffentlichen HTTPS-Endpunkt, der seit April Anfragen beantwortete — mit nichts dahinter.
Er lieferte kein 502. Er lieferte 401.
Was wir gefunden haben
Der Endpunkt ist eine Browser-Proxy-Route auf einem unserer eigenen Hostnamen. nginx terminiert TLS auf einem hohen Port, verlangt HTTP Basic Auth und leitet an einen lokalen Port weiter:
server {
listen 18443 ssl;
server_name ...;
auth_basic "...";
auth_basic_user_file /etc/nginx/.htpasswd-...;
location / {
proxy_pass http://127.0.0.1:18789;
}
}
Auf 127.0.0.1:18789 lauscht seit Monaten nichts. Es gibt keinen Prozess und keine systemd-Unit — entweder wurde der Dienst auf dieser Maschine nie als verwaltete Unit installiert, oder er wurde entfernt und die Route blieb stehen.
Von außen sieht der Endpunkt gesund aus:
$ curl -s -o /dev/null -w '%{http_code} %{time_total}\n' https://.../
401 0.019678
Fünfzehn bis zwanzig Millisekunden und ein 401. Eine völlig unauffällige Antwort von einem Dienst, der nicht existiert.
Auf dem Host ist die Wahrheit ein Kommando entfernt — wenn man weiß, wonach man sucht:
$ ss -ltnp | grep 18789
(keine Ausgabe)
$ curl http://127.0.0.1:18789/
curl: (7) Failed to connect to 127.0.0.1 port 18789: Connection refused
Warum ein 401 zurückkommt und kein 502
nginx bearbeitet eine Anfrage in festgelegten Phasen. auth_basic wird in der Access-Phase ausgewertet, proxy_pass läuft in der späteren Content-Phase. Lehnt die Access-Phase eine Anfrage ab, läuft die Content-Phase nie — nginx öffnet also keine Verbindung zum Backend und erfährt nie, dass das Backend weg ist.
Der Hinweis steckt im Error-Log. Ein wirklich defekter Proxy schreibt eine Zeile wie connect() failed (111: Connection refused) while connecting to upstream. Unserer schrieb gar nichts, weil nie eine Verbindung versucht wurde.
In etwa einer Minute nachstellen
Dafür braucht es keine Anwendung und keine zweite Maschine. Die Konfiguration:
daemon off;
worker_processes 1;
events { worker_connections 16; }
http {
access_log off;
server {
listen 127.0.0.1:18999;
auth_basic "probe";
auth_basic_user_file /tmp/nginx-401-repro/htpasswd;
location / {
proxy_pass http://127.0.0.1:18789; # hier lauscht nichts
}
}
}
Passwortdatei anlegen und ein Wegwerf-nginx im Vordergrund starten:
mkdir -p /tmp/nginx-401-repro
printf 'probe:%s\n' "$(openssl passwd -apr1 probe)" > /tmp/nginx-401-repro/htpasswd
chmod 644 /tmp/nginx-401-repro/htpasswd
nginx -p /tmp/nginx-401-repro -c /tmp/nginx-401-repro/nginx.conf \
-e /tmp/nginx-401-repro/error.log
Dann die zwei Anfragen, die alles erzählen:
# ohne Zugangsdaten: nginx antwortet aus der Auth-Schicht
$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:18999/
401
$ grep -c upstream /tmp/nginx-401-repro/error.log
0
# mit gültigen Zugangsdaten: der Proxy wird tatsächlich versucht
$ curl -s -o /dev/null -w '%{http_code}\n' -u probe:probe http://127.0.0.1:18999/
502
$ grep -c upstream /tmp/nginx-401-repro/error.log
1
Dieselbe Konfiguration, dasselbe tote Backend, derselbe Moment. Der Statuscode hängt allein davon ab, ob die Anfrage durch die Auth-Schicht kam.
Die eigentliche Lehre
Jede Prüfung, die vor einer Authentifizierungsschicht ansetzt, prüft die Authentifizierungsschicht. Unsere meldete einen toten Dienst monatelang als erreichbar, weil „hat geantwortet und war kein 5xx“ die gesamte Prüfung war.
Das gilt weit über Basic Auth hinaus. Derselbe blinde Fleck entsteht bei:
- einer IP-Freigabeliste, die
403liefert - einer CDN- oder WAF-Challenge-Seite mit
403oder503 - einer Wartungsseite, die der Proxy selbst ausliefert
- einem
/healthz, das perreturn 200in der Proxy-Konfiguration beantwortet wird, statt an die Anwendung weitergeleitet zu werden
In all diesen Fällen antwortet der Proxy anstelle der Anwendung — und der tatsächliche Zustand der Anwendung bleibt unsichtbar.
Drei Korrekturen, in der Reihenfolge, in der wir sie anwenden würden:
- Auf die erwartete Antwort prüfen, nicht auf das Ausbleiben eines Fehlers. Ein konkreter Statuscode und ein konkretes Stück des Antwortkörpers. „Kein
5xx“ ist keine Zustandsprüfung. - Den Dienst dort prüfen, wo er tatsächlich läuft. Auf dem Host beantwortet
curl http://127.0.0.1:18789/die Frage, die die öffentliche URL nicht beantworten kann. - Der Prüfung einen Weg durch die Auth-Schicht geben. Ein
location = /healthzmitauth_basic off;, das an die Anwendung weiterleitet, liefert ein echtes502, wenn die Anwendung unten ist. Dieser Endpunkt sollte nur Lebenszeichen zurückgeben — er ist bewusst unauthentifiziert.
Wie es hier gerade steht
Behoben ist es nicht, und das ist die ehrliche Antwort. Die Route ist aus unserer Spalte „funktioniert“ heraus, und der Dienst ist jetzt eine ausdrückliche Entscheidung zwischen Löschen und Reparieren statt einer als gesund angenommenen Abhängigkeit. Ein totes Backend ist selbst nicht angreifbar — das hier ist ein Monitoring-Fehler, kein Sicherheitsvorfall. Genau deshalb hat er so lange überlebt.
Wann das Backend gestorben ist, wissen wir nicht. Wir wissen, dass die Routen-Konfiguration am 7. April 2026 geschrieben wurde und dass am
- September 2026 nichts dahinter war. In den 172 Tagen dazwischen hat nichts
Alarm geschlagen, weil nie etwas die richtige Frage gestellt hat.
Referenz: ngx_http_auth_basic_module und die Phasenreihenfolge im nginx Development Guide. Gemessen auf nginx 1.26.0 (Ubuntu).