Debugging eines WAF vor Scaleway Serverless Containers
Inhaltsverzeichnis
Ich betreibe einen öffentlichen ModSecurity- und OWASP-CRS-WAF vor einem durch IAM geschützten Application-Container. Mein Ziel ist, den WAF zum vorgesehenen öffentlichen Einstiegspunkt zu machen und ihn zur einzigen bereitgestellten Komponente zu machen, die das Origin-Zugangstoken besitzt. „Privat“ bedeutet hier, dass Scaleway eine X-Auth-Token-Authentifizierung verlangt; der generierte Application-Endpunkt bleibt am Edge des Providers erreichbar.
Dieser Beitrag dokumentiert, wie ich diese Architektur mit Scaleway Serverless Containers umgesetzt und debuggt habe. Er ist kein Argument dafür, dass jede Anwendung einen WAF braucht. Die hier beschriebenen Probleme – hostbasiertes Routing, TLS-Verifizierung des Upstreams und der Umgang mit Weiterleitungen – entstehen dadurch, dass ein authentifizierter Reverse Proxy vor einem privaten Origin steht. Dies ist die endgültige Konfiguration, nachdem ich eine Routing-Schleife, TLS-Fehler beim Upstream und durchgesickerte Weiterleitungs-URLs behoben hatte.
Es gibt zwei unterschiedliche Namen:
| Name | Beispiel | Aufgabe |
|---|---|---|
| Öffentlicher Hostname | app.example.com | Browseradresse und Ziel von Weiterleitungen. |
| Generierter Upstream-Hostname | private-app.provider.example | Leitet die WAF-Anfrage über den Serverless-Ingress zur Anwendung. |
Diese Rollen getrennt zu halten, war der Schlüssel für das gesamte Design. Die Debugging-Reihenfolge war daher: Die Upstream-Anfrage an den richtigen Container routen, diese Verbindung verifizieren und anschließend browserseitige Weiterleitungen wieder auf die öffentliche Domain zurückübersetzen.
Warum ModSecurity und das OWASP Core Rule Set #
Das WAF-Image verwendet ModSecurity mit dem OWASP Core Rule Set (CRS) und wird von NGINX ausgeliefert. ModSecurity stellt die Prüf-Engine bereit; CRS liefert gepflegte und allgemein nützliche Regeln für verbreitete Angriffsmuster im Web. Das ist ein besserer Ausgangspunkt, als eine Sammlung anwendungsspezifischer regulärer Ausdrücke im Proxy zu schreiben.
Ich habe diese Kombination gewählt, weil sie Open Source, portabel und als offizielles NGINX-Container-Image verfügbar ist. Außerdem bleibt die Zuständigkeit an der HTTP-Grenze: Die Anwendung ist weiterhin für Authentifizierung, Autorisierung, Validierung und Geschäftsregeln verantwortlich, während der WAF eine Ebene allgemeiner Anfrageprüfung ergänzt.
Ein Regelsatz bietet keinen einsatzbereiten Rundumschutz. Starte im DetectionOnly-Modus, prüfe die Audit-Logs, ergänze gezielte Ausnahmen für nachvollziehbare False Positives und aktiviere das Blockieren erst, wenn der beobachtete Datenverkehr dies unterstützt. Pinne das Image anhand von Version und Digest, damit Regelaktualisierungen bewusst erfolgen und testbar sind.
Der WAF ist außerdem weder eine DDoS-Lösung noch eine Netzwerkisolation. Ein privater Scaleway-Container hat weiterhin einen Provider-Endpunkt, aber Anfragen dorthin benötigen ein gültiges X-Auth-Token. Ratenbegrenzung auf Plattformebene und Schutz vor volumetrischen Angriffen sind davon getrennte Aufgaben.
Zuerst Upstream-Anfragen mit dem generierten Hostnamen routen #
Meine ursprüngliche Proxy-Konfiguration übernahm den eingehenden Host explizit – eine verbreitete Entscheidung, wenn eine Anwendung ihren öffentlichen Hostnamen sehen muss:
proxy_set_header Host $http_host;
Das war eine Überschreibung und kein NGINX-Standardverhalten: Das Proxy-Modul setzt den Upstream-Host normalerweise auf $proxy_host.
In dieser Topologie ist das unsicher. Wenn app.example.com auf den WAF zeigt, kann das Zurücksenden dieses Hosts über den gemeinsam genutzten Ingress des Providers dazu führen, dass die Origin-Anfrage des WAF wieder an ihn selbst geroutet wird:
WAF -> shared ingress -> WAF -> shared ingress -> ...
In meinem Fall zeigte sich die Schleife als irreführender Fehler 400 Request Header Or Cookie Too Large. Kein Browser-Cookie musste zu groß sein; bei jedem Durchlauf wurden Weiterleitungs-Header angehängt, bis NGINX die Anfrage ablehnte.
Die Origin-Anfrage muss stattdessen den generierten Hostnamen verwenden:
proxy_set_header Host $proxy_host;
Wenn proxy_pass den generierten Application-Endpunkt verwendet, ist $proxy_host der Hostname dieses Endpunkts. Der Serverless-Ingress kann die Anfrage dadurch an den Application-Container ausliefern.
Danach das Origin-Zertifikat einschließlich seiner Kette verifizieren #
Der WAF sollte die TLS-Verifizierung nicht einfach deshalb deaktivieren, weil der Upstream ein generierter Provider-Endpunkt ist. Er sendet ein Origin-Zugangstoken und muss daher verifizieren, dass er das vorgesehene Origin erreicht hat.
proxy_ssl_server_name on;
proxy_ssl_name $proxy_host;
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
proxy_ssl_verify on;
proxy_ssl_verify_depth 3;
SNI (proxy_ssl_server_name) und proxy_ssl_name sorgen dafür, dass die TLS-Anfrage zum generierten Endpunkt passt. proxy_ssl_verify validiert dessen Zertifikat gegen das CA-Bundle des Container-Images.
Die explizite Verifizierungstiefe wird leicht übersehen. Verwaltete Endpunkte können ein Leaf-Zertifikat gefolgt von mehreren Zwischenzertifikaten und schließlich einer vertrauenswürdigen Root präsentieren. NGINX verwendet für proxy_ssl_verify_depth standardmäßig 1, wodurch eine gültige Kette abgelehnt werden kann; eine Tiefe, die die präsentierte Kette abdeckt, erhält die Verifizierung aufrecht, statt sie abzuschwächen.
Nachdem das Routing funktionierte, erschienen Upstream-Fehler im Browser weiterhin nur als 502. Das Proxy-Fehlerlog zeigte die tatsächliche Ursache, und diese Meldungen wiesen in sehr unterschiedliche Richtungen:
connect() failed -> DNS-, Netzwerk-, Adressfamilien- oder Firewall-Problem
SSL handshaking failed -> Problem mit SNI, CA-Bundle, Zertifikat oder Verifizierungstiefe
Bei Containern macht das Schreiben der Fehler nach Standard Error sie in den Scaleway-Cockpit-Logs verfügbar:
error_log /dev/stderr info;
Zum Schluss Weiterleitungen an der WAF-Grenze umschreiben #
Nach der Korrektur von Routing und TLS war das nächste Symptom eine absolute Weiterleitung der Anwendung mit dem generierten Hostnamen:
Location: https://private-app.provider.example/login
Der Browser darf nicht an diese interne Implementierungsadresse geschickt werden. Schreibe die Antwort stattdessen an der öffentlichen Grenze um, anstatt den Host-Header des Upstreams zu ändern:
proxy_redirect https://$proxy_host/ https://${PUBLIC_HOST}/;
PUBLIC_HOST sollte ein expliziter, für das Deployment spezifischer Wert wie app.example.com sein und kein reflektierter Request-Header. Dadurch bleiben Weiterleitungsziele auf einer Allowlist und die öffentliche Adresse ist eine bewusste Konfiguration.
proxy_redirect ändert nur die Antwort-Header Location und Refresh. Die Anwendung benötigt ihre öffentliche URL weiterhin, wenn sie Links in HTML, API-Antworten, E-Mails oder OAuth-Callbacks erzeugt. Konfiguriere nach Möglichkeit eine explizite externe Basis-URL in der Anwendung. Alternativ kannst du den allowlisteten öffentlichen Namen als X-Forwarded-Host übergeben und die Anwendung so konfigurieren, dass sie diesem Header nur vom WAF aus vertraut. Domainbezogene Cookies brauchen eine eigene Behandlung, etwa durch Anwendungskonfiguration oder proxy_cookie_domain.
Eine kompakte Konfiguration #
Der relevante Ausschnitt aus meiner WAF-Konfiguration steht unten. Der umgebende TLS-Listener, die ModSecurity-Regelkonfiguration, Anfragebegrenzungen und andere deploymentspezifische Einstellungen sind nicht enthalten.
Speichere dies im offiziellen CRS-Image als NGINX-Template, zum Beispiel unter /etc/nginx/templates/conf.d/default.conf.template; der Startprozess ersetzt ${ORIGIN_AUTH_TOKEN} und ${PUBLIC_HOST}. Plain NGINX ersetzt Umgebungsvariablen in Konfigurationsdateien ohne diese Vorverarbeitung nicht.
location = /livez {
access_log off;
default_type text/plain;
return 200 "ok\n";
}
location / {
proxy_pass https://private-app.provider.example;
proxy_http_version 1.1;
# Scaleway ingress routing
proxy_set_header Host $proxy_host;
# Request context and WAF-to-origin authentication
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header X-Forwarded-Host ${PUBLIC_HOST};
proxy_set_header X-Auth-Token ${ORIGIN_AUTH_TOKEN};
# Authenticated TLS to the origin
proxy_ssl_server_name on;
proxy_ssl_name $proxy_host;
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
proxy_ssl_verify on;
proxy_ssl_verify_depth 3;
# Browser-facing URLs
proxy_redirect https://$proxy_host/ https://${PUBLIC_HOST}/;
proxy_connect_timeout 10s;
proxy_read_timeout 120s;
proxy_send_timeout 120s;
}
Das Origin-Token ist der geheime Schlüssel eines dedizierten Scaleway-IAM-API-Keys. Gib seiner IAM-Anwendung die Berechtigung ContainersPrivateAccess und keine unabhängigen weiteren Berechtigungen. Verwende eine IAM-Bedingung, wenn der Zugriff enger als auf Projektebene eingeschränkt werden muss. Speichere den Schlüssel in der Secret-Konfiguration der Container-Plattform, niemals im Image oder Repository.
Das kurze Verbindungs-Timeout und das längere Lese-Timeout sind absichtlich gewählt: Nach erfolgreichem Verbindungsaufbau kann Warten sinnvoll sein, zum Beispiel während eines Cold Starts. Eine Verbindung, die nicht schnell hergestellt werden kann, weist normalerweise auf ein anderes Problem hin.
Liveness des WAF von der Bereitschaft des Origins trennen #
Der WAF und die Anwendung brauchen unterschiedliche Health Checks:
GET /livez -> nimmt der WAF-Prozess Anfragen an?
GET /readyz -> kann der WAF den authentifizierten Health-Endpunkt der Anwendung erreichen?
Verwende den lokalen /livez-Endpunkt für die Startup- oder Liveness-Probe des WAF-Containers. Laut Scaleways Dokumentation zu Health Checks markieren wiederholte Liveness-Fehler den Container als fehlerhaft und stoppen die Weiterleitung von Datenverkehr an ihn, anstatt ihn neu zu starten. Ein ausgefallenes Origin darf daher keinen gesunden WAF aus dem Dienst nehmen; der WAF ist die Komponente, die benötigt wird, um den Fehler zu untersuchen.
Verwende /readyz über den Proxy in Smoke Tests des Deployments. Damit wird der gesamte Pfad abgedeckt: WAF, Upstream-TLS, Serverless-Routing, Origin-Authentifizierung und Health der Anwendung. Eine abschließende Prüfung der Weiterleitung vervollständigt die grundlegenden Checks:
curl -fsS https://app.example.com/livez
curl -fsS https://app.example.com/readyz
curl -sSI https://app.example.com/ \
| tr -d '\r' \
| grep -Fxi 'Location: https://app.example.com/login'
Das nützliche mentale Modell besteht darin, Upstream-Routing, TLS-Identität und browserseitige URLs als getrennte Verantwortlichkeiten zu betrachten. Der generierte Hostname ist für Scaleways Ingress bestimmt, TLS validiert diesen generierten Endpunkt, und der öffentliche Hostname ist ein expliziter Kontext für Anwendung und Browser statt der Routing-Schlüssel des Upstreams. Diese Trennung verhindert Proxy-Schleifen und sorgt dafür, dass die Origin-URL nicht in Weiterleitungen durchsickert.