Obtenir l'adresse IP réelle d'un visiteur avec une application Rails derrière un reverse-proxy comme Cloudflare

Dans une application Rails, il est aisé de récupérer l'adresse IP du client web avec la méthode remote_ip accessible dans toute action d'un contrôleur. Lorsque l'on place notre application “derrière” un reverse-proxy, comme CloudFlare par exemple, on obtient l'adresse IP de ce proxy à la place (le proxy intercepte la requête entre notre serveur et le visiteur).

Comment Rails récupère l'adresse IP du visiteur ?

Pour bon nombre d'applications web écrites en Ruby, les middleware Rack sont une composante essentielle de leur fonctionnement. En Rails, c'est ActionDispatch::Request qui a pour charge d'analyser les éléments de la requête en cours. Ces données sont compilées dans un objet request exposé par ActionController::Base, la classe dont héritent tous les contrôleurs de nos applications.

On peut donc appeller request.remote_ip pour obtenir l'adresse IP du visiteur.

Pourquoi n'obtient-on pas la bonne adresse IP ?

Suite à la configuration d'une application avec Cloudflare en tant que proxy, nous avons remarqué que l'adresse renvoyée par remote_ip n'était pas celle du visiteur, mais celle d'un des serveurs frontaux de Cloudflare.

La documentation de Cloudflare précise pourtant l'adresse du visiteur est indiquée dans le header X-Forwarded-For comme le veut l'usage. Rails indique pourtant extraire cette information de la requête et nous l'exposer. Qu'est-ce qui ne fonctionne pas ?

Comprendre en lisant le code source

Nous n'avons pas facilement trouvé d'information sur ce problème, aussi la seule solution était de comprendre le fonctionnement de remote_ip pour voir d'où venait le problème. Et c'est cette ligne qui a provoqué le déclic.

Le middleware de Rails ne fait pas confiance, par défaut, à un proxy dont l'IP n'est pas explicitement spécifiée dans votre application. La liste d'adresses acceptées par défaut par Rails est très restreinte et limitée à des plage d'adresses non routables. Ces adresses sont habituellement utilisées dans les configurations serveur web frontal/reverse-proxy sur un réseau local, ou une même machine comme 127.0.0.1 par exemple.

Cela évite une attaque malicieuse où le header X-Forwarded-For serait renseigné par le visiteur avec une adresse IP qui n'est pas la sienne.

Rails constitue donc une liste de toutes les adresses IP : l'adresse de la requête puis toutes les adresses des proxies (spécifiées dans X-Forwarded-For). Si toutes les adresses de cette liste font partie de la liste de confiance, la dernière adresse sera considérée comme l'adresse réelle. Sinon, on restera sur l'adresse IP de la requête et on ignorera complètement le champ X-Forwarded-For.

Dans notre cas, lorsque l'application tournait derrière NGINX, l'IP du proxy étant locale à la machine (127.0.0.1) tout se passait bien. En basculant derrière Cloudflare, l'adresse de leur proxy de ajoutée dans le header n'étant pas trusted, c'est l'IP de la requête qui est logiquement retournée dans request.remote_ip.

Simple mais pas évident !

Configurer l'application pour autoriser les proxies de Cloudflare

Consulter le guide de configuration de Rails apporte comme souvent la solution : une ligne de configuration permet de spécifier une liste de proxies autorisés.

Notre problème a trouvé sa solution en ajoutant ces lignes à notre fichier config/environment/production.rb :

config.action_dispatch.trusted_proxies = [
  "127.0.0.1",       # Liste des proxies de base https://github.com/rails/rails/blob/v5.2.3/actionpack/lib/action_dispatch/middleware/remote_ip.rb#L35
  "::1",
  "fc00::/7",
  "10.0.0.0/8",
  "172.16.0.0/12",
  "192.168.0.0/16",
  "173.245.48.0/20", # Liste obtenue sur https://www.cloudflare.com/ips-v4/
  "103.21.244.0/22",
  "103.22.200.0/22",
  "103.31.4.0/22",
  "141.101.64.0/18",
  "108.162.192.0/18",
  "190.93.240.0/20",
  "188.114.96.0/20",
  "197.234.240.0/22",
  "198.41.128.0/17",
  "162.158.0.0/15",
  "104.16.0.0/12",
  "172.64.0.0/13",
  "131.0.72.0/22",
  "2400:cb00::/32", # Liste obtenue sur https://www.cloudflare.com/ips-v6
  "2606:4700::/32",
  "2803:f800::/32",
  "2405:b500::/32",
  "2405:8100::/32",
  "2a06:98c0::/29",
  "2c0f:f248::/32",
].map { |proxy| IPAddr.new(proxy) }

IPAddr permettant d'autoriser la comparaison sur des plages IP tout comme sur des adresses fixes.

Utilisation dans d'autres middlewares

Si jamais l'adresse IP du visiteur vous est nécessaire dans un autre middleware, par exemple pour faire un filtrage, pensez-bien à utiliser ActionDispatch::RemoteIP pour récupérer la bonne adresse :

request = ActionDispatch::Request.new(env)
if request.remote_ip === IPAddr.new("...")
  ...
end

Ou alors si cette information vous est nécessaire dans un middleware existant, il y a toujours possibilté de s'inspirer du code source de GitLab, qui monkey-patch Rack::Request pour y utiliser la liste des proxies autorisés dans ActionDispatch::Request.

Garder la liste des IP CloudFlare à jour

Évidement, il reste la question de la maintenance de cette liste d'adresses. Elle est susceptible d'évoluer dans le temps, CloudFlare déployant régulièrement de nouveaux serveurs, donc proxies.

La liste de leurs adresses IP v4 et v6 est mise à disposition sous forme de page web ou de fichiers texte faciles à aspirer et analyser. Ces URLs étant simples et assurément invariable dans le temps, on peut s'appuyer dessus.

Si vous souhaitez automatiser la mise à jour de votre liste, nous avons remarqué la gem cloudflare-rails qui monkey-patch le fonctionnement de ActionDispatch::RemoteIp pour y intégrer un cache qui est régulièrement rafraichi à partir des urls fournies par Cloudflare.