Servir HTTPS proprement : vhost isolé, Let's Encrypt automatisé, headers de sécurité, pièges du default_server, rate-limiting.
Sur Debian/Ubuntu, nginx propose un pattern sites-available / sites-enabled qu'il faut suivre pour rester maintenable.
/etc/nginx/
├── nginx.conf # config globale
├── sites-available/ # définitions vhosts (toutes)
│ ├── default
│ ├── formation-odoo
│ └── formation-secu
└── sites-enabled/ # symlinks vers ceux qui sont actifs
├── default → ../sites-available/default
└── formation-odoo → ../sites-available/formation-odoo
# Activer
sudo ln -s /etc/nginx/sites-available/mon-site /etc/nginx/sites-enabled/
# Désactiver (le fichier source reste disponible)
sudo rm /etc/nginx/sites-enabled/mon-site
# Tester + reload (TOUJOURS dans cet ordre)
sudo nginx -t && sudo systemctl reload nginx
Si la config a une erreur de syntaxe, nginx -t vous l'indique avant qu'elle ne casse votre nginx. Sans le test, vous risquez de couper tous vos sites jusqu'à correction.
Dans un nginx avec plusieurs vhosts, il y a TOUJOURS un vhost qui répond aux requêtes dont le server_name ne matche aucun bloc explicite. C'est le default_server.
On déploie un nouveau site formation.odoo.ezway.technology avec :
formation-odoo activé dans sites-enabled//var/www/formation-odoo/Symptôme : on accède au domaine et... page "Welcome to nginx" par défaut. Pourquoi ?
Diagnostic : Certbot, lors d'un premier passage, a injecté les directives SSL dans le fichier /etc/nginx/sites-available/default (déjà activé), et pas dans notre vhost. Du coup, le default matche notre server_name avant nous, et sert /var/www/html/index.nginx-debian.html.
# 1. Désactiver le default (le symlink, le fichier source reste)
sudo rm /etc/nginx/sites-enabled/default
# 2. Vérifier que NOTRE vhost a bien les blocs 80 ET 443
sudo grep -E "listen|server_name" /etc/nginx/sites-available/formation-odoo
# 3. Si le bloc 443 manque (Certbot a fait son job ailleurs), relancer
sudo certbot install --cert-name formation.odoo.ezway.technology
# 4. Reload
sudo nginx -t && sudo systemctl reload nginx
# 5. Tester
curl -I https://formation.odoo.ezway.technology/
Sur un VPS multi-sites, désactivez systématiquement le default. Mieux vaut un 404 propre sur un domaine non configuré qu'un risque de fuite (welcome page, ou pire, mauvais site servi).
Voici le template à reprendre pour tout nouveau site statique HTTPS :
server {
listen 80;
listen [::]:80;
server_name mon-site.example.com;
# Permettre les challenges ACME pour Certbot
location ^~ /.well-known/acme-challenge/ {
root /var/www/html;
default_type "text/plain";
}
# Tout le reste : redirect vers HTTPS
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name mon-site.example.com;
ssl_certificate /etc/letsencrypt/live/mon-site.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mon-site.example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
root /var/www/mon-site;
index index.html;
# Headers de sécurité
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
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;
# Content Security Policy à adapter selon votre app
# add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline';" always;
# Cacher la version nginx
server_tokens off;
# Limites
client_max_body_size 5M;
location / {
try_files $uri $uri/ $uri.html =404;
}
# Bloquer fichiers cachés (sauf .well-known)
location ~ /\.(?!well-known) {
deny all;
access_log off;
log_not_found off;
}
# Logs séparés (essentiel pour le multi-sites)
access_log /var/log/nginx/mon-site.access.log;
error_log /var/log/nginx/mon-site.error.log;
# Compression
gzip on;
gzip_types text/css application/json application/javascript text/xml application/xml image/svg+xml;
}
| Header | Rôle | Valeur type |
|---|---|---|
Strict-Transport-Security | Force HTTPS pendant N secondes (cache navigateur) | max-age=31536000; includeSubDomains |
X-Content-Type-Options | Empêche MIME sniffing (exécution JS dans fichier servi en text/plain) | nosniff |
X-Frame-Options | Empêche clickjacking (iframe d'un autre site sur votre page) | SAMEORIGIN ou DENY |
Referrer-Policy | Contrôle ce qui est envoyé dans le header Referer | strict-origin-when-cross-origin |
Content-Security-Policy | Whitelist sources scripts/styles/img/iframes — anti-XSS | default-src 'self'; ... |
Permissions-Policy | Désactive features browser (camera, mic, geoloc) non utilisées | camera=(), microphone=(), geolocation=() |
Content-Security-Policy est la défense XSS la plus efficace, mais aussi la plus dure à configurer (chaque CDN, chaque libairie externe doit être whitelistée). Commencez en mode Report-Only pour voir ce qui casse avant d'appliquer en bloquant.
sudo apt install certbot python3-certbot-nginx -y
# Option A : Certbot configure nginx automatiquement (plus simple)
sudo certbot --nginx -d mon-site.example.com
# Option B : juste émettre, on configure manuellement le bloc 443 (plus contrôlé)
sudo certbot certonly --nginx -d mon-site.example.com
Certbot installe automatiquement un timer systemd (/etc/systemd/system/timers.target.wants/certbot.timer) qui tente le renouvellement 2× / jour. Les certs ne sont renouvelés que s'ils expirent dans moins de 30 jours.
# Vérifier
sudo systemctl status certbot.timer
# Tester le renouvellement (dry-run)
sudo certbot renew --dry-run
Pour couvrir *.zoho.ezway.technology avec un seul cert, il faut le challenge DNS-01 (HTTP-01 ne supporte pas les wildcards) :
# Via Cloudflare (exemple)
sudo apt install python3-certbot-dns-cloudflare -y
# Créer le fichier credentials
echo "dns_cloudflare_api_token = VOTRE_TOKEN_API" | sudo tee /etc/letsencrypt/cloudflare.ini
sudo chmod 600 /etc/letsencrypt/cloudflare.ini
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
-d "*.zoho.ezway.technology" \
-d "zoho.ezway.technology"
50 certificats / semaine / registered domain. Si vous multipliez les sous-domaines en série, attention au plafond. Solution : wildcard (1 cert pour tous les sous-domaines) ou utiliser --staging pour les tests (ne consomme pas le quota prod).
# Dans http {} (nginx.conf) ou un fichier dans conf.d/
limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=60r/m;
# Dans le vhost :
location /wp-login.php {
limit_req zone=auth_limit burst=3 nodelay;
# ...
}
location /api/ {
limit_req zone=api_limit burst=20;
# ...
}
# Installer htpasswd
sudo apt install apache2-utils -y
# Créer le fichier de credentials
sudo htpasswd -c /etc/nginx/.htpasswd francois
# (entrer un password fort)
# Ajouter d'autres users
sudo htpasswd /etc/nginx/.htpasswd autre_user
# Dans le vhost
location /admin/ {
auth_basic "Admin Area";
auth_basic_user_file /etc/nginx/.htpasswd;
}
Basic Auth envoie le password en base64 à chaque requête. C'est OK uniquement en HTTPS. Et c'est une protection pénible (pas de logout, pas de 2FA). Utilisez pour du staging/temporaire, jamais pour un vrai login utilisateur final.