Laravel et les CORS derrière un reverse proxy7 min read

Quand on se lance dans un projet de développement logiciel, on s’en tient rarement au tuto qui nous à mis le pied à l’étrier. On aime combiner les différentes technos, et à force on se retrouve forcément dans un cas un peu particulier. Et c’est dans ces cas là qu’il est particulièrement difficile de trouver de l’aide sur le web.

Cet article traite de l’un de ces cas particuliers : l’utilisation de Laravel dans un environnement multi-conteneurs Docker et d’un conteneur Traefik en guise de reverse-proxy. L’application Laravel expose une API à laquelle accède notre front-end, une Single Page Application (contenue dans l’un des conteneurs), mais aussi des applications externes.


Point sur le vocabulaire

Avant d’aller plus loin, voici les définitions de quelques termes utilisés dans cet article.

Conteneur
Un conteneur, “container” en anglais, est un élément logiciel qui peut contenir un service ou une application. Pour le comparer à une machine virtuelle, il ne contient pas l’OS mais repose sur un moteur de conteneur (Docker par exemple) qui lui, partage les ressources de l’OS sur lequel il est installé avec tous les conteneurs qu’il gère. L’avantage principale étant le poids des images de conteneurs bien plus léger que celui des images de machines virtuelles.

CORS
CORS signifie “Cross-origin resource sharing”, soit “Partage des ressources entre origines multiples” en français. C’est un mécanisme qui permet d’accéder aux ressources d’un serveur situé dans un autre domaine que celui dont sont originaires les requêtes.

API
API signifie “Application Programming Interface”, soit “Interface de programmation applicative” en français. C’est une interface bien définie et documentée qui permet de d’interagir avec une entité informatique. Cette interface est souvent publiée pour permettre à des applications extérieures de communiquer avec elle. L’API est la façade destinée aux programmeurs d’une application.


Quel est le problème ?

Après activation du support des CORS, et de part notre situation particulière (conteneurisation + reverse-proxy), l’application Laravel interprète de manière erronée les requêtes venant de son propre front-end. Elles sont considérées comme venant d’une autre origine.

Comprendre le comment du pourquoi nécessite quelques explications sur le fonctionnement de divers éléments. Le schéma organisationnel de l’environnement est donnée au début de cet article.

Traefik et les liaisons non sécurisées

Traefik est ce qu’on appelle un “Edge router”. C’est un aiguilleur qui organise le trafique des requêtes entre d’un côté l’internet, et de l’autre nos conteneurs. Il est la seule interface extérieure de cet environnement Docker. C’est donc lui qui s’occupe de la sécurisation des données par certificats SSL, le fameux https. Dans tous les réseaux internes à notre environnement de conteneurs (docker networks), les communications entre les conteneurs se font de manière non sécurisée, en http. Il n’y a pas besoin de les sécuriser puisque ce sont des réseaux fermés non accessibles de l’extérieur, et dont on connait les acteurs.

Laravel et l’identification des CORS

Depuis Laravel 7.0, le support des CORS se fait via le package fruitcake/laravel-cors, ajouté dans le fichier composer.json à l’initialisation d’un nouveau projet. Si vous avez une version plus ancienne de Laravel, vous pouvez simplement ajouter le package à votre projet.

Pour savoir si une requête vient d’un autre domaine (CORS) ou non, Laravel regarde en premier lieu si elle contient un header nommé Origin (sur lequel nous n’avons aucun pouvoir). Si ce n’est pas le cas, elle est considérée comme venant d’une autre origine.

Si le header est présent en revanche, ce qui est notre cas, Laravel regarde sa valeur et la compare à ce qu’il appelle SchemeAndHttpHost. Si leurs valeurs sont égales, alors la requête est considérée comme venant de la même origine et ne nécessite pas de traitement particulier. S’ils sont différents par contre…

C’est la récupération de ce SchemeAndHttpHost qui peut poser problème, et plus particulièrement la partie Scheme qui peut valoir soit “http” ou “https”. En effet Laravel, ou plutôt Symfony (framework sur lequel est basé Laravel) dans ce cas précis, fait une vérification en profondeur et ne retournera “https” que si la requête vient d’un proxy de confiance (une liste qu’on lui aurait fournie), ou si elle est réellement issue d’une connexion https.

Et c’est bien ici que réside notre problème. Comme la requête reçue par Laravel arrive de Traefik, notre reverse-proxy, elle est issue d’une connexion non sécurisée. La valeur de SchemeAndHttpHost, “http://domain.com” est donc différente de la valeur du header Origin “https://domain.com”, la requête est donc traitée comme CORS par Laravel alors qu’on aurait voulu qu’elle soit traitée normalement, comme une requête locale.

