Gestion de la "chaîne de confiance" de l'IP réelle de l'utilisateur final

Contexte

Discourse doit être au courant de l’adresse IP réelle de l’utilisateur final.

Cependant, un utilisateur final ne se connecte jamais directement à Discourse car il y a toujours un ou plusieurs serveurs web en amont (nginx fonctionnant dans le conteneur Discourse) en place. Ainsi, nous avons besoin d’un moyen de transmettre ces informations à Discourse de manière fiable.

L’en-tête x-forwarded-for est la solution. Dans ce sujet, je décrirai les mécanismes spécifiques pour gérer correctement ces informations et comment nous nous attendons à ce qu’elles soient propagées.

Modèles

Les différents modèles pour faire confiance aux proxys en amont (par exemple cloudflare.template.yml ou fastly.template.yml) ont été mis à jour pour utiliser des noms de fichiers prévisibles dans les sorties plutôt que de dépendre de la substitution de texte (ce qui est fragile).

Noms de fichiers

server/real-ip-header.conf

Ce fichier contient l’en-tête que nginx fonctionnant dans le conteneur utilisera comme source de vérité, par exemple :

real_ip_header x-forwarded-for;

ou tel que défini dans le modèle Cloudflare :

real_ip_header cf-connecting-ip;

server/real-ip-recursive.conf

Si ce fichier existe, il contrôle la récursion dans le traitement de l’en-tête “IP réelle”. Vous devrez l’activer s’il y a plus d’un proxy devant le conteneur Discourse.

real_ip_recursive on;

Exemple :

Cloudflare → Load Balancer → Conteneur Discourse (qui est nginx + Discourse lui-même)

Avec cette configuration, nginx recevra un x-forwarded-for qui ressemble à :

x-forwarded-for: real_end_user_ip, cloudflare_ip

sur une connexion depuis une IP Load Balancer.

Pour traiter cela, nginx détermine d’abord si l’adresse source de la connexion (l’IP Load Balancer) est de confiance (voir set_real_ip_from) et, si c’est le cas, traitera la dernière IP de l’en-tête x-forwarded-for.

Puisque cette adresse IP est cloudflare_ip, nginx doit ensuite faire cela à nouveau, en vérifiant si cloudflare_ip est de confiance, et en utilisant la prochaine adresse IP qui est real_end_user_ip.

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

Ce fichier contient des directives qui indiquent à nginx quelles adresses IP sont de confiance, et nous pouvons avoir autant de fichiers et de directives que nécessaire.

Les modèles dans discourse_docker créent ces fichiers au besoin (par exemple set-real-ip-from-cloudflare.conf) et si vous avez des besoins supplémentaires, vous pouvez ajouter les vôtres.

Exemple :

Si vous fonctionnez sur AWS et avez un ALB devant le conteneur Discourse, vous pouvez ajouter un fichier supplémentaire en ajoutant ce qui suit à votre définition de conteneur (adapté à votre environnement) :

run:
  - file:
      path: /etc/nginx/conf.d/outlets/server/set-real-ip-from-aws.conf
      chmod: 644
      # AWS VPC est 10.42.0.0/16, faites confiance à toutes les connexions des réseaux ALB
      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 « J'aime »

J’ai un fichier /data/lc-manager-playbook/discourse/docker-templates/allow-local-proxy.template.yml avec

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 {

Cela semble toujours fonctionner aujourd’hui.

Y a-t-il une raison de ne pas distribuer un tel modèle ?

1 « J'aime »

Ce modèle est tout à fait acceptable, il fonctionnera aujourd’hui et pour l’avenir prévisible.

En substance, il s’agit de prévoyance et de résilience.

Si le fichier venait à changer, c’est-à-dire si nous supprimions ou modifiions la chaîne types { que vous recherchez, il cesserait simplement de fonctionner.

Si vous le modifiez pour utiliser une sortie before-server ou server, il continuera de fonctionner même dans ce cas.

2 « J'aime »

À quoi sert exactement de définir X-Forwarded-For maintenant ? Il est prévu/courant qu’il contienne non seulement l’IP du client (dans la mesure où elle est connue) mais aussi la chaîne de proxies.

Dans le message de commit, vous écrivez :

et pourrait finir par utiliser `client_ip` ou `proxyA_ip` selon le chemin d'exécution.

N’est-ce pas un bug de Discourse, ne pas analyser/utiliser la valeur courante de cet en-tête de manière cohérente (resp. correctement pour la tâche pour laquelle il est analysé), plutôt que la valeur de l’en-tête (et Nginx en tant que proxy l’ajoutant couramment) soit un problème ?

Si Discourse n’a pas besoin de connaître la chaîne de proxies, et que la configuration prévue est plutôt de transmettre uniquement l’IP réelle du client (dans la mesure où elle est connue), alors X-Forwarded-For perd son utilité. L’en-tête X-Real-IP est déjà défini, correspondant à l’utilité, rendant maintenant X-Forwarded-For redondant.

C’est une bonne remarque, nous pourrions simplement le supprimer et arriver au même résultat (sauf… voir ci-dessous)

La limite de l’application est en réalité nginx lui-même, pas Discourse ou Rails. Ainsi, la décision sur les proxies distants exacts à faire confiance est prise au point d’entrée de l’application, qui est nginx. Il peut ensuite transmettre cette décision à Discourse.

Par défaut, Rails ne fait confiance qu’aux adresses locales lors du traitement de x-f-f, donc nous le faisons à un endroit différent où nous pouvons facilement le contrôler.

En fait, il s’avère que Rails ne regarde même pas l’en-tête x-real-ip… les en-têtes qu’il examine sont :

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

D’une manière ou d’une autre, cela est arrivé jusqu’à

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;

Nous devrons faire quelques recherches, mais pour l’instant, la réponse est « ça marche ». Ce qui, je suppose, est la raison pour laquelle nous nous retrouvons dans cette situation.

Il semble qu’un gem l’utilise peut-être ?

1 « J'aime »

C’est clair, donc cela concerne davantage ce que Rails ou d’autres gems font avec les en-têtes, et moins ce que le code de Discourse fait.

Il est intéressant de noter que Rails n’utilise pas X-Real-IP, qui est probablement moins couramment utilisé que X-Forwarded-For, mais certainement mieux connu que Forwarded et Client-IP :thinking:.

X-Real-IP est donc probablement obsolète dans la configuration Nginx. Discourse l’utilise en complément de X-Forwarded-For dans les journaux, si je l’interprète correctement ? Je n’ai trouvé aucune autre utilisation ou mention explicite dans le code :

Le passage ci-dessous m’a semblé incorrect à deux égards lorsque je l’ai vu pendant le débogage du partage de la limitation de débit et les erreurs enregistrées concernant l’adresse IP cliente « unix: » invalide après notre mise à niveau de Discourse (nous utilisons un proxy via socket UNIX devant le conteneur et nous nous appuyons sur X-Forwarded-For).

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

Mais je comprends que « ça marche » pour faire de $remote_addr la seule source de vérité fiable, et real_ip_header la méthode canonique pour que les administrateurs contrôlent l’unique adresse IP que Discourse/Rails reçoit. Je vois qu’elle a déjà été ajoutée à Serve Discourse from a subfolder (path prefix) instead of a subdomain :+1:.