reverse proxy rails

Reverse Proxy Rails : Le guide complet pour une gestion de trafic sans surprise


RubyRéférence pratiqueAvancé

Reverse Proxy Rails : Le guide complet pour une gestion de trafic sans surprise

Dans le développement d’applications web modernes basées sur Ruby on Rails, la gestion du trafic et des requêtes est un sujet central qui dépasse souvent les seules limites de l’application elle-même. Lorsqu’une application devient complexe ou nécessite une couche de sécurité avancée, il devient indispensable de comprendre comment intercepter et diriger le flux d’accès avant qu’il n’atteigne le framework Rails. C’est là qu’intervient la notion de passerelle inverse (reverse proxy). Maîtriser l’intégration d’un reverse proxy rails est une compétence architecturale clé pour garantir performance, sécurité et scalabilité.

reverse proxy rails
Illustration : reverse proxy rails

Prérequis

Pour suivre ce guide, il faut maîtriser les bases de l’HTTP/1.1 et HTTP/2. Nous travaillons avec des versions précises pour garantir la reproductibilité :

  • OS: macOS 14 (ou Linux récent).
  • Ruby: Ruby 3.3.x (avec les dernières mises à jour de Gem dependencies).
  • Framework: Rails 7.2.x.
  • Proxy Web Server: Nginx 1.26+ (compilé avec support OpenSSL 3.0 pour la gestion TLS moderne).

Installation des dépendances de base :

gem install rails -v 7.2

# Installation et test du proxy Nginx sur macOS (via Homebrew)
brew update && brew upgrade nginx

# Initialisation d'une application Rails de démonstration
rails new my_app --database=sqlite3
cd my_app
bundle install

Nous allons utiliser ce setup pour les tests et benchmarks.

Comprendre reverse proxy rails

Le processus implémenté par un mécanisme de type reverse proxy Rails opère généralement au niveau de la couche 7 (Application) du modèle OSI. Comprendre cette distinction entre les couches réseau est fondamental. Un simple proxy TCP ne suffit pas, car il ignore le protocole HTTP lui-même et ses mécanismes internes.

Flux L4 vs L7 :

  • L4 (Transport): Opère sur les sockets IP/Port. Il transfère des octets sans interpréter l’en-tête HTTP. Ce niveau est utile pour la haute disponibilité basique, mais il est totalement incapable de faire du routage basé sur le chemin spécifique (par exemple, distinguer /api de /admin).
  • L7 (Application): Interprète finement les en-têtes GET, POST, et les chemins URI. C’est ce niveau qui permet un véritable reverse proxy rails avancé, car il peut analyser la requête pour déterminer quelle partie du système doit recevoir le trafic. Le passage de L4 à L7 est essentiel pour toute logique métier complexe ou tout comportement d’un serveur type Rails.

Le code — reverse proxy rails

Ruby
require 'rack/utils'

def simple_proxy(app, env)
  # Utilise Rack::Utils pour simuler le transfert de requête.
  # Dans Nginx, ceci serait géré par proxy_pass et les headers.
  request = Rack::Request.new(env)

  # On capture l'IP source dans un header personnalisé ici,
  # car en réalité c'est la couche réseau qui devrait le faire de manière fiable.
  real_ip = env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR']
  env['HTTP_CLIENT_REAL_IP'] = real_ip if real_ip

  # On passe l'environnement modifié à l'application Rails cible.
  [200, {'Content-Type' => 'text/plain', 'X-Client-Ip' => env['HTTP_CLIENT_REAL_IP']}, ["OK pour #{real_ip}"]] 
end

def app = ->(env) { simple_proxy(nil, env) }
# Ce bloc simule la fonction de middleware que le reverse proxy doit respecter.

Explication

Le middleware Rack ne doit pas seulement *transmettre* la requête ; il doit en faire un agent actif pour garantir que les informations critiques (IP, Host) sont visibles par l’application Rails. C’est là qu’intervient le principe du moindre étonnement.

Le rôle de env['HTTP_X_FORWARDED_FOR'] : Lorsque Nginx reçoit une requête réelle (Client -> Proxy) puis la transmet au backend (Proxy -> Rails), il doit impérativement ajouter l’en-tête X-Forwarded-For. Si tu écris un middleware qui lit uniquement env['REMOTE_ADDR'], tu vas lire l’IP de Nginx, pas celle du client.

