NginxSécuritéIntermédiaireLinuxAdministration systèmeHébergement web

Nginx en reverse proxy avec Let's Encrypt SSL

Jerry RajaonarivoMis à jour le 28 sept. 20268 min de lecture

Ceci met en place un reverse proxy Nginx qui transmet correctement la requête d'origine au backend, plus un TLS Let's Encrypt avec un renouvellement automatique réellement vérifié.

Le bloc serveur du reverse proxy

Le rôle du proxy est de terminer la connexion avec le client, d'en ouvrir une seconde vers votre backend, et de dire à ce backend la vérité sur la première.

nginx
upstream app_backend {
    server 127.0.0.1:3000;
    keepalive 32;
}
 
server {
    listen 80;
    server_name app.example.com;
 
    location / {
        proxy_pass http://app_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
 
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
 
        proxy_connect_timeout 5s;
        proxy_read_timeout 60s;
    }
}

X-Forwarded-For donne au backend la véritable chaîne d'IP du client. Utilisez $proxy_add_x_forwarded_for, pas $remote_addr, pour qu'il s'ajoute à la valeur existante au lieu de l'écraser s'il y a déjà un proxy en amont de celui-ci. X-Forwarded-Proto est ce que la plupart des frameworks vérifient pour savoir si la requête d'origine était en HTTPS. Sans lui, une application derrière un proxy qui termine le TLS croit être toujours en HTTP simple, et tout middleware de "redirection vers https" tourne en boucle. Host compte aussi : de nombreux backends routent ou valident directement en fonction de cette valeur.

Si vous omettez proxy_set_header Host $host et que le backend utilise la valeur par défaut, l'ALLOWED_HOSTS de Django, l'autorisation d'hôte de Rails et la génération d'URL absolues de Next.js vont toutes casser d'une manière qui ressemble à un bug applicatif.

Obtenir un certificat avec certbot

J'utilise le plugin Nginx de certbot parce qu'il modifie directement vos blocs serveur existants (en ajoutant la directive listen 443 ssl, les chemins vers les certificats et la redirection), plutôt qu'une configuration séparée à réconcilier à la main.

text
sudo apt install certbot python3-certbot-nginx
 
sudo certbot --nginx -d app.example.com

Chaque nom passé en -d doit être servi par le server_name et avoir un DNS qui pointe vers cet hôte. Si vous voulez aussi www.app.example.com, ajoutez-le d'abord au server_name, puis passez un second -d. Certbot demande une adresse e-mail de compte (pour la récupération du compte et les annonces importantes), puis valide la propriété du domaine en HTTP sur le port 80 et obtient le certificat. Ne comptez pas sur cet e-mail pour être prévenu de l'expiration. Let's Encrypt a arrêté d'envoyer ses rappels d'expiration par e-mail en 2025, c'est donc à vous de surveiller l'expiration (voir plus bas). Faites ensuite un nginx -t et relisez le diff de la configuration pour savoir exactement ce qui a changé.

Redirection HTTP vers HTTPS et HSTS

L'option de redirection de certbot fait l'essentiel du travail, mais j'ajoute toujours HSTS explicitement plutôt que de me fier aux valeurs par défaut, c'est cet en-tête qui empêche une attaque de downgrade de prendre pied.

Vérifiez d'abord votre version de Nginx, car la syntaxe HTTP/2 en dépend :

text
nginx -v
# nginx version: nginx/1.24.0   <- apt sur Ubuntu 24.04 ; apt sur Debian 12 fournit la 1.22

