Handhabung der "Vertrauenskette" der echten IP-Adresse des Endbenutzers

Hintergrund

Discourse muss den echten IP-Adresse des Endbenutzers kennen.

Allerdings verbindet sich ein Endbenutzer nie direkt mit Discourse, da es immer einen oder mehrere vorgelagerte Webserver (nginx, die in dem Discourse-Container laufen) gibt. Daher benötigen wir eine Möglichkeit, diese Information auf vertrauenswürdige Weise an Discourse weiterzuleiten.

Der x-forwarded-for-Header ist die Lösung. In diesem Thema werde ich die spezifischen Mechanismen zur ordnungsgemäßen Handhabung dieser Informationen beschreiben und erläutern, wie wir deren Weitergabe erwarten.

Vorlagen

Die verschiedenen Vorlagen zum Vertrauen von vorgelagerten Proxys (z. B. cloudflare.template.yml oder fastly.template.yml) wurden aktualisiert, um in den Ausgabeeinheiten vorhersehbare Dateinamen zu verwenden, anstatt sich auf Textsubstitution zu verlassen (was fehleranfällig ist).

Dateinamen

server/real-ip-header.conf

Diese Datei enthält den Header, den nginx, das in dem Container läuft, als vertrauenswürdige Quelle verwenden wird, z. B.:

real_ip_header x-forwarded-for;

oder wie in der Cloudflare-Vorlage festgelegt:

real_ip_header cf-connecting-ip;

server/real-ip-recursive.conf

Wenn diese Datei existiert, steuert sie die Rekursion bei der Verarbeitung des „echten IP“-Headers. Sie müssen dies aktivieren, wenn mehr als ein Proxy vor dem Discourse-Container steht.

real_ip_recursive on;

Beispiel:

Cloudflare → Load Balancer → Discourse-Container (bestehend aus nginx + Discourse selbst)

Bei dieser Konfiguration empfängt nginx ein x-forwarded-for, das wie folgt aussieht:

x-forwarded-for: echte_endbenutzer_ip, cloudflare_ip

bei einer Verbindung von einer Load-Balancer-IP.

Um dies zu verarbeiten, bestimmt nginx zunächst, ob die Quelladresse der Verbindung (die Load-Balancer-IP) vertrauenswürdig ist (siehe set_real_ip_from) und verarbeitet gegebenenfalls die letzte IP des x-forwarded-for-Headers.

Da diese IP-Adresse cloudflare_ip ist, muss nginx dies noch einmal tun, indem es überprüft, ob cloudflare_ip vertrauenswürdig ist, und die nächste IP-Adresse verwendet, die real_end_user_ip ist.

server/set-real-ip-from-ENVIRONMENT.conf

Diese Datei enthält Direktiven, die nginx mitteilen, welche IP-Adressen vertrauenswürdig sind, und wir können so viele Dateien und Direktiven haben, wie erforderlich.

Die Vorlagen in discourse_docker erstellen diese Dateien bei Bedarf (z. B. set-real-ip-from-cloudflare.conf), und wenn Sie zusätzliche Anforderungen haben, können Sie Ihre eigenen hinzufügen.

Beispiel:

Wenn Sie in AWS laufen und einen ALB vor dem Discourse-Container haben, können Sie eine zusätzliche Datei hinzufügen, indem Sie Folgendes zu Ihrer Container-Definition hinzufügen (angepasst an Ihre Umgebung):

run:
  - file:
      path: /etc/nginx/conf.d/outlets/server/set-real-ip-from-aws.conf
      chmod: 644
      # AWS VPC ist 10.42.0.0/16, vertraue allen Verbindungen aus den ALB-Netzwerken
      contents: |
        set_real_ip_from 10.42.66.0/24;
        set_real_ip_from 10.42.67.0/24;
  - file:
      path: /etc/nginx/conf.d/outlets/server/real-ip-header.conf
      chmod: 644
      contents: |
        real_ip_header x-forwarded-for;
5 „Gefällt mir“

Ich habe eine /data/lc-manager-playbook/discourse/docker-templates/allow-local-proxy.template.yml mit