Le code ci-dessus (RailsProxyMiddleware) agit comme une couche d’abstraction pour forcer cette transmission des headers. Il ne se contente pas de passer les données ; il *normalise* la source de vérité IP.

Pourquoi HTTP_CLIENT_REAL_IP ? Certaines gemmes Rails ou librairies spécifiques au logging s’attendent à une variable d’environnement particulière pour l’IP réelle. En injectant ce header (même si cela semble redondant), on garantit la compatibilité avec le stack applicatif existant, même après un changement de version majeure de Rails (ex: passage de 6.x à 7.2). C’est une mesure défensive contre les changements implicites des couches inférieures.

Le piège du Host : Si le reverse proxy rails ne transmet pas correctement l’en-tête Host original, Rails va tenter de construire des URLs internes avec son propre nom d’hôte (ex: http://localhost:3000/users). En réalité, il doit utiliser le domaine public (https://monapp.com/users). Le middleware ou la configuration Nginx doivent donc forcer l’utilisation du Host original dans les headers pour que Rails puisse générer des URLs absolues correctes (via ActionView::Helpers::UrlHelper). Il faut souvent configurer un header X-Forwarded-Host en plus de X-Forwarded-For.

Documentation officielle : Ruby

Second exemple

Ruby
class RailsProxyMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    # 1. Récupérer l'IP source (si elle vient du proxy) ou utiliser ce que Rack fournit.
    client_ip = env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR']
    
    # 2. Injecter/mettre à jour les headers de confiance pour le backend Rails.
    env['rack.request.head'] ||= {} 
    env['rack.request.head']['X-Forwarded-For'] = client_ip
    env['HTTP_CLIENT_REAL_IP'] = client_ip # Pour des librairies spécifiques qui lisent ces env vars
    
    # 3. Transférer la requête au backend.
    @app.call(env)
  end
end

Exemple d'utilisation

Supposons que nous ayons configuré Nginx et notre middleware Rack dans Rails (méthode RailsProxyMiddleware). Nous testons l’accès via un client externe, qui ne connaît pas les IPs internes.

# Simulation du flux de requête
$ curl -v http://localhost/api/v2/users 
  --header "X-Forwarded-For: 192.0.2.4"

# Sortie attendue (si le middleware fonctionne) :
*   Trying 127.0.0.1:80... Connected to localhost (IP réelle visible).
< HTTP/1.1 200 OK
< Content-Type: text/plain; charset=utf-8
< X-Client-Ip: 192.0.2.4  # L'en-tête personnalisé est bien transmis

$ # Dans le log Rails, remote_ip doit être '192.0.2.4', et non l'IP de la machine locale.

Cas d'usage avancés

1. Microservices avec trafic asynchrone :

Scenario: Un service Rails gère le frontend, mais délègue les tâches lourdes (ex: export CSV) à un worker RabbitMQ/Sidekiq hébergé sur un autre port ou machine. Le reverse proxy rails doit gérer deux types de trafic : HTTP synchrone et requêtes POST qui déclenchent des jobs.

Contrainte: Latence critique pour la réponse utilisateur (doit renvoyer immédiatement 202 Accepted). Solution: Configurer Nginx avec un bloc proxy_pass vers le service principal, mais ajouter une logique de redirection/réponse immédiate dans ce même proxy si l'endpoint est connu pour être asynchrone. Le reverse proxy rails doit donc savoir quand répondre lui-même et quand laisser passer.

2. Multi-tenant SaaS avec routage basé sur le sous-domaine :

Scenario: clientA.monapp.com vs clientB.monapp.com. Le reverse proxy rails doit router les requêtes vers des instances Rails différentes ou passer un header spécifique pour que l'application sache quel client elle sert.

Contrainte: Sécurité et performance de la lecture du Host. Solution: Utiliser le server_name dans Nginx, mais crucialement, s'assurer qu'un middleware Rack lit ce sous-domaine depuis les headers transférés (ex: Host) pour l'intégrer au contexte utilisateur Rails.

3. API Gateway avec limitation de débit :

Scenario: Le point d'entrée unique (/api/*) doit appliquer une limite globale de taux (Rate Limiting) avant que la requête n'atteigne l'application Rails, quel que soit le modèle ou l'endpoint appelé.

Contrainte: Performance et précision du comptage. Solution: Nginx est excellent pour cela (module ngx_http_limit_req). Le reverse proxy rails doit être configuré en amont avec des limites strictes (rate=10r/s limit=5r) pour protéger l'application contre les pics de charge malveillants. On mesure souvent ici la latence ajoutée par le mécanisme de comptage.

Erreurs courantes

Perte des en-têtes d'authentification

Le reverse proxy rails tronque les headers personnalisés (ex: jetons Bearer) car ils ne sont pas dans la liste par défaut de `proxy_set_header`. L'application Rails voit donc le client comme non authentifié, même si l'en-tête était présent.

À éviter

proxy_pass http://backend; # Manque les headers
Correct

proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    # Doit inclure tous les headers nécessaires, ex: Autorisation
    proxy_set_header Authorization $http_authorization;

Bonnes pratiques

  • Standardiser l'IP : Ne jamais faire confiance à un seul header (XFF ou TrueClient). Utilise une couche de validation IP stricte en amont pour forcer la source de vérité.
  • Utiliser $request_uri dans Nginx : Pour le proxy_pass, toujours utiliser cette variable ($request_uri) plutôt que des chemins fixes (/) afin d'assurer un routage sans perte de contexte URI.\
  • Séparer les préoccupations (Concern Separation) : Si ton reverse proxy rails gère à la fois l'API et le SPA, envisage deux services backend distincts. Cela limite le blast radius en cas de failover ou d'injection.
  • Tester les timeouts agressivement : Ne te fie pas au défaut du serveur web. Mesure explicitement les proxy_read_timeout pour toutes les opérations I/O longues (rapports, ETL) afin que l'expérience utilisateur reste cohérente.\
  • Mettre en place un Health Check dédié : Le chemin /healthz doit être le plus minimal possible et ne pas dépendre de la logique métier complexe. Il est uniquement pour vérifier la connexion au service backend cible (DB, cache, etc.).

Questions fréquentes

Est-ce que le passage de Rails 6.1 à 7.2 nécessite des changements dans la gestion du <code>reverse proxy rails</code> ?
Oui, potentiellement. Les versions plus récentes de Rack et les mises à jour d'ActionDispatch peuvent modifier subtilement comment `REMOTE_ADDR` est traité ou quels headers sont par défaut inclus/ignorés. Je recommande de re-tester explicitement la chaîne complète des en-têtes (XFF, Host) sur un environnement CI qui reflète ton proxy Nginx cible.
Si j'utilise Envoy comme service mesh devant mon <code>reverse proxy rails</code>, est-ce que les headers XFF sont toujours nécessaires ?
Non. Envoy gère nativement le contexte de la requête et injecte ses propres en-têtes propriétaires (comme `xds_client` ou des métadonnées spécifiques au service mesh). Il faut adapter ton middleware Rack pour lire ces nouveaux champs plutôt que les XFF traditionnels, car c'est lui qui est maintenant ta source de vérité.
Quel impact a l'utilisation d'un proxy inverse sur la latence perçue lors des appels externes ?
Le reverse proxy rails ajoute une surcharge CPU/IO minimale (quelques millisecondes) pour le traitement HTTP et les headers. Le vrai risque n'est pas cette latence, mais l'ajout d'un timeout trop agressif qui force un échec prématuré alors que la requête était en cours de traitement.
Devrais-je utiliser des variables d'environnement pour les configurations du proxy plutôt que le fichier Nginx ?
Non. Les directives `proxy_pass` et les paramètres TLS/SSL complexes sont intrinsèquement liés à la configuration système de Nginx (ou équivalent). Utiliser des variables d'environnement est bon pour l'API Key, mais pas pour définir le comportement réseau du reverse proxy rails.

Sur le même blog

Conclusion

Maîtriser l'architecture d'une passerelle inverse appliquée à un environnement rails dépasse la simple écriture de directives Nginx ou Apache. C'est une question fondamentale d'architecture des couches réseau et applicatives, où chaque en-tête transmis est potentiellement vital pour le bon fonctionnement métier (sécurité, traçabilité). L'implémentation correcte du reverse proxy rails exige donc non seulement de la connaissance HTTP, mais aussi de l'ingénierie système. Je crois que l'approche middleware/Rack qui force la standardisation des headers critiques reste la méthode la plus élégante en termes de code pour intercepter et valider les requêtes avant même qu'elles n'atteignent le cœur applicatif Rails.

À propos de l'auteur
Inès Carondéveloppeuse Rails depuis 2014, sensible à l'élégance du code

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *