Il existe bien des manières de configurer son serveur web sans qu’une soit nécessairement meilleure que les autres. Mais parce que l’on me demande régulièrement des conseils sur ce sujet et que je veux bien aider mais pas me répéter, j’explique ici ma manière de voir les choses. Copiez, modifiez, inspirez-vous, critiquez, c’est open-bar.
Compression #
Avant de rentrer dans le vif du sujet, il est important de faire un point sur la compression des données car il existe plusieurs stratégies possibles. Le choix de la stratégie influençant directement sur les directives de configuration, il est donc nécessaire de bien la définir avant de se lancer dans la configuration elle-même.
Afin de réduire la volume des données envoyées au client, le serveur peut compresser les données avant de les transmettre. Pour ça, le client indique dans sa requête les algorithmes de compression qu’il supporte à l’aide de l’en-tête Accept-Encoding et le serveur lui répond en compressant avec un de ces algorithmes qu’il supporte, en indiquant lequel dans l’en-tête Content-Encoding.
Il existe plusieurs algorithmes de compression, les trois principaux étant les suivants :
- gzip: algorithme historique supporté par quasiment tout le monde, compresse peu mais le fait rapidement en consommant peu de ressources ;
- Brotli: algorithme récent supporté par les navigateurs modernes, compresse beaucoup mais consomme des ressources ;
- Zstd: algorithme encore plus récent supporté par les navigateurs modernes, compresse beaucoup mieux que gzip, un peu moins bien que Brotli, mais le fait rapidement en consommant peu de ressources.
En réalité les niveaux de compression sont configurables, ce que je viens d’indiquer ne sont que des ordres de grandeur à un niveau de compression similaire.
Nginx supporte nativement gzip. Pour utiliser la Brotli ou Zstd, il est nécessaire d’utiliser respectivement les plugin ngx_brotli et zstd-nginx-module. Ces plugins sont généralement inclus dans les paquets de la plupart des distributions, par exemple les paquets nginx-mod-http-brotli et nginx-mod-http-zstd pour Alpine Linux.
A priori, choisir quel algorithme de compression supporter peut ne pas
sembler évident car il ne semble pas possible d’avoir à la fois une très
large compatibilité, un fort niveau de compression et ne pas augmenter la
charge du processeur. Cependant, il existe un cheat-code : il est possible de
pré-compresser les données statiques (typiquement les images, feuilles de style,
etc.) afin que le serveur n’ai pas besoin de les compresser à chaque fois. Afin
que ça fonctionne, il faut que le fichier présente l’extension correspondant au
mode de compression (.gz, .br ou .zst). Ainsi, afin de servir le fichier
index.html pré-compressé, il faut avoir :
index.html(non compressé, conservé pour les clients ne supportant pas la compression)index.html.gz(compression gzip)index.html.br(compression Brotli)index.html.zst(compression Zstd)
La stratégie que j’ai adopté est de désactiver la compression à la volée
(directives gzip et assimilées à off) mais de servir les fichiers statiques
compressés dès qu’une version pré-compressée existe (directives gzip_static et
assimilées à on).
La génération des fichiers pré-compressé peut se faire avec les commandes suivantes qu’il convient, bien entendu, d’adapter à vos besoins spécifiques. La première produira des versions compressées de tous les fichiers, la seconde se limitera aux fichiers ayant une certaine extension.
find "./chemin/vers/le/dossier" \
-type f \
-exec gzip --keep --force '{}' \; \
-exec brotli -9 '{}' \; \
-exec zstd -z -19 '{}' \;
find "./chemin/vers/le/dossier" \
-type f \
\( \
-name "*\.css" \
-or -name "*\.html" \
-or -name "*\.jpeg" \
-or -name "*\.jpg" \
-or -name "*\.js" \
-or -name "*\.json" \
-or -name "*\.png" \
-or -name "*\.svg" \
-or -name "*\.txt" \
-or -name "*\.xml" \
\) \
-exec gzip --keep --force '{}' \; \
-exec brotli -9 '{}' \; \
-exec zstd -z -19 '{}' \;C’est encore un des bénéfices des sites statiques : pré-compresser tout le site est extrêmement simple. Pour un site dynamique ce n’est pas possible, vous devez compresser à la volée, ce qui consomme des ressources. Donc double peine vu que vous avez déjà consommé des ressources pour générer la réponse. En fait, avec un site dynamique, la seule option que vous avez c’est de devoir garder les réponses en cache, avec toute la complexité de la chose ainsi que les problèmes récurrents inhérents à cette méthode.
Arborescence des fichiers #
/etc/nginx
├── custom
│ ├── anti-bots.conf
│ ├── force-download.conf
│ ├── headers.conf
│ ├── headers-nocsp.conf
│ ├── php.conf
│ ├── tls.conf
│ └── tls-headers.conf
├── fastcgi.conf
├── fastcgi_params
├── koi-utf
├── koi-win
├── mime.types
├── modules.d
├── nginx.conf
├── scgi_params
├── uwsgi_params
├── websites
│ ├── default.conf
│ ├── example.com.conf
│ └── example.org.conf
└── win-utfÀ la racine, on retrouve tous les fichiers par défaut fournis avec
l’installation du paquet. Je vous laisse lire la documentation de votre
distribution pour savoir comment les utiliser. Il n’y a véritablement que le
fichier principal, nginx.conf, que j’ai modifié comme suit :
|
|
Pour user, laissez donc la valeur indiquée de base qui sera en accord avec
d’éventuels changements dans la conception du paquet de votre distribution.
Ce qu’il est intéressant de voir ici, c’est que j’ai mis l’intégralité des
blocs server dans des fichiers séparés par domaines et localisés dans le
répertoire websites. Bien entendu, default.conf correspond à ce qui sera
servi par défaut si une personne demande un domaine non existant sur l’IPv4.
Pour l’IPv6, on utilisera une adresse différente par serveur afin de ne pas
avoir ce genre de problèmes.
Notez également qu’il est fait usage du module nginx-acme afin de prendre en charge les certificats X.509 utilisés pour TLS. Il est donc nécessaire d’installer ce module. Au besoin, vous pouvez retirer les éléments de configuration propres à ce module afin de gérer les certificats par un autre moyen.
De plus, pour fonctionner, nginx-acme nécessite que l’on configure un résolveur DNS. Ici j’ai mis la boucle locale et indiqué en commentaire des alternatives publiques possibles. Configurez ce paramètre suivant votre cas.
|
|
Dans le serveur par défaut, nous demandons des certificats non pas pour un nom de domaine, mais pour des adresses IP. Let’s Encrypt ne permet pas cela dans le profil par défaut, c’est pour ça qu’il est impératif d’utiliser le profil shortlived.
Chaque véritable application web est configurée dans un fichier au nom du domaine sur laquelle elle se trouve :
|
|
Ici, le premier bloc server sert à rediriger les requêtes HTTP vers HTTPS.
Les points relatifs au service lui-même se trouvent dans le second bloc server
qui, lui, est réservé à HTTPS, d’où l’écoute sur le port 443.
À noter que, bien qu’il ne soit théoriquement pas nécessaire d’utiliser TLS sur HTTP/2, c’est en réalité imposé par la plupart des implémentations, d’où le fait qu’on ne le retrouve pas sur le bloc réservé à HTTP.
Notez la présence d’un listen en double sur l’adresse IPv6. Le premier
contient quic reuseport, ce qui permet d’accepter HTTP/3. Si cela fonctionne
également en IPv4, il n’est en revanche possible de le spécifier qu’une seule
fois par adresse IP. Dans la mesure où la plupart du temps l’adresse IPv4 est
réutilisée alors que l’on utilise des adresses IPv6 uniques, j’ai ici préféré
ne le mettre que pour l’IPv6. Au besoin, pensez à adapter à votre situation.
Et maintenant, passons aux choses sérieuses…
Les fichiers de configuration personalisés #
Au nombre de 7, ils sont situés dans le répertoire custom. C’est dans ces
fichiers que je mets des bouts de configuration réutilisables dans différentes
applications, il n’y a qu’à les inclure dans le bloc server correspondant.
anti-bots.conf #
S’il y a une chose que je déteste voir sur mon serveur web, ce sont des robots qui cherchent des failles de sécurité de manière totalement automatisée. J’ai donc préparé un fichier un peu spécial qui leur sera servi. Pour cela, commençons par générer un fichier de 300 GiB remplis de zéros et compressons le avec gzip, Brotli et Zstd afin qu’il ne prenne qu’environ 300 MiB d’espace disque pour gzip, moins de 300 Kio pour Brotli et environ 9,5 Mio for Zstd.
$ dd if=/dev/zero bs=1GiB count=300 | gzip >/srv/http/example.com/anti_bots/big_bomb.gz
$ dd if=/dev/zero bs=1GiB count=300 | brotli -9 -o /srv/http/example.com/anti_bots/big_bomb.br
$ dd if=/dev/zero bs=1GiB count=300 | zstd -z -19 -o /srv/http/example.com/anti_bots/big_bomb.zst
Et maintenant on créé le fichier anti-bots.conf qui, si l’URL contient au
moins un des motifs définis, retourne le contenu de ce fichier. Ainsi, grâce
aux directives gzip_static, brotli_static et zstd_static nous n’avons
pas besoin du fichier initial, le fichier compressé sera renvoyé tel quel
avec l’en-tête HTTP indiquant que le contenu est compressé, ce qui forcera la
bibliothèque HTTP du bot à le décompresser afin d’en lire le contenu.
|
|
Bien entendu, pensez à adapter les motifs en fonction de votre situation. Ce serait dommage d’accidentellement vous servir votre propre bombe de compression parce que vous avez un phpMyAdmin.
force-download.conf #
Parfois vous ne souhaitez pas que le client ouvre un fichier dans son navigateur, vous souhaitez au contraire forcer le téléchargement du fichier en question. Voici la configuration qui permet de faire ça, il ne reste plus qu’à l’inclure là où c’est pertinent.
|
|
headers-nocsp.conf #
C’est ici que sont ajoutés toute une série d’en-têtes HTTP communes à chaque application.
|
|
Les paramètres sont intentionnellement en « mode paranoïaque » afin d’éviter tout problème.
headers.conf #
Inclue le fichier précédent et ajoute une politique de sécurité de contenu
très stricte. C’est le fichier que j’utilise par défaut : si une application
nécessite une CSP plus souple j’utilise headers-nocsp.conf afin de préciser
autre chose.
|
|
php.conf #
Tout simplement ma configuration PHP, utilisant chez moi FastCGI pour interfacer Nginx avec PHP-FPM.
|
|
tls.conf #
Et enfin, le meilleur étant pour la fin, la configuration TLS.
|
|
Il ne reste plus qu’à indiquer dans chaque bloc server où se trouvent la clé
privée ainsi que le certificat. Pour ce dernier, on n’oubliera pas d’inclure
les certificats intermédiaires.
Avec tout ça, si vos logiciels sont à jour, vous devriez avoir un joli A+ sur les test de configuration les plus stricts. À cette fin, citons le Qualys SSL Server test, nmap et testssl.sh.
$ nmap --script ssl-enum-ciphers -p 443 example.org
D’autres éléments de votre configuration peuvent être testés avec le Mozilla observatory.
tls-headers.conf #
Ce fichier est inclut par le fichier tls.conf, cependant, dans certains cas
de configuration, il ne sera pas possible d’ajouter de header à l’endroit où
l’on configure TLS et l’import ne fonctionnera donc pas. J’ai donc créé un
fichier séparé afin de pouvoir l’inclure là où c’est pertinent.
|
|
L’exemple de la fin : gogs/gitea/forgejo #
Afin de montrer un exemple de configuration avec des CSP ajustées au plus près d’une application, je vous propose ma configuration pour gogs/gitea/forgejo :
|
|
HTTP/3 #
HTTP/3 n’utilise pas TCP mais QUIC, ce qui fait qu’un client commence par
initier une connexion TCP en HTTP/1.1 ou HTTP/2. Le support d’HTTP/3 ne lui est
annoncé que lors de la réponse à cette première requête à l’aide de l’en-tête
Alt-Svc (cf. fichier /etc/nginx/custom/headers-nocsp.conf).
Cette première requête dégrade fortement les performances. Afin qu’elle
s’effectue directement en HTTP/3, il est donc nécessaire d’indiquer au client
que HTTP/3 est supporté avant même qu’il n’initie sa première connexion. C’est
possible grâce au DNS avec une entrée de type HTTPS. Ce sujet dépassant le
sujet de la configuration d’Nginx, je vous laisse vous renseigner plus en détail
et tester votre configuration sur le site savearoundtrip.
Le résultat #
Si vous avez suivi ce billet et que vous avez également bien configuré votre DNS, alors vous devriez obtenir une excellente notation sur les différents outils de test. À titre d’information, voici les résultats pour ce blog à date de la dernière mise à jour du présent billet.
Qualys SSL Labs #

Mozilla HTTP Observatory #