after_bundle_exec:
  - replace:
    filename: /etc/nginx/conf.d/discourse.conf
    from: "types {"
    to: |
      set_real_ip_from 192.168.1.0/24;
      set_real_ip_from 192.168.11.0/24;
      set_real_ip_from 172.16.0.0/12;
      set_real_ip_from 10.0.0.0/8;
      real_ip_recursive on;
      real_ip_header X-Forwarded-For;
      types {

Das scheint heute noch zu funktionieren.

Gibt es einen Grund, warum ein solches Template nicht verteilt wird?

1 „Gefällt mir“

Diese Vorlage ist völlig in Ordnung, sie wird heute und in absehbarer Zukunft funktionieren.

Im Wesentlichen geht es um Zukunftssicherheit und Robustheit.

Wenn sich die Datei jemals ändert, d. h. wenn wir die types {-Zeichenfolge, nach der du suchst, entfernen oder ändern, funktioniert sie einfach nicht mehr.

Wenn du sie so änderst, dass sie einen before-server- oder server-Ausgang verwendet, wird sie auch dann noch funktionieren.

2 „Gefällt mir“

Was ist der Zweck, X-Forwarded-For überhaupt zu setzen? Es ist üblich, dass dieser Header nicht nur die Client-IP (soweit bekannt) enthält, sondern auch die Proxy-Kette.

In der Commit-Nachricht schreibst du:

and might end using `client_ip` or `proxyA_ip` depending on codepath.

Ist das nicht ein Fehler in Discourse, der diesen Header nicht konsistent (bzw. korrekt für die jeweilige Aufgabe, bei der er geparst wird) auswertet, anstatt dass der Wert des Headers (und die übliche Anhängung durch Nginx als Proxy) das Problem ist?

Wenn Discourse die Proxy-Kette nicht kennen muss und die beabsichtigte Konfiguration stattdessen darin besteht, nur die tatsächliche Client-IP (soweit bekannt) zu übergeben, verliert X-Forwarded-For seinen Sinn. Der X-Real-IP-Header ist bereits gesetzt und erfüllt diesen Zweck, wodurch X-Forwarded-For redundant wird.

Das ist fair, wir könnten es einfach löschen und zum gleichen Ergebnis kommen (außer… siehe unten)

Die Anwendungsgrenze ist tatsächlich nginx selbst, nicht Discourse oder Rails. Somit wird die Entscheidung, welchen entfernten Proxies genau vertraut wird, am Anwendungseinstiegspunkt getroffen, der nginx ist. Dieser kann diese Entscheidung dann an Discourse weiterleiten.

Standardmäßig vertraut Rails nur lokalen Adressen bei der Verarbeitung von x-f-f, daher tun wir das an einer anderen Stelle, an der wir es leicht kontrollieren können.

Tatsächlich stellt sich heraus, dass Rails den x-real-ip-Header überhaupt nicht beachtet… die Header, die es beachtet, sind

  • forwarded
  • client-ip
  • x-forwarded-for

Irgendwie ist das bis hierher gelangt…

commit 21b562852885f883be43032e03c709241e8e6d4f (tag: v0.8.0)
Author: Robin Ward
Date:   Tue Feb 5 14:16:51 2013 -0500

    Initial release of Discourse

diff --git a/config/nginx.sample.conf b/config/nginx.sample.conf
new file mode 100644
index 00000000..62fabf4a
--- /dev/null
+++ b/config/nginx.sample.conf
…
+    proxy_set_header  X-Real-IP  $remote_addr;

Wir müssen noch ein bisschen recherchieren, aber für jetzt lautet die Antwort „es funktioniert“. Was ich vermute, ist der Grund, warum wir es so gemacht haben.

Sieht aus, als würde vielleicht ein Gem es verwenden?

1 „Gefällt mir“

Verstanden, also geht es mehr darum, was Rails oder andere Gems mit den Headern machen, und weniger darum, was der Discourse-Code tut.

Interessant, dass Rails X-Real-IP nicht nutzt, was wahrscheinlich weniger häufig verwendet wird als X-Forwarded-For, aber sicherlich besser bekannt ist als Forwarded und Client-IP :thinking:.

Wahrscheinlich ist X-Real-IP dann in der Nginx-Konfiguration veraltet. Discourse erweitert die Nutzung zusammen mit X-Forwarded-For in den Logs, wenn ich das richtig interpretiere? Ich konnte keine andere explizite Verwendung/Erwähnung im Code finden:

Das unten sah für mich in zweierlei Hinsicht falsch aus, als ich es beim Debugging der gemeinsamen Rate-Beschränkung sah und Fehlermeldungen wegen der ungültigen „unix:“-Client-IP nach unserem Discourse-Upgrade protokollierte (wir verwenden einen UNIX-Socket-Proxy vor dem Container und verlassen uns auf X-Forwarded-For).

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;

Aber ich verstehe den „es funktioniert“-Aspekt, um $remote_addr zuverlässig als einzigen Wahrheitspunkt zu machen, und real_ip_header als den kanonischen Weg für Admins, die einzelne IP zu steuern, die Discourse/Rails erhält. Ich sehe, dass es bereits zu Serve Discourse from a subfolder (path prefix) instead of a subdomain hinzugefügt wurde :+1:.