Aller au contenu

Ma configuration Nginx

··8 mins
Sommaire

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.

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
│   ├── auranet.conf
│   ├── blog.conf
│   ├── default.conf
│   ├── freebox.conf
│   └── projects.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 :

user             http;
worker_processes auto;

events {
    worker_connections 1024;
}

http {
    include                mime.types;
    default_type           application/octet-stream;

    server_tokens          off;
    port_in_redirect       off;

    types_hash_bucket_size 128;

    charset                utf-8;
    index                  index.html;

    sendfile               on;
    keepalive_timeout      65;

    gzip                   on;

    access_log             off;

    include websites/default.conf;
    include websites/example.com.conf;
}

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.

server {
    listen      198.51.100.42:80 default_server;
    server_name "";

    include     custom/headers.conf;

    return      403;
}

server {
    listen              198.51.100.42:443 default_server ssl;
    http2               on;
    server_name         "";

    include             custom/headers.conf;
    include             custom/tls.conf;
    include             custom/anti-bots.conf;

    ssl_certificate     /var/lib/acmed/certs/exemple.com_ecdsa-p384.crt.pem;
    ssl_certificate_key /var/lib/acmed/certs/exemple.com_ecdsa-p384.pk.pem;

    return              403;
}

Chaque véritable application web est configurée dans un fichier au nom du domaine sur laquelle elle se trouve :

server {
    listen      198.51.100.42:80;
    listen      [2001:db8::42]:80;
    server_name example.com;

    include     custom/headers.conf;

    root        /srv/http/example.com;

    location ~ ^/(?!(\.well-known)) {
        return      301 https://example.com$request_uri;
    }
}

server {
    listen      198.51.100.42:443 ssl;
    listen      [2001:db8::42]:443 quic reuseport;
    listen      [2001:db8::42]:443 ssl;
    http2       on;
    server_name example.com;

    include             custom/headers.conf;
    include             custom/tls.conf;

    ssl_certificate     /var/lib/acmed/certs/exemple.com_ecdsa-p384.crt.pem;
    ssl_certificate_key /var/lib/acmed/certs/exemple.com_ecdsa-p384.pk.pem;

    root        /srv/http/example.com;
    error_page  404 /404/index.html;
}

Ici, le premier bloc server sert à rediriger les requêtes HTTP vers HTTPS, à l’exception des requêtes dont l’URL commence par /.well-known. Ces dernières sont en effet utilisées pour des taches spéciales, en particulier pour répondre aux défis HTTP-01 du protocole ACME. 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 très gros fichier remplis de zéros et compressons le avec gzip afin qu’il ne prenne qu’environ 300 Mo d’espace sur le disque.

dd if=/dev/zero bs=1M count=$((300*1024)) | gzip >/srv/http/example.com/anti_bots/big_bomb.gz

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. Notez la présence de gzip_static afin de dire que le fichier est déjà compressé et qu’il ne faut donc pas le recompresser. Ainsi, le fichier 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.

# Environment and configuration
location ~* /(\.env|\.git|composer\.json|composer\.lock|docker\.env|docker\.yml|docker\.yaml|Docker\.env|Docker\.yml|Docker\.yaml|mysql\.env|mysql\.yml|mysql\.yaml|config\.bak\.php) {
    gzip        on;
    gzip_static always;
    alias       /srv/http/example.com/anti_bots/big_bomb;
}

# WordPress
location ~* /(wp-login\.php|atomlib\.php|wordpress|wp|wp2|old-wp|wp-includes) {
    gzip        on;
    gzip_static always;
    alias       /srv/http/example.com/anti_bots/big_bomb;
}

# phpMyAdmin
location ~* /(phpmyadmin|pma) {
    gzip        on;
    gzip_static always;
    alias       /srv/http/example.com/anti_bots/big_bomb;
}

# Misc
location ~* /(alfacgiapi|apple-app-site-association|class\.api\.php|cloud\.php|cms|frontend_dev\.php|github\.php|wso112233\.php|xmrlpc\.php) {
    gzip        on;
    gzip_static always;
    alias       /srv/http/example.com/anti_bots/big_bomb;
}

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.

types           {}
default_type    application/octet-stream;

headers-nocsp.conf
#

C’est ici que sont ajoutés toute une série d’en-têtes HTTP communes à chaque application.

# HTTP/3
add_header 'Alt-Svc'                'h3=":443"; ma=86400';

# https://developer.mozilla.org/fr/docs/Web/HTTP/Headers/X-Frame-Options
add_header 'X-Frame-Options'        'DENY';

# https://developer.mozilla.org/fr/docs/Web/HTTP/Headers/X-Content-Type-Options
add_header 'X-Content-Type-Options' 'nosniff';

# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
add_header 'Referrer-Policy'        'same-origin';

# https://xclacksoverhead.org/home/about
# http://www.gnuterrypratchett.com/
add_header 'X-Clacks-Overhead'      'GNU Terry Pratchett';

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.

include     custom/headers-nocsp.conf;
add_header  'Content-Security-Policy'   "default-src 'self'";

php.conf
#

Tout simplement ma configuration PHP, utilisant chez moi FastCGI pour interfacer Nginx avec PHP-FPM.

index  index.php;

location ~ \.php$ {
    try_files $uri  $document_root$fastcgi_script_name =404;
    fastcgi_pass    unix:/run/php-fpm/php-fpm.sock;
    fastcgi_index   index.php;
    fastcgi_param   SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include         fastcgi.conf;
}

tls.conf
#

Et enfin, le meilleur étant pour la fin, la configuration TLS.

# Recommandations de l'ANSSI
# https://www.ssi.gouv.fr/administration/guide/recommandations-de-securite-relatives-a-tls/

# ANSSI R3 : Privilégier TLS 1.3 et accepter TLS 1.2
# ANSSI R4 : Ne pas utiliser SSLv2, SSLv3, TLS 1.0 et TLS 1.1
ssl_protocols			TLSv1.2 TLSv1.3;

# ANSSI R13 : Préférer l’ordre de suites du serveur
ssl_prefer_server_ciphers	on;

# ANSSI R6 : Échanger les clés en assurant toujours la PFS
# ANSSI R7 : Échanger les clés avec l’algorithme ECDHE
# ANSSI R9 : Privilégier AES ou ChaCha20
# ANSSI R10 : Utiliser un mode de chiffrement intègre
# ANSSI R12 : Disposer de plusieurs suites cryptographiques
#
# openssl ciphers -v -stdname 'ECDH:!SHA1:!SHA256:!SHA384:!aRSA:!AESCCM8:!ARIA'
# man openssl-ciphers
ssl_ciphers			ECDH:!SHA1:!SHA256:!SHA384:!aRSA:!AESCCM8:!ARIA;

# ANSSI R7 : Échanger les clés avec l’algorithme ECDHE
# openssl ecparam -list_curves
ssl_ecdh_curve      X25519:prime256v1:secp384r1:secp521r1:brainpoolP256r1:brainpoolP384r1:brainpoolP512r1;

# ANSSI R20 : Limiter la durée de vie des tickets
ssl_session_tickets             on;
ssl_session_timeout             2m;
ssl_session_cache               shared:SSL:10m;

# ANSSI R23 : Ne pas transmettre de données 0-RTT
ssl_early_data                  off;

# /!\ ANSSI R35 : Préférer l’agrafage OCSP
# OCSP Stapling: https://en.wikipedia.org/wiki/OCSP_stapling
#
# Bien que l'ANSSI recommande d'activer l'OCSP Stapling, Let's Encrypt ne
# supporte plus cette fonctionnalité.
# https://letsencrypt.org/2024/12/05/ending-ocsp/
ssl_stapling                    off;
ssl_stapling_verify             off;
ssl_trusted_certificate         /etc/ssl/certs/ca-certificates.crt;

include     custom/tls-headers.conf;

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.

# HSTS
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
# 31536000 sec = 365 days
add_header 'Strict-Transport-Security' 'max-age=31536000; includeSubDomains; preload';

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 :

server {
    listen      198.51.100.42:443 ssl;
    listen      [2001:db8::69]:443 quic reuseport;
    listen      [2001:db8::69]:443 ssl;
    http2       on;
    server_name git.example.com;

    error_log   /var/log/nginx/git.example.com-error.log;

    include     custom/anti-bots.conf;

    include     custom/headers-nocsp.conf;
    add_header  'Content-Security-Policy'   "default-src 'self'; connect-src 'self'; font-src 'self' data:; form-action 'self'; img-src 'self' https: data:; manifest-src 'self' data:; object-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; worker-src 'self'";

    include             custom/tls.conf;
    ssl_certificate     /etc/acmed/certs/exemple.com_ecdsa-p384.crt.pem;
    ssl_certificate_key /etc/acmed/certs/exemple.com_ecdsa-p384.pk.pem;

    location / {
        proxy_pass          http://127.0.0.1:3000/;
        proxy_set_header    X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

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
#

Capture d’écran du Qualys SSL Labs montrant un résultat A+
Qualys SSL Labs A+

Mozilla HTTP Observatory
#

Capture d’écran de l’observatoire HTTP de Mozilla montrant un résultat A+.
Mozilla HTTP Observatory A+