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.
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 : listen
1 et server_name
2.
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 ?
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 ?
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.3.10
5, forcer une nomenclature pour contrôler la précédence des motifs ;server_name
et listen
en wildcard) pour créer une réponse par défaut et limiter la propagation des fuites ;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).