La solution

Les proxys de confiance

Depuis la version 5.5 de Laravel, le package fideloper/proxy a fait son apparition dans le fichier composer.json. Ce package va nous sauver la vie car il permet de facilement configurer une liste de proxies de confiance, c’est-à-dire des adresses IP de provenance des requêtes pour lesquelles on considérera la connexion comme sécurisée.

De cette manière la valeur de SchemeAndHttpHost retournée par Laravel sera bien “https://domain.com” et nos requêtes venant de notre front-end seront traitées comme il se doit.

Une IP imprévisible

Seulement voilà, les adresse IP à l’intérieur d’un réseau Docker ne sont pas fixes. Elle peuvent changer à chaque nouveau démarrage de Docker, et en plus de ça, il est impossible de les connaître à l’avance.

Heureusement, puisque notre application Laravel est dans un conteneur, protégée par notre reverse-proxy, le seul moyen d’y accéder est de passer par notre conteneur Traefik qui, lui, gère une connexion sécurisée avec le reste du monde. On peut donc affirmer avec certitude que toutes les requêtes que notre application reçoit sont sécurisées. On peut alors s’autoriser à considérer toutes les IP comme “de confiance”. Et cela n’affectera pas les requêtes qui ont comme origine un autre domaine, puisque leur nom de domaine sera effectivement différent (lors de la comparaison avec SchemeAndHttpHost).

Mise en place de la solution

Si le fichier config/trustedproxy.php n’apparait pas dans votre projet Laravel, vous pouvez l’y ajouter en lançant la commande de publication suivante :

php artisan vendor:publish --provider="Fideloper\Proxy\TrustedProxyServiceProvider"

Astuce : dans Laravel, on peut utiliser la commande vendor:publish sans arguments. Un menu interactif permettra alors de sélectionner les contenus à publier.

Voici donc à quoi ressemble notre fichier config/trustedproxy.php :

<?php

return [

    /*
     * Set trusted proxy IP addresses.
     *
     * Both IPv4 and IPv6 addresses are
     * supported, along with CIDR notation.
     *
     * The "*" character is syntactic sugar
     * within TrustedProxy to trust any proxy
     * that connects directly to your server,
     * a requirement when you cannot know the address
     * of your proxy (e.g. if using ELB or similar).
     *
     */
    // 'proxies' => null, // [<ip addresses>,], '*', '<ip addresses>,'

    /*
     * To trust one or more specific proxies that connect
     * directly to your server, use an array or a string separated by comma of IP addresses:
     */
    // 'proxies' => ['192.168.1.1'],
    // 'proxies' => '192.168.1.1, 192.168.1.2',

    /*
     * Or, to trust all proxies that connect
     * directly to your server, use a "*"
     */
    'proxies' => '*',

    /*
     * Which headers to use to detect proxy related data (For, Host, Proto, Port)
     *
     * Options include:
     *
     * - Illuminate\Http\Request::HEADER_X_FORWARDED_ALL (use all x-forwarded-* headers to establish trust)
     * - Illuminate\Http\Request::HEADER_FORWARDED (use the FORWARDED header to establish trust)
     * - Illuminate\Http\Request::HEADER_X_FORWARDED_AWS_ELB (If you are using AWS Elastic Load Balancer)
     *
     * - 'HEADER_X_FORWARDED_ALL' (use all x-forwarded-* headers to establish trust)
     * - 'HEADER_FORWARDED' (use the FORWARDED header to establish trust)
     * - 'HEADER_X_FORWARDED_AWS_ELB' (If you are using AWS Elastic Load Balancer)
     *
     * @link https://symfony.com/doc/current/deployment/proxies.html
     */
    'headers' => Illuminate\Http\Request::HEADER_X_FORWARDED_ALL,

];

Pour aller plus loin

Pour en savoir un peu plus sur cette histoire de proxys de confiance, Symfony donne un petit manuel sur How to Configure Symfony to Work behind a Load Balancer or a Reverse Proxy. La documentation du package fideloper/proxy sur Github est également très riche en informations.

Configuration

Voici la configuration logicielle dans laquelle l’article a été rédigé. Si vos versions sont trop différentes, il se peut que l’article ne s’applique par parfaitement (n’hésitez pas à nous le faire savoir en commentaires).

  • Docker : 19.03.5
  • Docker-compose : 1.25.0
  • Laravel : 7.0
  • Traefik : 2.0
  • fruitcake/laravel-cors : 1.0

Related Posts

Leave a Reply