Ma configuration Nginx

Publié le 01/12/2017, dernière modification le 16/03/2021.

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
│   ├── headers.conf
│   ├── headers-nocsp.conf
│   ├── php.conf
│   └── tls.conf
├── fastcgi.conf
├── fastcgi_params
├── koi-utf
├── koi-win
├── mime.types
├── nginx.conf
├── scgi_params
├── uwsgi_params
├── websites
│   ├── default.conf
│   └── example.com.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;

    charset             utf-8;
    index               index.html;

    sendfile            on;
    keepalive_timeout   70;

    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      404;
}

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

    include             custom/headers.conf;
    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;

    return              404;
}

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 http2;
    listen      [2001:db8::42]:443 ssl http2;
    server_name example.com;

    include             custom/headers.conf;
    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;

    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.

Commençons par les directives listen : j'y ajoute http2 à la fin afin d'utiliser le protocole HTTP/2 si le client le supporte. Bien qu'il ne soit théoriquement pas absolument nécessaire d'utiliser TLS sur HTTP/2, c'est un fait imposé par la plupart des implémentations, d'où le fait qu'on ne le retrouve pas sur le bloc réservé à HTTP.

Et maintenant, passons aux choses sérieuses…

Les fichiers de configuration personalisés

Au nombre de 4, 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.

headers-nocsp.conf

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

add_header  'X-Frame-Options'           'DENY';
add_header  'X-XSS-Protection'          '1; mode=block';
add_header  'X-Content-Type-Options'    'nosniff';
add_header  'Referrer-Policy'           'same-origin';
add_header  'X-Clacks-Overhead'         'GNU Terry Pratchett';

Les paramètres sont intentionnellement en « mode paranoïaque » afin d'éviter tout problème.

  • X-Frame-Options : empêche le clickjacking par intégration sur d'autres sites ;
  • X-XSS-Protection : empêche certaines attaques XSS, surtout utile pour les vieux navigateurs ne supportant pas les CSP ;
  • X-Content-Type-Options : demande au client de respecter le type MIME renvoyé par le serveur ;
  • Referrer-Policy : dit au client de ne pas dire d'où il vient s'il se rend sur une page située en dehors du domaine actuel (protection de la vie privée) ;
  • X-Clacks-Overhead : hommage à Terry Pratchett.

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. C'est tout droit tiré du wiki Arch Linux en ajoutant le nom du fichier d'index.

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 / R4
ssl_protocols                   TLSv1.2 TLSv1.3;

# ANSSI R13
ssl_prefer_server_ciphers       on;

# ANSSI R6 / R9 / R10
ssl_ciphers                     HIGH:!eNULL:!LOW:!MEDIUM:!EXP:!RC4:!3DES:!MD5:!SHA1:!SHA256:!SHA384:!PSK:!kRSA:!SRP:-DH:+ECDH;

# ANSSI R7
ssl_ecdh_curve                  secp521r1:secp384r1:prime256v1:X25519:X448:brainpoolP512r1:brainpoolP384r1:brainpoolP256r1;

# ANSSI R20
ssl_session_timeout             2m;
ssl_session_cache               shared:SSL:10m;

# ANSSI R35
# OCSP Stapling
# https://en.wikipedia.org/wiki/OCSP_stapling
ssl_stapling                    on;
ssl_stapling_verify             on;
ssl_trusted_certificate         /etc/ssl/certs/ca-certificates.crt;

# ANSSI R23
ssl_early_data                  off;

# https://en.wikipedia.org/wiki/BREACH
# (Not ANSSI R19 since both OpenSSL and nginx disabled TLS compression after CRIME CVE-2012-4929)
gzip                            off;

# 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';

# Certificate Transparency
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect-CT
# 86400 sec = 1 day
add_header                      'Expect-CT' 'max-age=86400, enforce';
  • ssl_prefer_server_ciphers : afin d'éviter certains attaques par régression, c'est le serveur qui impose la suite cryptographique qui sera utilisée ;
  • ssl_protocols : seul TLS 1.2 et, s'il est supporté, TLS 1.3 sont autorisés (les trouillards ayant peur de perdre des clients autoriseront également TLS 1.0 et 1.1) ;
  • ssl_ciphers : définit les suites cryptographiques autorisées, au format OpenSSL ;
  • ssl_ecdh_curve : définit les courbes elliptiques à utiliser lors de l'échange de clé ;
  • add_header : ajoute une en-tête HTTP activant HSTS pour une longue durée ;
  • ssl_stapling : active l'agrafage OCSP ;
  • ssl_stapling_verify : permet au serveur de vérifier la réponse OCSP ;
  • ssl_trusted_certificate : chemin vers la liste des certificats racine de confiance utilisée pour vérifier les réponses OCSP ;
  • gzip : interdit la compression pouvant poser un problème de sécurité.

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 et CryptCheck. D'autres éléments de votre configuration peuvent être testés avec le Mozilla observatory.

L'exemple de la fin : gogs/gitea

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 :

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

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

    include     custom/headers-nocsp.conf;
    add_header  'Content-Security-Policy'   "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; font-src 'self' data:";

    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;
    }
}