Certbot Ubuntu 24.04 : nginx + Let's Encrypt
Certbot sur Ubuntu 24.04 avec apt ou snap : un certificat Let's Encrypt pour nginx, plus les blocages du port 80 qui cassent le renouvellement HTTP-01.
Installer Certbot : apt ou snap
Sur Ubuntu 24.04, sudo apt install certbot python3-certbot-nginx vous donne un Certbot fonctionnel qui émet de vrais certificats Let's Encrypt reconnus publiquement. La documentation amont de Certbot vous oriente plutôt vers le snap ; la différence est mince — le snap suit les versions amont, le paquet de l'archive suit ce qui a été livré avec la LTS et reçoit des correctifs de sécurité.
Choisissez-en un. Deux copies de Certbot, ce sont deux timers de renouvellement pointés sur la même arborescence /etc/letsencrypt, et celui que vous avez oublié est celui qui vous surprend.
La voie apt :
sudo apt update
sudo apt install certbot python3-certbot-nginxCela installe /usr/bin/certbot, le plugin nginx, une paire certbot.service + certbot.timer, et une entrée /etc/cron.d/certbot qui ne fait rien sous systemd.
La voie snap :
sudo apt remove certbot python3-certbot-nginx
sudo snap install core && sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbotLe snap embarque son propre timer, snap.certbot.renew.timer. Supprimez le paquet apt avant d'installer le snap.
Les deux installations se comportent ensuite de la même façon. Certbot 2.x utilise par défaut des clés ECDSA (P-256) — ne passez --key-type rsa que pour un client incapable de faire de l'ECDSA. Tout l'état vit sous /etc/letsencrypt : archive/ contient les vrais fichiers de clé et de certificat, live/ fait des liens symboliques vers les fichiers courants, renewal/ un fichier de configuration par certificat, accounts/ votre clé de compte ACME.
Ce que fait réellement HTTP-01, et pourquoi le port 80 n'est pas optionnel
Le défi HTTP-01 est un rappel (callback). Vous demandez à Let's Encrypt un certificat couvrant example.com ; il résout le nom dans le DNS public, ouvre une connexion vers le port 80 à l'adresse qu'il trouve, et demande http://example.com/.well-known/acme-challenge/<token>. Votre serveur répond avec le contenu exact du token que Certbot vient d'écrire sur le disque. C'est tout le mécanisme. Trois conséquences en découlent, et elles expliquent la plupart des émissions ratées.
- Le port 80 doit être joignable depuis l'internet public, pas seulement depuis votre ordinateur portable. Une règle
ufw, un groupe de sécurité chez le fournisseur cloud, ou un pare-feu de console VPS qui n'ouvre que le 443 casse l'émission et, avec elle, tout renouvellement futur. - Le DNS doit déjà pointer vers cette machine. Le serveur de validation fait sa propre résolution depuis l'extérieur ; vos entrées
/etc/hostset le cache de votre navigateur ne comptent pour rien à ses yeux. - Si vous publiez un enregistrement AAAA, l'IPv6 est essayée en premier. Let's Encrypt réessaie en IPv4 quand la connexion IPv6 échoue franchement — mais un AAAA périmé pointant vers un hôte qui accepte la connexion et sert autre chose vous donne un échec net.
Les redirections sont autorisées : la validation suit une redirection HTTP vers HTTPS et se moque que le certificat à l'autre bout soit absent, expiré ou auto-signé. Ce qu'elle ne fera pas, c'est démarrer ailleurs que sur le port 80. Certbot n'a aucune implémentation TLS-ALPN-01, donc « utilisez juste le 443 » n'est pas une porte de sortie.
Choisir un authentificateur : --nginx, --webroot, --standalone
--nginx est le bon choix par défaut quand nginx tourne déjà et sert déjà le domaine. Certbot analyse votre configuration, injecte un emplacement de défi temporaire, recharge nginx, valide, puis écrit les directives TLS dans votre bloc serveur. Aucune interruption.
sudo certbot --nginx -d example.com -d www.example.comEn version scriptée, pour une machine neuve :
sudo certbot --nginx \
-d example.com -d www.example.com \
--agree-tos -m ops@example.com --no-eff-email \
--redirect --non-interactive--webroot est le bon choix quand vous voulez tenir Certbot loin de votre configuration nginx — une configuration que vous générez depuis un modèle, gardez dans git, ou poussez avec Ansible. Certbot n'écrit que le fichier de défi, dans un répertoire que vous servez déjà.
sudo certbot certonly --webroot -w /var/www/example.com \
-d example.com -d www.example.com \
--deploy-hook "systemctl reload nginx"--standalone est le bon choix quand rien n'écoute sur le port 80 : un serveur de messagerie, une API qui ne parle que le 443, un script de premier démarrage qui s'exécute avant que nginx n'existe. Certbot lie lui-même le port 80 pendant quelques secondes. Si nginx tourne, cela échoue — arrêtez-le autour de l'exécution :
sudo certbot certonly --standalone -d mail.example.com \
--pre-hook "systemctl stop nginx" \
--post-hook "systemctl start nginx"Ces hooks sont enregistrés dans la configuration de renouvellement du certificat, donc le même arrêt/démarrage se produit sans intervention au moment du renouvellement.
Un bloc serveur qui fonctionne avant et après l'existence du certificat
Le problème de l'œuf et de la poule : nginx refuse de démarrer avec ssl_certificate pointant vers un fichier qui n'existe pas, et Certbot ne peut pas valider tant que nginx est arrêté. Montez d'abord le site sur le port 80.
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example.com;
index index.html;
location ^~ /.well-known/acme-challenge/ {
root /var/www/example.com;
default_type "text/plain";
try_files $uri =404;
}
location / {
try_files $uri $uri/ =404;
}
}Exécutez sudo nginx -t && sudo systemctl reload nginx, confirmez que curl -I http://example.com/ répond depuis l'extérieur de la machine, puis émettez. Ensuite :
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
location ^~ /.well-known/acme-challenge/ {
root /var/www/example.com;
default_type "text/plain";
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
root /var/www/example.com;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}Le préfixe ^~ sur l'emplacement ACME gagne sa place : il empêche le bloc return 301 d'avaler la requête de défi. Garder cet emplacement sur le port 80 fait que les renouvellements continuent de fonctionner une fois le reste du site passé en HTTPS uniquement.
La syntaxe HTTP/2 dépend de votre version de nginx, et mélanger les deux formes produit une erreur au démarrage. Ubuntu 24.04 livre nginx 1.24, qui la veut en ligne — listen 443 ssl http2;. Debian 13 livre un nginx plus récent, qui veut la directive séparée http2 on;. Vérifiez d'abord nginx -v.
Pointez nginx vers live/, jamais vers archive/. Les liens symboliques de live/ sont repointés à chaque renouvellement ; un chemin en dur vers archive/ vous fige sur un certificat qui expire sous vos pieds.
Les wildcards imposent DNS-01, et DNS-01 impose un plugin
Un certificat wildcard (*.example.com) ne peut pas être validé par HTTP-01 — il n'y a aucun nom d'hôte unique depuis lequel récupérer un fichier. DNS-01 est la seule voie : vous prouvez le contrôle en publiant un enregistrement TXT _acme-challenge.example.com. Certbot a besoin d'identifiants d'API pour votre fournisseur DNS afin de le faire sans intervention, et c'est précisément à cela que servent les plugins de fournisseur. Le guide complet du certificat wildcard couvre la mécanique de l'enregistrement TXT et le piège du renouvellement en mode manuel ; la version courte pour Cloudflare suit.
sudo snap set certbot trust-plugin-with-root=ok
sudo snap install certbot-dns-cloudflareSur la voie apt, c'est plutôt sudo apt install python3-certbot-dns-cloudflare. Les identifiants vont dans un fichier réservé à root :
# /root/.secrets/cloudflare.ini
# then: sudo chmod 600 /root/.secrets/cloudflare.ini
dns_cloudflare_api_token = your_scoped_token_hereLimitez la portée du token aux droits d'édition DNS sur cette seule zone. C'est une clé de votre DNS ; traitez-la comme telle.
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /root/.secrets/cloudflare.ini \
-d example.com -d '*.example.com'Mettez le wildcard entre guillemets pour que votre shell ne le développe pas (glob). DNS-01 résout aussi ce que HTTP-01 ne peut pas : des certificats pour des hôtes sans port 80 public — un service interne, une machine joignable uniquement via un VPN WireGuard auto-hébergé sur un VPS, un panneau d'administration sur une interface privée.
Renouvellement : les 90 jours, le timer, le hook de déploiement
Les certificats Let's Encrypt sont valides 90 jours. Certbot renouvelle quand il reste moins de 30 jours, ce qui vous donne une fenêtre de 30 jours pendant laquelle un renouvellement cassé est un désagrément réparable plutôt qu'une panne. Let's Encrypt n'envoie plus d'e-mails d'avertissement d'expiration — personne ne va vous taper sur l'épaule, donc la surveillance vous revient désormais.
Vérifiez le timer que votre installation a livré :
systemctl list-timers 'certbot*' 'snap.certbot*'
sudo certbot certificatescertbot renew parcourt chaque configuration de /etc/letsencrypt/renewal/, ignore tout ce qui est hors de la fenêtre des 30 jours, et renouvelle le reste en utilisant exactement les options de l'exécution d'origine. Voilà pourquoi la première exécution compte : c'est celle qui est enregistrée.
Renouveler le fichier sur le disque ne change rien en soi — nginx continue de servir l'ancien certificat depuis la mémoire jusqu'à ce que quelque chose lui dise de recharger. Câblez un hook de déploiement une fois pour toutes :
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh >/dev/null <<'EOF'
#!/bin/sh
set -e
nginx -t && systemctl reload nginx
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.shTout exécutable présent dans renewal-hooks/deploy/ s'exécute après tout renouvellement réussi. L'option --deploy-hook fait le même travail pour un seul certificat, en stockant renew_hook = ... dans sa configuration de renouvellement. certbot --nginx recharge pour vous ; les montages --webroot et --standalone ne le font pas. Un hook manquant est exactement la raison pour laquelle un site sert un certificat expiré alors que certbot certificates rapporte gaiement un certificat tout frais. Tout ce qui d'autre lit le certificat au démarrage veut le même hook — une application conteneurisée telle qu'une installation Nextcloud sur un VPS avec Docker, TLS et sauvegardes a besoin que sa propre étape de redémarrage ou de rechargement soit câblée ici aussi.
Tester le renouvellement pour de vrai
sudo certbot renew --dry-runCela exécute le défi complet contre l'environnement de test (staging) de Let's Encrypt : même chemin de code, même pare-feu, même DNS, aucun coût de limite de débit, rien d'écrit sur le disque. Si cela passe aujourd'hui, le renouvellement sans intervention dans 60 jours passe aussi, à condition que rien ne change sous la machine.
Un essai à blanc ne prouve pas que votre hook de rechargement se déclenche — le comportement varie selon la version de Certbot. Testez cette moitié à la main : exécutez directement le script de hook, confirmez que systemctl reload nginx réussit, et vérifiez sudo grep renew_hook /etc/letsencrypt/renewal/example.com.conf.
Les erreurs que vous rencontrerez réellement
Could not bind to IPv4 or IPv6. — --standalone alors que nginx détient déjà le port 80. Utilisez --nginx ou --webroot, ou arrêtez nginx autour de l'exécution. Confirmez qui détient le port avec sudo ss -lntp | grep ':80'.
Timeout during connect (likely firewall problem) — Let's Encrypt n'a pas pu joindre le port 80. Progressez vers l'extérieur : sudo ufw status (ouvrez-le avec sudo ufw allow 'Nginx Full'), puis le pare-feu propre du fournisseur VPS, puis le DNS. Testez depuis un endroit qui n'est pas votre serveur : curl -sSv http://example.com/.well-known/acme-challenge/test. Un enregistrement AAAA périmé produit ce même message.
unauthorized :: Invalid response from http://example.com/.well-known/acme-challenge/xyz: 404 — le port 80 est joignable, mais le token n'est pas servi. La requête a atterri dans un autre bloc serveur (vérifiez lequel possède default_server), ou le répertoire passé à -w n'est pas celui que nginx sert. Déposez un fichier dans /var/www/example.com/.well-known/acme-challenge/test et récupérez-le de l'extérieur ; si cela renvoie 404, le certificat n'a jamais été le problème.
DNS problem: NXDOMAIN looking up A for example.com — le nom ne se résout pas publiquement. Des enregistrements neufs qui ne se sont pas propagés, ou un enregistrement dans une zone que votre registrar ne sert pas.
too many certificates already issued for: example.com — une limite de débit, et celle que les gens atteignent en déboguant en boucle. Let's Encrypt plafonne les certificats en double — exactement le même ensemble de noms — à cinq par semaine, et autorise séparément 50 nouveaux certificats par domaine enregistré par semaine ; rien ne débloque l'un ou l'autre à part le temps. Déboguez contre le staging avec --dry-run.
nginx: [emerg] cannot load certificate "/etc/letsencrypt/live/example.com/fullchain.pem": No such file or directory — nginx est configuré pour un certificat jamais émis, ou pour un certificat supprimé avec certbot delete. Mettez en commentaire le bloc serveur TLS, démarrez nginx, émettez, restaurez le bloc.
open() "/etc/letsencrypt/options-ssl-nginx.conf" failed — ce fichier arrive avec le paquet du plugin nginx. Sur une machine certonly sans python3-certbot-nginx, ajoutez soit le plugin, soit remplacez la ligne include par vos propres réglages ssl_protocols et ssl_ciphers.
Gérer cela à l'échelle
Un certificat peut porter jusqu'à 100 noms, et un seul certbot --nginx -d a.example.com -d b.example.com ... est tentant — jusqu'à ce qu'un enregistrement DNS périmé fasse échouer la validation et emporte tous les autres noms de ce certificat avec lui. Des certificats séparés par site échouent indépendamment, ce que vous voulez sur une machine qui héberge plus de deux ou trois choses. Au-delà d'une poignée de sites, une porte d'entrée qui parle ACME gagne sa place : un reverse proxy Traefik faisant tourner plusieurs applications sous Docker Compose demande et renouvelle lui-même les certificats, et Certbot sort complètement du tableau.
Sauvegardez /etc/letsencrypt en entier — sudo tar -czf letsencrypt-$(date +%F).tar.gz -C /etc letsencrypt — liens symboliques intacts. Cette arborescence contient accounts/, votre clé de compte ACME, que vous ne pouvez pas régénérer à l'identique. Passer à un nouveau VPS se réduit alors à : rsync de l'arborescence avec -a, installer Certbot, repointer le DNS, et exécuter certbot renew --dry-run avant de basculer.
Reconstruisez la machine ou sautez vers une nouvelle LTS et le timer de renouvellement ne vous suit pas. Après toute migration, restauration de snapshot ou mise à niveau de distribution, exécutez systemctl list-timers 'certbot*' et un --dry-run. Sauter cette étape, c'est ainsi qu'un site tombe dans le noir 89 jours plus tard, à 3h du matin, sur un certificat que tout le monde supposait se renouveler tout seul.
Tout ceci suppose une machine que vous contrôlez, avec une IP publique et le port 80 ouvert au monde — un VPS, autrement dit. La mécanique ci-dessus est identique sur n'importe lequel d'entre eux.
Les mêmes étapes de certificat s'appliquent sur Apache au lieu de nginx, et quand un certificat public n'est pas envisageable, un certificat auto-signé sur Ubuntu couvre les services internes.
FAQ
Ai-je besoin que le port 80 soit ouvert si mon site ne sert que du HTTPS ?
Oui, pour le défi HTTP-01. Let's Encrypt commence toujours sa requête de validation sur le port 80, et Certbot n'a aucune implémentation TLS-ALPN-01, donc un pare-feu qui n'ouvre que le 443 bloque à la fois la première émission et tout renouvellement automatique après. Une redirection du port 80 vers HTTPS convient — la validation la suit. La seule façon d'éviter le port 80 entièrement est DNS-01 avec un plugin de fournisseur.
apt ou snap — quel Certbot installer pour nginx sur Ubuntu 24.04 ?
Utilisez apt. sudo apt install certbot python3-certbot-nginx vous donne Certbot 2.9.0 sur Ubuntu 24.04, ce qui est assez récent pour tout ce que couvre ce guide, reçoit les correctifs de sécurité via unattended-upgrades, et n'exige pas snapd. Ne choisissez le snap que si vous avez besoin de la toute dernière version immédiatement ou d'un plugin DNS distribué exclusivement en snap. Dans les deux cas, choisissez-en exactement un : deux installations, ce sont deux timers de renouvellement pointés sur la même arborescence /etc/letsencrypt, et celui qu'on oublie est celui qui vous mord.
Certbot peut-il émettre un certificat wildcard pour nginx ?
Seulement via DNS-01. Un wildcard comme *.example.com n'a aucun nom d'hôte unique depuis lequel récupérer un fichier de défi, donc --nginx, --webroot et --standalone sont tous exclus. Installez le plugin de votre fournisseur DNS, mettez un token d'API à portée limitée dans un fichier d'identifiants réservé à root, et exécutez certbot certonly --dns-cloudflare -d example.com -d '*.example.com', en mettant le wildcard entre guillemets pour empêcher le shell de le développer.
Pourquoi nginx sert-il encore l'ancien certificat après un renouvellement réussi ?
nginx garde le certificat en mémoire et ne remarque pas le nouveau fichier sur le disque tant qu'il ne recharge pas. certbot --nginx recharge pour vous, mais les exécutions --webroot et --standalone ne le font pas, donc un renouvellement peut réussir alors que le navigateur voit encore un certificat qui expire. Déposez un script exécutable dans /etc/letsencrypt/renewal-hooks/deploy/ qui exécute nginx -t && systemctl reload nginx, et il se déclenche après chaque renouvellement réussi.
Est-ce que certbot renew --dry-run prouve que le renouvellement fonctionnera ?
En grande partie. Il exécute le vrai défi contre l'environnement de staging — même pare-feu, même DNS, même chemin de code — sans coût de limite de débit et sans rien écrire sur le disque, donc un succès signifie que la moitié réseau est saine. Il ne prouve pas de façon fiable que votre hook de déploiement se déclenche, en revanche. Testez cela séparément : exécutez le script de hook à la main et vérifiez sudo grep renew_hook /etc/letsencrypt/renewal/example.com.conf.