Le vrai fonctionnement des vhosts de Nginx

Temps de lecture estimé : 5 minutes

À l’instar de son grand frère Apache, le routage du serveur web Nginx s’appuie sur le mécanisme de serveurs virtuels (ou vhosts) afin de gérer plusieurs services / sites à partir d’une même application. On sera tous d’accord, c’est bien pratique. Seulement il arrive que Nginx se comporte bizarrement : parfois, c’est le vhost redwatch.io qui répond à la requête pour… foobar.com. Oui, tu as bien lu. Alors bug, mauvaise configuration ou piège ? Je t’explique tout.

Quel que soit son statut, cette situation est problématique en plus d’être disgracieuse vu qu’elle envoie au mauvais endroit. Et si on accédait à une zone ou des configurations sensibles ? Ou que le certificat TLS faisait fuiter des informations ? Pour éviter que ça n’arrive et se prémunir de ces trop nombreuses heures de débogage sans comprendre de quoi il retourne (#trueStory), je te propose de plonger au cœur du fonctionnement des vhosts.

Description d’un vhost

Un vhost simplissime comme nous en avons tous fait ressemble à ceci :

server {
  listen 80;
  server_name redwatch.io;

  location / {
    root /usr/share/nginx/html/redwatch.io;
    index index.html;
  }
}

… Vhost que nous avons l’habitude de décrire ainsi :

Lorsqu’une requête arrive sur redwatch.io:80, envoie vers le site situé à /usr/share/nginx/html/redwatch.io.

Bien malheureusement, cette interprétation est fausse. Avant de comprendre la raison précise, et surtout la bonne interprétation à avoir, arrêtons-nous un instant sur les deux directives du bloc server qui poussent à cette croyance : listen1 et server_name2.

La directive listen peut s’écrire de deux façons : soit la syntaxe courte comme on le voit dans l’exemple, soit la syntaxe complète listen address:port. Les deux cas reviennent au même, la syntaxe n’étant qu’un raccourci pour dire « toutes les adresses locales ». Ce qui compte, c’est qu’avec ça Nginx a tout le nécessaire et suffisant pour « écouter » les requêtes entrantes sur les interfaces réseaux.

La directive server_name, elle, est celle qui définit vraiment un vhost spécifique ; elle se compare pour cela au header Host. Pour éviter de dupliquer les blocs serveurs sur une foultitude de noms, il est possible de définir des alias au nom principal en accumulant les alternatives à la suite. Un système de motif nous permet aussi de regrouper les serveurs se ressemblant, comme ceci :

server {
  listen 80;
  server_name ~(www\.)?redwatch.io *.myalias.dot;
  # [...]
}

OK, maintenant que nous avons ces informations bien en tête, comment on tombe sur un vhost précis ?

Comment fonctionne le routage d’une requête

L’erreur de jugement que nous faisons en lisant un vhost est de voir le trinome adresse, port et server_name comme une spécification du routage, couplée fortement. Or, rien n’est moins faux. En effet, comme nous l’avons vu plus haut, Nginx est attaché au système socle par l’entremise des deux informations de la directive listen et c’est par là que rentre la requête réseau. En d’autres termes, cette requête n’a besoin que de l’adresse et du port pour être transmise au logiciel.

Une fois arrivée au sein de Nginx, la requête doit être ventilée au vhost adéquat et c’est seulement là que la directive server_name entre en scène. Mais puisque les vhosts peuvent s’écrire en motif ou par alias, il n’est pas possible d’avoir une relation 1 - 1 entre la requête et un vhost ! Nginx doit donc faire un choix pour établir une correspondance ; très simplement, il procède par comparaison successive des différents candidats, en suivant la règle du « premier arrivé, premier servi ».

De ce fait, nous pouvons préciser notre description du routage des vhosts :

Lorsqu’une requête arrive sur le port 80, si l’en-tête Host correspond à redwatch.io, envoie vers le site situé à /usr/share/nginx/html/redwatch.io.

Toute la subtilité réside dans le si. En conséquence, une question se pose : que se passe-t-il si Nginx ne trouve pas de motif correspondant ?

Le vhost par défaut

Forcément, l’absence de la moindre correspondance est un cas très particulier pour le serveur. En tant que logiciel libre laissant toute latitude de configuration aux développeurs, Nginx ne peut pas mettre une configuration « au cas où ». Aussi, le choix que fait Nginx (et Apache fait exactement de même) est d’avoir un vhost par défaut, celui à qui sera envoyé toutes les requêtes dès lors qu’elles ne correspondent à rien.

La configuration d’un vhost par défaut sous Nginx3 se fait comme suit :

server {
    listen 80 default_server;
    server_name _; # vhost null pour interdire les correspondances

    return 403;
}

Et sur Apache4 :

<VirtualHost _default_:*>
    DocumentRoot "/www/default"
</VirtualHost>

Et s’il n’y a aucun vhost par défaut, les serveurs web choisissent le premier vhost de la liste, tout simplement, et c’est là que ça peut poser problème. Il ne s’agit pas d’un bug de leur part ou d’une situation volontairement piégeuse, juste du moins mauvais choix technique en ultime recours vu qu’il faut bien répondre quelque chose. En définitive, le problème ne réside pas tant dans le fonctionnement de ces outils, mais dans notre compréhension de leur fonctionnement.

Forts de tout ça, précisons une toute dernière fois notre description :

Lorsqu’une requête arrive sur le port 80, si l’en-tête Host correspond à redwatch.io, envoie vers le site situé à /usr/share/nginx/html/redwatch.io, sinon gère-la par le vhost par défaut.

En complément de notre description, nous pouvons dégager quelques mantras pour renforcer le comportement de notre serveur :

  1. les fichiers vhosts étant triés par ordre lexicographique depuis la 1.3.105, forcer une nomenclature pour contrôler la précédence des motifs ;
  2. créer un vhost catch-all sur tous les ports (avec server_name et listen en wildcard) pour créer une réponse par défaut et limiter la propagation des fuites ;
  3. spécifier explicitement le default vhost pour s’assurer du lien vers le catch-all.

Et sinon, la commande nginx -T est votre amie.

Comme d’habitude, vous pouvez retrouver le code pour essayer par vous-même sur le dépôt d’exemple.


    Ce billet vous a plu ? Partagez-le sur les réseaux…


    … Ou inscrivez-vous à la newsletter pour ne manquer aucun article (Si vous ne voyez pas le formulaire, désactivez temporairement uBlock).

    Voir aussi