La directive autonome http2 on; n'existe qu'à partir de Nginx 1.25.1. Sur une version antérieure, nginx -t la rejette. Utilisez alors la forme listen 443 ssl http2; indiquée en commentaire (les versions récentes l'acceptent encore, avec un avertissement de dépréciation). Si vous préférez une version à jour, les dépôts nginx.org fournissent les branches mainline et stable pour ces deux distributions.

nginx
server {
    listen 80;
    server_name app.example.com;
    return 301 https://$host$request_uri;
}
 
server {
    listen 443 ssl;
    http2 on;                  # Nginx >= 1.25.1
    # listen 443 ssl http2;    # Nginx < 1.25.1 : cette ligne à la place des deux précédentes
    server_name app.example.com;
 
    ssl_certificate     /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
 
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
 
    location / {
        proxy_pass http://app_backend;
        # Indispensable pour que le "keepalive 32" de l'upstream réutilise vraiment les connexions
        proxy_http_version 1.1;
        proxy_set_header Connection "";
 
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

includeSubDomains s'applique à tous les sous-domaines. Une fois l'en-tête vu, le navigateur refuse le HTTP simple vers *.app.example.com pendant toute la durée de max-age (deux ans ici). Si un sous-domaine ne sert encore que du HTTP, retirez includeSubDomains, ou commencez avec un max-age court, par exemple 300, et augmentez-le une fois que vous avez vérifié que tout fonctionne en HTTPS.

N'ajoutez pas preload à l'en-tête HSTS tant que chaque sous-domaine que vous possédez ne peut pas servir du HTTPS indéfiniment. Entrer dans la liste de préchargement des navigateurs est facile, en sortir prend des mois, et entre-temps tout sous-domaine incapable de faire du TLS devient inaccessible.

Le renouvellement automatique, mais vérifié

Les certificats Let's Encrypt durent 90 jours. Le timer systemd de certbot renouvelle tout certificat à moins de 30 jours de son expiration, deux fois par jour, jusqu'à ce qu'une mise à jour du système change un chemin, qu'une règle de pare-feu bloque la validation sur le port 80, ou qu'une modification de configuration casse le bloc de location du challenge ACME.

text
systemctl list-timers | grep certbot
 
sudo systemctl status certbot.timer

L'unité livrée par certbot ressemble grosso modo à ceci :

ini
# /lib/systemd/system/certbot.timer
[Unit]
Description=Run certbot twice daily
 
[Timer]
OnCalendar=*-*-* 00,12:00:00
RandomizedDelaySec=43200
Persistent=true
 
[Install]
WantedBy=timers.target

La commande que j'exécute sur chaque serveur, immédiatement, avant de faire confiance au dispositif :

text
sudo certbot renew --dry-run --run-deploy-hooks

--dry-run exécute le chemin de renouvellement (validation ACME et émission du certificat) contre l'environnement de test de Let's Encrypt, sans toucher à votre vrai certificat ni à ses quotas. Par défaut, il saute les deploy hooks, donc un --deploy-hook cassé (copie des certificats ailleurs, rechargement d'un autre service) passerait inaperçu. Ajoutez --run-deploy-hooks pour les exécuter après un dry run réussi. Cette option demande certbot 2.7 ou plus. Le paquet d'Ubuntu 24.04 l'a, l'ancien paquet apt de Debian 12 non, et le snap certbot est à jour. Si le dry run échoue, votre vrai renouvellement échouera aussi, simplement plus tard. Je le lance une fois à la mise en place, puis après chaque modification du pare-feu, de la configuration Nginx ou du DNS du serveur.

Branchez l'expiration des certificats sur votre supervision au lieu de vous fier au seul timer, d'autant que Let's Encrypt n'envoie plus d'alertes d'expiration par e-mail. Mettez en place une sonde externe qui vérifie la date d'expiration du certificat sur le endpoint en production (probe_ssl_earliest_cert_expiry du Blackbox exporter, ou le contrôle TLS de votre outil d'uptime) et qui alerte vers 14 jours restants. Quelqu'un sera ainsi prévenu avant qu'un client ne tombe sur un certificat expiré.

Durcissement de base : protocoles, chiffrements et en-têtes de sécurité

Une fois la terminaison TLS et le renouvellement solides, la dernière passe consiste à restreindre ce que le proxy accepte et à ajouter quelques en-têtes de sécurité.

nginx
# Réglages TLS : valables dans le bloc http {} (partagés par tous les serveurs) ou dans le bloc server 443
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
 
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
 
# En-têtes de sécurité : à placer dans le bloc server 443, à côté du add_header HSTS
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

L'emplacement des lignes add_header compte. Nginx n'hérite des add_header d'un niveau supérieur que si le niveau courant n'en définit aucun. Si vous mettez ces trois lignes dans http {}, le bloc server 443 (qui a son propre add_header HSTS) les ignore toutes, sans le moindre avertissement. Gardez tous les add_header dans le même bloc server, et n'oubliez pas qu'une location avec son propre add_header les réinitialise à nouveau. Nginx 1.29.3 a ajouté add_header_inherit merge; pour changer ce comportement, mais la plupart des paquets de distribution sont bien plus anciens, donc n'en dépendez pas encore.

Abandonner TLS 1.0 et 1.1 ne pose aucun problème pour un site grand public en 2026, les clients encore bloqués dessus représentent un risque de sécurité, quoi qu'il arrive. ssl_prefer_server_ciphers off laisse les clients modernes choisir leur chiffrement préféré, la meilleure pratique actuelle maintenant que les choix côté client sont généralement fiables. C'était l'inverse il y a quelques années, un bon rappel de revérifier les guides de durcissement tous les deux ou trois ans plutôt que de figer une configuration pour toujours.

Passez le résultat au test SSL Labs de Qualys ou à testssl.sh après chaque modification de ce bloc : cela permet de repérer les directives qui ne font pas ce que vous croyez, ou un ancien fragment de configuration ailleurs dans le fichier qui l'écrase silencieusement.

Ce sont les huit ou neuf mêmes directives à intégrer dans presque chaque configuration de proxy. Les avoir, c'est la différence entre une configuration TLS qu'on oublie et une qui revient un an plus tard sous forme de rapport d'incident.

ALLER PLUS LOIN

Envie de faire tourner ça en production ?

Ce tutoriel couvre les concepts et l'architecture. Si vous voulez l'implémenter dans votre propre infrastructure, ou monter en compétence pour posséder ce sujet durablement, je propose du mentorat individuel construit autour de votre environnement réel, pas une formation générique.

Ce tutoriel

  • Concepts clés et architecture principale
  • Extraits de code illustratifs
  • Le raisonnement derrière chaque décision

Mentorat individuel

  • Des sessions de travail sur votre propre environnement
  • Des réponses directes aux cas particuliers que vous rencontrez
  • Un retour sur votre implémentation réelle
  • Un accompagnement continu pendant que vous la construisez
Me contacterRéponse sous 1 jour ouvré.

Tutoriels similaires