Durcissement de la sécurité Nginx sur Ubuntu et Debian
Renforcez Nginx au-delà de sa configuration par défaut avec les en-têtes de sécurité, TLS 1.3, HSTS, le filtrage des méthodes HTTP et les contrôles d'accès. Chaque directive est liée à l'attaque qu'elle bloque.
Une installation Nginx par défaut sert du trafic. Elle ne le protège pas. La configuration par défaut divulgue le numéro de version, accepte n'importe quelle méthode HTTP, n'envoie aucun en-tête de sécurité et utilise les paramètres TLS fournis par OpenSSL.
Ce guide durcit Nginx sur Debian 12 et Ubuntu 24.04. Chaque section nomme d'abord la menace, puis montre la directive qui la bloque. Vous pouvez appliquer ces modifications sur un serveur en production sans interruption de service.
Prérequis :
- Nginx installé et servant au moins un site en HTTPS (Configurer Let's Encrypt SSL/TLS pour Nginx sur Debian 12 et Ubuntu 24.04)
- Accès root ou sudo
- Familiarité de base avec la structure de configuration Nginx (Structure des fichiers de configuration Nginx)
Nous stockons toutes les directives de durcissement dans un seul fichier include. Cela garde vos blocs server propres et facilite l'audit :
sudo touch /etc/nginx/snippets/security-hardening.conf
Chaque bloc server à durcir reçoit une seule ligne :
include /etc/nginx/snippets/security-hardening.conf;
Après chaque modification dans ce guide, testez et rechargez :
sudo nginx -t && sudo systemctl reload nginx
Comment masquer la version du serveur Nginx ?
Ajoutez server_tokens off; dans le bloc http de /etc/nginx/nginx.conf. Cela supprime le numéro de version de l'en-tête de réponse Server et des pages d'erreur par défaut. Les attaquants scannent les versions spécifiques ayant des CVE connues. Masquer la version ne corrige pas les vulnérabilités, mais augmente le coût des attaques ciblées.
Ajoutez dans /etc/nginx/nginx.conf à l'intérieur du bloc http :
server_tokens off;
Cela va dans la configuration principale, pas dans le snippet, car c'est un paramètre global.
Après rechargement, vérifiez l'en-tête de réponse :
curl -sI https://your-domain.com | grep -i server
Server: nginx
Plus de numéro de version. Avant cette modification, l'en-tête affichait quelque chose comme Server: nginx/1.24.0.
Comment bloquer les requêtes vers des noms d'hôte inconnus ?
Quand quelqu'un accède directement à l'adresse IP de votre serveur ou utilise un nom d'hôte non reconnu, Nginx sert le premier bloc server qu'il trouve. Cela ouvre la porte aux attaques de type DNS rebinding et permet aux scanners de caractériser vos services.
Un bloc server par défaut qui rejette toutes les requêtes non appariées règle ce problème. Ajoutez-le comme premier bloc server chargé par Nginx :
sudo nano /etc/nginx/sites-available/00-default-deny.conf
server {
listen 80 default_server;
listen [::]:80 default_server;
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;
# Self-signed or snakeoil cert just to complete the TLS handshake
ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
return 444;
}
Le code de statut 444 est spécifique à Nginx. Il ferme la connexion immédiatement sans envoyer de réponse.
Activez-le :
sudo ln -s /etc/nginx/sites-available/00-default-deny.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Sur Debian 12 et Ubuntu 24.04, installez le paquet ssl-cert si le certificat snakeoil est absent :
sudo apt install ssl-cert
Testez en accédant directement par l'IP :
curl -sI -k https://YOUR_SERVER_IP
La connexion se ferme avec une réponse vide ou une erreur curl (code de sortie 52). Aucun contenu servi.
Quelle configuration TLS utiliser pour Nginx en 2026 ?
Utilisez le profil Intermediate de Mozilla : TLS 1.2 et 1.3 avec des suites de chiffrement à confidentialité persistante. Cela couvre les clients jusqu'à Firefox 27 et Android 4.4.2 tout en abandonnant les protocoles non sécurisés. TLS 1.0 et 1.1 sont vulnérables à POODLE et BEAST et sont dépréciés depuis la RFC 8996 (2021). Si tous vos clients supportent TLS 1.3, utilisez plutôt le profil Modern (ssl_protocols TLSv1.3; uniquement).
Le durcissement TLS est devenu plus urgent début 2026. CVE-2026-1642 (CVSS 5.9) a démontré que la gestion TLS upstream de Nginx avait une condition de concurrence permettant une injection MitM avant la fin du handshake. Le correctif est arrivé dans Nginx 1.28.2 et 1.29.5. Vérifiez votre version avec nginx -v et mettez à jour si nécessaire.
| Profil | Protocoles | Client le plus ancien | Cas d'usage |
|---|---|---|---|
| Modern | TLS 1.3 uniquement | Firefox 63, Chrome 70 | API, applications web modernes |
| Intermediate | TLS 1.2 + 1.3 | Firefox 27, Android 4.4 | Serveurs polyvalents |
| Old | TLS 1.0 + 1.1 + 1.2 + 1.3 | IE 8 sur XP | Conformité legacy uniquement |
Générez les paramètres DH pour les suites de chiffrement DHE du profil Intermediate. Cela prend quelques minutes :
sudo openssl dhparam -out /etc/nginx/dhparam.pem 2048
sudo chmod 644 /etc/nginx/dhparam.pem
Ajoutez le durcissement TLS dans /etc/nginx/snippets/security-hardening.conf :
# TLS protocols - Mozilla Intermediate profile
ssl_protocols TLSv1.2 TLSv1.3;
# Cipher suites - Mozilla Intermediate profile (version 5.7)
# TLS 1.3 suites are configured automatically by OpenSSL
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305';
ssl_prefer_server_ciphers off;
ssl_dhparam /etc/nginx/dhparam.pem;
# Session settings
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
Pourquoi ssl_prefer_server_ciphers off ? Avec uniquement des chiffrements forts dans la liste, la préférence client offre de meilleures performances. Les clients choisissent le chiffrement que leur matériel accélère le mieux.
Pourquoi ssl_session_tickets off ? Les tickets de session utilisent une clé serveur globale. Si cette clé fuit, un attaquant peut déchiffrer toutes les sessions enregistrées. Sans tickets, le cache de session partagé assure la reprise uniquement sur le même serveur, ce qui convient aux installations mono-serveur.
Note sur le stapling OCSP : Let's Encrypt a arrêté son service OCSP en août 2025. Si vous utilisez des certificats Let's Encrypt, supprimez les directives ssl_stapling. Elles provoqueront des avertissements dans vos logs. Let's Encrypt publie désormais les informations de révocation exclusivement via les CRL.
Rechargez et testez depuis votre machine locale :
openssl s_client -connect your-domain.com:443 -tls1_2 </dev/null 2>/dev/null | grep "Protocol\|Cipher"
Protocol : TLSv1.2
Cipher : ECDHE-RSA-AES256-GCM-SHA384
Confirmez que TLS 1.1 est rejeté :
openssl s_client -connect your-domain.com:443 -tls1_1 </dev/null 2>&1 | head -5
Le handshake échoue. Pour un audit complet, soumettez votre domaine à SSL Labs. Visez un grade A ou A+.
Comment activer HSTS dans Nginx ?
HSTS (HTTP Strict Transport Security) indique aux navigateurs de se connecter uniquement en HTTPS pendant une durée définie. Sans cette directive, un attaquant MitM peut intercepter la requête HTTP initiale et forcer un downgrade de la connexion. Une fois qu'un navigateur a vu l'en-tête HSTS, il refuse les connexions HTTP en clair vers votre domaine jusqu'à l'expiration de max-age.
Ajoutez dans /etc/nginx/snippets/security-hardening.conf :
# HSTS - 2 years, include subdomains
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
Le flag always garantit que Nginx envoie cet en-tête sur les réponses d'erreur aussi (4xx, 5xx). Sans always, une réponse 403 ou 500 n'inclurait pas l'en-tête, laissant une fenêtre pour les attaques par downgrade.
Avant d'ajouter includeSubDomains, confirmez que tous vos sous-domaines supportent HTTPS. Cette directive s'applique à chaque sous-domaine. Un sous-domaine sans certificat valide devient inaccessible.
Preloading HSTS : Ajouter preload à l'en-tête et soumettre votre domaine à hstspreload.org inscrit l'application HTTPS en dur dans les navigateurs. C'est permanent. Le retrait prend des mois. N'ajoutez preload que lorsque vous êtes certain que chaque sous-domaine servira toujours du HTTPS.
Après rechargement :
curl -sI https://your-domain.com | grep -i strict
strict-transport-security: max-age=63072000; includeSubDomains
Quels en-têtes de sécurité chaque serveur Nginx devrait-il avoir ?
Cinq en-têtes de réponse protègent contre les attaques courantes côté navigateur : le sniffing MIME, le clickjacking, le cross-site scripting, la fuite du referrer et l'abus des API navigateur. Ajoutez les cinq avec le flag always pour qu'ils s'appliquent aussi aux réponses d'erreur. L'en-tête déprécié X-XSS-Protection ne doit pas être inclus. Les navigateurs modernes ont supprimé leurs auditeurs XSS, et l'en-tête lui-même introduisait des vulnérabilités dans certains cas.
Ajoutez dans /etc/nginx/snippets/security-hardening.conf :
# Prevent MIME type sniffing - stops browsers from interpreting files as a
# different content type than declared, blocking drive-by download attacks
add_header X-Content-Type-Options "nosniff" always;
# Clickjacking protection - prevents your pages from being embedded in
# iframes on other sites
add_header X-Frame-Options "SAMEORIGIN" always;
# Content Security Policy - controls which sources can load scripts, styles,
# images, and other resources. Start restrictive, loosen as needed.
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
# Referrer Policy - controls how much URL information the browser sends
# when navigating away from your site
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Permissions Policy - disables browser features you don't use, preventing
# compromised scripts from accessing camera, microphone, etc.
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;
interest-cohort=() apparaissait dans les anciens guides pour bloquer Google FLoC. FLoC a été abandonné en 2023. Les navigateurs modernes ne reconnaissent pas cette directive et affichent des avertissements dans la console si vous l'incluez.
| En-tête | Valeur | Menace atténuée |
|---|---|---|
| X-Content-Type-Options | nosniff |
Attaques par confusion MIME, téléchargements furtifs |
| X-Frame-Options | SAMEORIGIN |
Clickjacking via iframe |
| Content-Security-Policy | default-src 'self' |
XSS, injection de données, chargement de ressources non autorisé |
| Referrer-Policy | strict-origin-when-cross-origin |
Fuite d'URL vers des tiers |
| Permissions-Policy | camera=(), microphone=()... |
Abus des API navigateur par des scripts injectés |
Ajustement CSP : L'exemple ci-dessus est strict. La plupart des applications nécessitent des ajustements. Si votre site charge des scripts depuis un CDN, ajoutez-le : script-src 'self' https://cdn.example.com;. Utilisez la console développeur de votre navigateur pour trouver les violations CSP et assouplir la politique progressivement. N'utilisez jamais unsafe-eval sans comprendre le risque.
Le piège de l'héritage add_header
Nginx supprime toutes les directives add_header des blocs parents quand un bloc location enfant définit son propre add_header. Si vous ajoutez un en-tête dans un bloc location, tous les en-têtes de sécurité du niveau server ou http disparaissent des réponses de cette location.
Deux solutions :
- Nginx 1.29.3+ : Utilisez
add_header_inherit merge;dans le bloc enfant pour conserver les en-têtes parents. - Versions antérieures : Répétez la ligne
include /etc/nginx/snippets/security-hardening.conf;dans chaque bloc location qui ajoute ses propres en-têtes.
Rechargez et vérifiez :
sudo nginx -t && sudo systemctl reload nginx
curl -sI https://your-domain.com | grep -iE "x-content-type|x-frame|content-security|referrer-policy|permissions-policy"
Les cinq en-têtes apparaissent dans la sortie. Vérifiez aussi un chemin location spécifique, pas seulement la racine, pour détecter les problèmes d'héritage.
Comment désactiver les méthodes HTTP non sécurisées dans Nginx ?
La plupart des sites n'ont besoin que de GET, HEAD et POST. Les méthodes comme DELETE, PUT, TRACE et OPTIONS (quand elles ne sont pas utilisées) élargissent votre surface d'attaque. TRACE en particulier peut divulguer les cookies et les tokens d'authentification via les attaques XST (cross-site tracing).
L'approche la plus propre dans Nginx standard est limit_except, conçue spécifiquement pour la restriction des méthodes. Placez-la dans vos blocs location :
location / {
limit_except GET POST {
deny all;
}
# ... your existing config
}
limit_except GET autorise implicitement HEAD (HEAD est un GET sans corps selon la spécification HTTP). Toute méthode non listée retourne un 403 Forbidden.
Pour une approche globale avec map (ajoutez dans nginx.conf dans le bloc http) :
map $request_method $method_not_allowed {
default 1;
GET 0;
HEAD 0;
POST 0;
}
Puis dans votre bloc server :
if ($method_not_allowed) {
return 405;
}
L'approche limit_except est préférée quand vous avez besoin d'un contrôle par location. L'approche map convient quand vous voulez une politique globale sur l'ensemble du serveur.
Testez avec une méthode bloquée :
curl -sI -X DELETE https://your-domain.com
Retourne HTTP/1.1 403 Forbidden ou HTTP/1.1 405 Not Allowed.
curl -sI -X GET https://your-domain.com
Retourne HTTP/1.1 200 OK (ou votre code de réponse habituel).
Comment restreindre l'accès aux chemins d'administration par adresse IP ?
Les panneaux d'administration, tableaux de bord de monitoring et endpoints de statut ne devraient pas être accessibles depuis l'internet public. Les directives allow et deny de Nginx restreignent l'accès par adresse IP au niveau location.
location /admin {
allow 203.0.113.10; # Your office IP
allow 198.51.100.0/24; # Your VPN range
deny all;
# ... proxy_pass or other directives
}
location /nginx-status {
stub_status;
allow 127.0.0.1;
allow ::1;
deny all;
}
L'ordre compte. Nginx évalue allow et deny de haut en bas et s'arrête à la première correspondance. Placez deny all en dernier.
Si votre IP change fréquemment, envisagez de restreindre l'accès via votre pare-feu ou utilisez un VPN. Les ACL par IP dans Nginx sont une seconde couche de défense, pas un remplacement de l'authentification.
Depuis une IP autorisée :
curl -sI https://your-domain.com/admin
Retourne votre réponse habituelle (200, 302, etc.).
Depuis une autre IP (ou via un proxy), la même requête retourne HTTP/1.1 403 Forbidden.
Consultez le log d'accès pour les requêtes refusées :
sudo tail -5 /var/log/nginx/access.log | grep admin
Quels réglages de buffers et timeouts préviennent les attaques DoS dans Nginx ?
Les tailles de buffers et les timeouts par défaut sont généreux. Un attaquant peut en abuser en envoyant des en-têtes volumineux, des requêtes lentes ou des corps surdimensionnés pour monopoliser les connexions worker. Resserrer ces valeurs limite les dégâts qu'une seule connexion peut causer.
Ajoutez dans /etc/nginx/snippets/security-hardening.conf :
# Maximum allowed request body size - reject uploads larger than 10MB
client_max_body_size 10m;
# Buffer for reading client request body
client_body_buffer_size 16k;
# Buffer for reading large client headers
large_client_header_buffers 4 16k;
# Timeouts - how long Nginx waits for client data
client_body_timeout 30s;
client_header_timeout 30s;
send_timeout 30s;
keepalive_timeout 65s;
| Directive | Valeur | Trop élevée | Trop basse |
|---|---|---|---|
client_max_body_size |
10m |
Autorise des uploads massifs qui remplissent le disque | Casse les formulaires d'upload |
client_body_buffer_size |
16k |
Gaspillage mémoire par connexion | Force des fichiers temporaires pour les petits POST |
large_client_header_buffers |
4 16k |
Gaspillage mémoire sous attaque d'en-têtes | Casse les applis avec gros cookies/URLs |
client_body_timeout |
30s |
Les connexions slow loris restent ouvertes | Coupe les utilisateurs sur réseaux lents |
client_header_timeout |
30s |
Idem | Idem |
send_timeout |
30s |
Monopolise les connexions pour les clients lents | Coupe les gros téléchargements |
keepalive_timeout |
65s |
Épuisement du pool de connexions | Handshakes TLS supplémentaires |
Ajustez client_max_body_size selon votre application. Si vous gérez des uploads de fichiers, réglez-le selon ce que votre application attend. Un défaut de 10 Mo est raisonnable pour la plupart des applications web.
Rechargez et testez la limite de taille du corps :
dd if=/dev/zero bs=1M count=11 2>/dev/null | curl -s -X POST --data-binary @- -o /dev/null -w "%{http_code}" https://your-domain.com/
Résultat attendu : 413 (Request Entity Too Large).
Pour le rate limiting et une protection DDoS plus poussée, consultez Limiter le débit avec Nginx et se protéger contre les DDoS.
Comment tester la configuration de durcissement complète ?
Une fois toutes les modifications en place, rechargez et passez en revue les points clés. Commencez par un test de configuration et un rechargement :
sudo nginx -t && sudo systemctl reload nginx
Vérifiez les en-têtes et les informations de version avec une seule requête :
curl -sI https://your-domain.com
L'en-tête Server devrait afficher nginx sans numéro de version. Les six en-têtes de sécurité (HSTS, X-Content-Type-Options, X-Frame-Options, Content-Security-Policy, Referrer-Policy, Permissions-Policy) devraient apparaître dans la réponse.
Pour TLS, confirmez le protocole et le chiffrement depuis votre machine locale :
openssl s_client -connect your-domain.com:443 </dev/null 2>/dev/null | grep -E "Protocol|Cipher"
Vous voulez TLSv1.2 ou TLSv1.3 avec un chiffrement GCM ou CHACHA20.
Accédez directement à l'IP du serveur pour confirmer que le bloc de rejet par défaut fonctionne :
curl -sk https://YOUR_SERVER_IP -o /dev/null -w "%{http_code}"
Un code de réponse 000 signifie que la connexion a été fermée sans réponse, ce qui est correct.
Essayez une méthode HTTP bloquée :
curl -sI -X TRACE https://your-domain.com -o /dev/null -w "%{http_code}"
Cela devrait retourner 403 ou 405.
Pour un audit externe, soumettez votre domaine à SSL Labs (visez un grade A ou A+) et securityheaders.com pour un rapport spécifique aux en-têtes.
Snippet de durcissement complet
Le fichier /etc/nginx/snippets/security-hardening.conf complet avec tout ce guide :
# /etc/nginx/snippets/security-hardening.conf
# Nginx security hardening - include in each server block
# --- TLS Hardening (Mozilla Intermediate profile v5.7) ---
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305';
ssl_prefer_server_ciphers off;
ssl_dhparam /etc/nginx/dhparam.pem;
# TLS session settings
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
# --- HSTS ---
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
# --- Security Headers ---
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;
# --- Buffer Limits ---
client_max_body_size 10m;
client_body_buffer_size 16k;
large_client_header_buffers 4 16k;
# --- Timeouts ---
client_body_timeout 30s;
client_header_timeout 30s;
send_timeout 30s;
keepalive_timeout 65s;
N'oubliez pas de configurer aussi server_tokens off; dans le bloc http de /etc/nginx/nginx.conf et de créer le bloc server de rejet par défaut séparément.
Dépannage
Nginx refuse de recharger après des modifications :
sudo nginx -t
Lisez l'erreur. Elle indique le fichier et le numéro de ligne. Causes courantes : points-virgules manquants, fautes de frappe dans les noms de directives, référence à un fichier DH param inexistant.
En-têtes manquants sur certains chemins :
Le piège de l'héritage add_header. Si un bloc location a son propre add_header, tous les en-têtes parents sont supprimés. Incluez le snippet dans ce bloc location ou utilisez add_header_inherit merge; sur Nginx 1.29.3+.
CSP bloque des ressources légitimes :
Ouvrez la console développeur du navigateur (F12). Cherchez les erreurs Refused to load. Elles nomment la ressource bloquée et la directive CSP responsable. Ajoutez la source à votre politique progressivement.
Échecs de handshake TLS :
journalctl -u nginx -f
Surveillez les erreurs SSL pendant la connexion. Vérifiez que votre chaîne de certificats est complète :
openssl s_client -connect your-domain.com:443 -servername your-domain.com </dev/null 2>/dev/null | grep "Verify return code"
Résultat attendu : Verify return code: 0 (ok).
Emplacement des logs :
# Error log
sudo tail -20 /var/log/nginx/error.log
# Real-time monitoring
sudo journalctl -u nginx -f
Prochaines étapes : Le serveur étant durci, configurez le Limiter le débit avec Nginx et se protéger contre les DDoS pour gérer les schémas de trafic abusifs. Pour les configurations par site, consultez Nginx Server Blocks : héberger plusieurs domaines sur un VPS.
Articles liés : Administration Nginx sur un VPS | Structure des fichiers de configuration Nginx | Configurer Let's Encrypt SSL/TLS pour Nginx sur Debian 12 et Ubuntu 24.04