Ubuntu 24.04 : Certbot renouvelle mais votre application échoue — corriger les permissions et recharger les hooks

Cet article vous a aidé ?

Vous lancez certbot renew. Il affiche « Congratulations. ». Votre monitoring indique « Absolument pas. ». Les utilisateurs voient des erreurs TLS, ou l’application continue de servir un certificat qui a expiré hier. C’est le genre d’irritation où la console est optimiste et la production est en feu.

Sur Ubuntu 24.04, le coupable habituel n’est pas Let’s Encrypt lui‑même. C’est la plomberie ennuyeuse autour : permissions de fichiers, symlinks dans /etc/letsencrypt/live, timers systemd qui renouvellent sans redémarrer quoi que ce soit, et applications qui ne peuvent (ou ne veulent) pas recharger les certificats sans un coup de pouce ferme.

Ce qui se passe réellement quand le renouvellement « réussit »

Le renouvellement d’un certificat par Certbot n’est qu’une jambe d’un tabouret à trois pieds :

  1. Emission/renouvellement : Let’s Encrypt signe un nouveau certificat et Certbot l’enregistre sous /etc/letsencrypt/archive/<name>/, puis met à jour les symlinks dans /etc/letsencrypt/live/<name>/.
  2. Accès : Votre application (nginx, Apache, HAProxy, un service Java, un conteneur) doit pouvoir lire fullchain.pem et privkey.pem. « Pouvoir » signifie permissions Unix et traversée de chemin à travers chaque dossier parent. Pas seulement « le fichier existe ».
  3. Rechargement : Le processus doit recharger les fichiers (ou être redémarré) après le renouvellement. Certains démons rechargent sur SIGHUP. Certains exigent d’abord un test de configuration. Certains nécessitent un redémarrage complet. Certains ne rechargent jamais les certificats à l’exécution et continueront à servir l’ancien jusqu’à la prochaine fenêtre de déploiement.

La plupart des échecs surviennent sur les jambes 2 ou 3. Certbot ne sait pas automatiquement comment votre service consomme les certificats. De plus, sur Ubuntu 24.04, systemd et les valeurs par défaut packagées vous poussent vers l’automatisation (timers, services), ce qui est formidable jusqu’au moment où personne ne raccorde la partie « recharger la chose réelle ».

Une vérité opérationnelle : un certificat renouvelé est inutile tant que le processus qui le présente n’a pas rouvert la paire de clés. Le client ne se soucie que des octets reçus pendant le handshake TLS, pas de ce que certbot a affiché.

Une seule citation, parce que c’est toujours vrai des décennies plus tard : « L’espoir n’est pas une stratégie. » — General Gordon R. Sullivan

Blague n°1 : renvouer un TLS sans hook de reload, c’est comme changer les piles d’un détecteur de fumée que vous n’avez pas installé. Techniquement du progrès, pratiquement de la fumée.

Playbook de diagnostic rapide (premier/deuxième/troisième)

Si vous êtes en astreinte, vous ne voulez pas une leçon. Vous voulez le chemin le plus court vers « Le cert est‑il renouvelé, lisible et chargé ? ». Utilisez cet ordre. Ça minimise le va‑et‑vient.

Premier : quel certificat le client voit‑il réellement ?

  • Vérifiez l’expiration et le numéro de série du certificat servi du point de vue client.
  • Si c’est ancien : c’est un problème de rechargement/sélection/routage. Arrêtez de blâmer Let’s Encrypt.
  • Si c’est nouveau mais que les clients échouent encore : vous pouvez avoir un problème de chaîne, un mauvais SNI ou le mauvais binding de vhost.

Deuxième : le processus de service a‑t‑il accès au chemin de la clé privée ?

  • Confirmez que le chemin de fichier dans la config du service correspond à votre symlink /etc/letsencrypt/live prévu.
  • Testez les permissions en tant qu’utilisateur du service (ou utilisez namei pour vérifier la traversée).
  • Cherchez des restrictions AppArmor/SELinux (Ubuntu souvent : AppArmor).

Troisième : qu’est‑ce qui déclenche (ou ne déclenche pas) le rechargement ?

  • Certbot peut s’exécuter sur un timer et renouveler en silence. Votre nginx ne le remarquera pas magiquement.
  • Vérifiez les logs de Certbot pour « Deploying Certificate » et l’exécution des hooks.
  • Ajoutez un deploy hook qui recharge votre service uniquement quand un renouvellement a réellement eu lieu.

Ce n’est qu’après ces trois étapes que vous creusez dans les challenges DNS, les limites ACME, les règles de pare‑feu ou le comportement aléatoire d’un load balancer cloud. Cela arrive, mais ce n’est pas le cas courant quand le renouvellement indique succès.

Faits et contexte intéressants (pourquoi ça mord encore les équipes)

  • Le lancement de Let’s Encrypt (2015) a fait du TLS une attente par défaut, mais il a aussi transformé le « renouvellement de certificat » en une tâche opérationnelle récurrente plutôt qu’un rappel calendaire.
  • Le modèle de stockage de Certbot utilise une structure « archive » plus des symlinks « live » spécifiquement pour permettre des mises à jour atomiques : de nouveaux fichiers arrivent dans archive/, les symlinks bougent dans live/.
  • La clé privée dans /etc/letsencrypt/live est normalement 0600 root:root. C’est la bonne posture de sécurité, et c’est aussi pourquoi les applications non‑root cassent fréquemment après des refactors « utiles ».
  • Les timers systemd ont remplacé cron pour de nombreux renouvellements packagés parce que les timers s’intègrent avec journald et la santé des services. Ils rendent aussi plus facile d’oublier que « renouvellement » n’est pas « déploiement ».
  • Nginx peut recharger la configuration sans couper les connexions, mais seulement si le rechargement est déclenché. Sans cela, nginx continuera à utiliser ce qu’il a chargé au démarrage.
  • Le comportement de reload gracieux d’Apache diffère selon le MPM et la pile de modules ; il est capable, mais des permissions cassées ou un test de config échoué peuvent le laisser tourner avec l’ancien certificat.
  • Les challenges ACME (http-01, dns-01, tls-alpn-01) résolvent l’émission, pas le déploiement. Les équipes confondent « challenge réussi » et « site réparé » parce que les deux apparaissent dans la même sortie de commande.
  • Le package Snap de Certbot a changé des chemins et le confinement pour certaines installations, ce qui peut surprendre les personnes migrant entre versions d’Ubuntu ou suivant des articles obsolètes.

Tâches pratiques : commandes, sorties et décisions (12+)

Ce sont des tâches opérationnelles réelles. Chacune inclut : la commande, une sortie réaliste, ce que ça signifie, et la décision que vous prenez.

Task 1: Confirm what cert the client sees (expiry, subject, issuer)

cr0x@server:~$ echo | openssl s_client -servername app.example.com -connect 127.0.0.1:443 2>/dev/null | openssl x509 -noout -subject -issuer -dates
subject=CN = app.example.com
issuer=C = US, O = Let's Encrypt, CN = R11
notBefore=Dec 29 02:10:11 2025 GMT
notAfter=Mar 29 02:10:10 2026 GMT

Ce que cela signifie : Le processus sur localhost:443 sert un certificat valide jusqu’au Mar 29. Si votre alerte dit « expiré », votre alerte peut vérifier un endpoint différent, ou une couche proxy différente sert l’ancien cert.

Décision : Si le cert servi est ancien/expiré, passez aux étapes de reload/permissions. S’il est récent, vérifiez le routage/SNI et la chaîne intermédiaire.

Task 2: Check the certificate files Certbot thinks are current

cr0x@server:~$ sudo certbot certificates
Saving debug log to /var/log/letsencrypt/letsencrypt.log

Found the following certs:
  Certificate Name: app.example.com
    Serial Number: 4e6a0f9a4b3c17c2a3b9e1d0c4a1a9f2
    Key Type: ECDSA
    Domains: app.example.com www.app.example.com
    Expiry Date: 2026-03-29 02:10:10+00:00 (VALID: 89 days)
    Certificate Path: /etc/letsencrypt/live/app.example.com/fullchain.pem
    Private Key Path: /etc/letsencrypt/live/app.example.com/privkey.pem

Ce que cela signifie : La vue de Certbot est bonne : il a un cert valide et des chemins canoniques.

Décision : Si Certbot affiche une nouvelle date d’expiration mais que les clients voient l’ancienne, votre service ne lit pas ces chemins, ne peut pas les lire, ou ne s’est pas rechargé.

Task 3: Verify the live symlinks actually point to the newest archive version

cr0x@server:~$ sudo ls -l /etc/letsencrypt/live/app.example.com/
total 4
lrwxrwxrwx 1 root root  43 Dec 29 02:10 cert.pem -> ../../archive/app.example.com/cert4.pem
lrwxrwxrwx 1 root root  44 Dec 29 02:10 chain.pem -> ../../archive/app.example.com/chain4.pem
lrwxrwxrwx 1 root root  48 Dec 29 02:10 fullchain.pem -> ../../archive/app.example.com/fullchain4.pem
lrwxrwxrwx 1 root root  46 Dec 29 02:10 privkey.pem -> ../../archive/app.example.com/privkey4.pem
-rw-r--r-- 1 root root 692 Jun  1  2024 README

Ce que cela signifie : Les liens live point maintenant vers la version « 4 ». Le renouvellement a fait pivoter les symlinks.

Décision : Si le numéro du symlink n’a pas changé après le renouvellement, le renouvellement peut ne pas avoir eu lieu ; vérifiez les logs et les limites de taux.

Task 4: Confirm nginx/Apache config references the correct paths

cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -R --line-number -E "ssl_certificate(_key)?\s" /etc/nginx/sites-enabled/* | head
/etc/nginx/sites-enabled/app.conf:12:    ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
/etc/nginx/sites-enabled/app.conf:13:    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;

Ce que cela signifie : nginx est configuré pour utiliser les chemins symlink attendus.

Décision : Si vous voyez /etc/letsencrypt/archive/... codé en dur, changez‑le. Coder en dur les fichiers d’archive garantit la douleur au prochain renouvellement.

Task 5: Check if the service is actually running and what user it runs as

cr0x@server:~$ systemctl status nginx --no-pager
● nginx.service - A high performance web server and a reverse proxy server
     Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
     Active: active (running) since Mon 2025-12-29 02:11:03 UTC; 2h 14min ago
       Docs: man:nginx(8)
   Main PID: 1842 (nginx)
      Tasks: 3 (limit: 19092)
     Memory: 8.3M
        CPU: 1.742s
     CGroup: /system.slice/nginx.service
             ├─1842 "nginx: master process /usr/sbin/nginx -g daemon on; master_process on;"
             ├─1843 "nginx: worker process"
             └─1844 "nginx: worker process"

Ce que cela signifie : nginx tourne. Les workers sont typiquement www-data.

Décision : Si le service échoue ou oscille, les erreurs de permissions apparaîtront probablement dans journald ici. S’il tourne mais sert l’ancien cert, il ne s’est probablement pas rechargé.

Task 6: Look for permission errors in journald

cr0x@server:~$ sudo journalctl -u nginx -n 50 --no-pager
Dec 29 02:10:59 server nginx[1842]: nginx: [emerg] cannot load certificate "/etc/letsencrypt/live/app.example.com/fullchain.pem": BIO_new_file() failed (SSL: error:8000000D:system library::Permission denied:calling fopen(/etc/letsencrypt/live/app.example.com/fullchain.pem, r) error:10080002:BIO routines::system lib)
Dec 29 02:10:59 server systemd[1]: nginx.service: Control process exited, code=exited, status=1/FAILURE
Dec 29 02:10:59 server systemd[1]: nginx.service: Failed with result 'exit-code'.

Ce que cela signifie : Classique. nginx ne peut pas lire le fichier de cert. Souvent parce que le fichier est accessible seulement par root et nginx tourne sans privilèges au mauvais moment, ou parce que la traversée de répertoires est bloquée.

Décision : Ne faites pas chmod 777 pour résoudre ça. Corrigez les permissions avec un modèle délibéré (voir la section permissions).

Task 7: Check path traversal permissions with namei (this catches the “directory is 0700” trap)

cr0x@server:~$ sudo namei -l /etc/letsencrypt/live/app.example.com/privkey.pem
f: /etc/letsencrypt/live/app.example.com/privkey.pem
drwxr-xr-x root root /
drwxr-xr-x root root etc
drwxr-xr-x root root letsencrypt
drwx------ root root live
drwxr-xr-x root root app.example.com
lrwxrwxrwx root root privkey.pem -> ../../archive/app.example.com/privkey4.pem

Ce que cela signifie : /etc/letsencrypt/live est en 0700, donc les utilisateurs non‑root ne peuvent pas traverser dedans, même si le fichier lui‑même avait des bits plus permissifs.

Décision : Si votre application tourne en non‑root et lit directement depuis live, elle échouera. Soit faites lire l’application depuis un chemin copié contrôlé, soit utilisez des ACLs avec précaution.

Task 8: Validate the certificate files parse correctly (catch partial writes or wrong file referenced)

cr0x@server:~$ sudo openssl x509 -in /etc/letsencrypt/live/app.example.com/fullchain.pem -noout -text | grep -E "Not After|Subject:"
        Subject: CN = app.example.com
            Not After : Mar 29 02:10:10 2026 GMT

Ce que cela signifie : Le fichier sur disque est un certificat X.509 valide et a l’expiration attendue.

Décision : Si l’analyse échoue, vous avez peut‑être pointé vers le mauvais fichier ou une corruption. Corrigez avant de toucher aux reloads.

Task 9: Confirm Certbot’s timer/service is present and when it last ran

cr0x@server:~$ systemctl list-timers --all | grep -E "certbot|letsencrypt"
Mon 2025-12-29 02:07:41 UTC  10h left Mon 2025-12-29 00:08:12 UTC  11h ago certbot.timer                certbot.service

Ce que cela signifie : Le renouvellement est géré par un timer. C’est bien, mais il faut voir ce que le service fait réellement.

Décision : Inspectez la définition de certbot.service et le comportement des hooks ensuite.

Task 10: Inspect what the Certbot systemd service executes (where hooks may or may not be wired)

cr0x@server:~$ systemctl cat certbot.service
# /usr/lib/systemd/system/certbot.service
[Unit]
Description=Certbot
Documentation=file:///usr/share/doc/certbot/readme.Debian.gz
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/bin/certbot -q renew
PrivateTmp=true

Ce que cela signifie : Il exécute certbot -q renew silencieusement. Il n’y a pas de --deploy-hook ici. Donc à moins que vous ayez configuré des hooks ailleurs, rien ne recharge.

Décision : Ajoutez un deploy hook dans /etc/letsencrypt/renewal-hooks/deploy/ ou surchargez l’unité systemd (plus loin on en parle).

Task 11: Check Certbot logs for hook execution and renewal outcome

cr0x@server:~$ sudo tail -n 60 /var/log/letsencrypt/letsencrypt.log
2025-12-29 02:10:11,214:INFO:certbot._internal.renewal:Cert is due for renewal, auto-renewing...
2025-12-29 02:10:12,992:INFO:certbot._internal.client:Successfully received certificate.
2025-12-29 02:10:13,103:INFO:certbot._internal.storage:Writing new private key to /etc/letsencrypt/archive/app.example.com/privkey4.pem.
2025-12-29 02:10:13,214:INFO:certbot._internal.storage:Deploying certificate to /etc/letsencrypt/live/app.example.com/fullchain.pem.
2025-12-29 02:10:13,215:INFO:certbot._internal.storage:Deploying key to /etc/letsencrypt/live/app.example.com/privkey.pem.

Ce que cela signifie : Le renouvellement a eu lieu. Mais il n’y a pas de preuve qu’un deploy hook se soit exécuté (ces lignes montreraient l’exécution des hooks si configurés).

Décision : Implémentez des deploy hooks. Si des hooks existent mais ne se sont pas exécutés, vérifiez les permissions/exécutabilité des scripts de hook.

Task 12: Manual reload with config test (avoid restarting into a broken config)

cr0x@server:~$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Ce que cela signifie : Le rechargement est sûr.

Décision : Procédez au reload. Si le test échoue, corrigez d’abord la config — aucun hook ne devrait recharger un démon dans un état défaillant.

Task 13: Reload the service and re-check the served cert

cr0x@server:~$ sudo systemctl reload nginx
cr0x@server:~$ echo | openssl s_client -servername app.example.com -connect 127.0.0.1:443 2>/dev/null | openssl x509 -noout -dates
notBefore=Dec 29 02:10:11 2025 GMT
notAfter=Mar 29 02:10:10 2026 GMT

Ce que cela signifie : Après le reload, le certificat servi correspond au certificat renouvelé.

Décision : Votre correctif est « assurer le rechargement après un renouvellement effectif ». Automatisez‑le maintenant avec un deploy hook.

Task 14: Test permissions as the service user (the only test that matters)

cr0x@server:~$ sudo -u www-data bash -lc 'head -n 1 /etc/letsencrypt/live/app.example.com/fullchain.pem'
head: cannot open '/etc/letsencrypt/live/app.example.com/fullchain.pem' for reading: Permission denied

Ce que cela signifie : Comme prévu, www-data ne peut pas lire le fichier. Si nginx a besoin de lire les certs en tant que www-data au moment du reload, vous échouerez.

Décision : Ne « corrigez » pas cela en rendant les clés privées lisibles par tous. Utilisez un modèle où le reload est effectué par root, ou une approche de copie/ACL contrôlée.

Corriger les permissions sans créer un incident de sécurité

La tentation est immédiate : chmod -R 755 /etc/letsencrypt, recharger, rentrer chez soi. Ça marche jusqu’au moment où vous réalisez que vous venez de rendre les clés privées lisibles par plus de principes que prévu. Dans certains environnements, c’est un incident à part entière.

Voici le modèle mental pratique :

  • La confidentialité de la clé privée est l’essentiel. Si un attaquant lit privkey.pem, il peut usurper votre service jusqu’à révocation/rotation du certificat et tant que les clients font confiance à l’ancien chain.
  • La plupart des démons n’ont pas besoin que la clé soit lisible par l’utilisateur worker si le processus master démarre/recharge en root puis abdique ses privilèges. nginx est un exemple classique : le master tourne en root, lit les clés, puis les workers tournent sans privilèges.
  • Les problèmes surviennent quand vous exécutez le service entièrement en non‑root (conteneurs, unités hardenées, utilisateur personnalisé) et que vous le pointez vers /etc/letsencrypt/live.

Choisissez un des trois schémas sains

Pattern A (préféré) : le reload du service s’exécute en root ; les fichiers restent root‑only

Si vous pouvez recharger nginx/Apache/HAProxy en root via systemd, gardez les valeurs par défaut de /etc/letsencrypt. C’est le plus sûr et le plus simple.

Ce que vous faites : créez un deploy hook qui exécute nginx -t puis systemctl reload nginx. Les fichiers de certificat restent root‑only. Le reload est privilégié, donc le service peut lire la clé.

Pattern B : copie contrôlée vers un répertoire lisible par l’app (bon pour les apps non‑root)

Certaines apps (ou conteneurs) s’exécutent complètement en non‑root et doivent lire la clé directement. Ne les pointez pas vers /etc/letsencrypt/live. À la place :

  • Créez un répertoire dédié comme /etc/ssl/app.example.com/ avec une propriété et des permissions strictes.
  • Dans le deploy hook, copiez fullchain.pem et privkey.pem là‑bas avec install (ça définit mode/propriétaire de façon suffisamment atomique pour nos besoins).
  • Rechargez le service après la copie.

Cela réduit le rayon d’impact : vous n’affaiblissez pas /etc/letsencrypt, vous n’exposez que ce que l’application nécessite, au bon utilisateur.

Pattern C : ACLs sur des chemins spécifiques (à utiliser parcimonieusement, documenter fortement)

Vous pouvez utiliser des ACL POSIX pour accorder des droits de lecture/traversée à l’utilisateur du service uniquement pour les fichiers/dossiers nécessaires. Ça peut fonctionner, mais c’est facile à oublier et difficile à auditer rapidement.

Si vous choisissez les ACLs, intégrez la vérification dans votre runbook. Sinon votre futur vous réparera encore ça avec un chmod à 3h du matin.

Ce qu’il ne faut pas faire (sauf si vous aimez les retros d’incident)

  • Ne rendez pas privkey.pem lisible par le groupe via un groupe large comme www-data si ce groupe contient d’autres services. C’est du mouvement latéral en cadeau.
  • Ne pointez pas les services vers /etc/letsencrypt/archive. Les noms de fichiers d’archive s’incrémentent ; votre config ne suivra pas.
  • N’écrivez pas de hooks qui redémarrent des services critiques à chaque exécution du timer quand rien n’a été renouvelé. C’est s’auto‑infliger du churn.

Hooks de rechargement bien faits (deploy hooks, systemd et pièges)

Certbot offre plusieurs types de hooks. Celui que vous voulez pour « recharger après un renouvellement réussi » est typiquement le deploy hook. Il s’exécute uniquement quand un certificat est effectivement renouvelé (ou nouvellement émis), ce qui maintient vos services stables les jours où rien ne change.

Le répertoire des deploy hooks (l’option la plus simple et la moins surprenante)

Déposez un script exécutable dans :

  • /etc/letsencrypt/renewal-hooks/deploy/

Certbot l’exécutera après avoir écrit le nouveau cert et mis à jour les symlinks live.

Exemple : hook de reload nginx avec vérifications de sécurité

cr0x@server:~$ sudo install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
cr0x@server:~$ sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx >/dev/null <<'EOF'
#!/bin/bash
set -euo pipefail

# Only reload if nginx is installed and running.
if ! command -v nginx >/dev/null 2>&1; then
  exit 0
fi

if ! systemctl is-active --quiet nginx; then
  exit 0
fi

# Validate config before reload; fail hook if config is broken.
nginx -t

# Reload picks up new cert without dropping connections.
systemctl reload nginx
EOF
cr0x@server:~$ sudo chmod 0755 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx

Pourquoi ce script est volontairement prudent : il ne fait rien si nginx n’est pas présent ou actif (utile sur des serveurs multi‑rôle), et il refuse de recharger une config cassée. Les hooks doivent être sûrs en présence de changements non liés.

Testez le hook sans attendre le jour du renouvellement

Utilisez le dry run de Certbot. Il simule un renouvellement (en staging) et exécute les hooks.

cr0x@server:~$ sudo certbot renew --dry-run
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/app.example.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Simulating renewal of an existing certificate for app.example.com and www.app.example.com

Congratulations, all simulated renewals succeeded:
  /etc/letsencrypt/live/app.example.com/fullchain.pem (success)

Ce que cela signifie : Le dry‑run a réussi. Confirmez maintenant que le hook s’est exécuté en vérifiant les logs de reload nginx ou les timestamps dans journald.

Décision : Si le dry‑run fonctionne mais que le renouvellement en production ne recharge pas, inspectez l’exécutabilité du fichier, SELinux/AppArmor, ou les différences de confinement Snap.

Quand utiliser --deploy-hook à la place

Si vous voulez lier le hook à une invocation spécifique (par exemple une unité spéciale ou un certificat particulier), vous pouvez passer --deploy-hook sur la ligne de commande. Mais sur Ubuntu avec timers systemd, le répertoire de hooks est généralement plus simple, car il s’applique de façon cohérente même quand des humains lancent certbot renew manuellement.

Overrides systemd : quand vous devez contrôler le comportement centralement

Si votre organisation exige que « tout soit dans des unités systemd », surchargez le service :

cr0x@server:~$ sudo systemctl edit certbot.service
cr0x@server:~$ sudo systemctl cat certbot.service
# /usr/lib/systemd/system/certbot.service
[Unit]
Description=Certbot
Wants=network-online.target
After=network-online.target

# /etc/systemd/system/certbot.service.d/override.conf
[Service]
ExecStart=
ExecStart=/usr/bin/certbot -q renew --deploy-hook "systemctl reload nginx"

Ce que cela signifie : Vous avez remplacé ExecStart par un appel contenant un deploy hook. Il rechargera nginx uniquement sur des renouvellements effectifs.

Décision : Choisissez soit les hooks par répertoire, soit les overrides d’unité. Ne faites pas les deux à moins d’aimer les doubles reloads mystérieux.

Piège : reload vs restart

Préférez reload si le service le supporte correctement. C’est moins perturbant. Utilisez restart quand :

  • Le démon ne peut pas recharger proprement les certificats (certains serveurs d’applications).
  • Vous êtes dans un conteneur où « reload » n’existe pas et vous devez relancer le process.
  • Vous avez vérifié que reload ne prend pas les nouvelles clés (rare mais réel, selon l’intégration).

Blague n°2 : si votre hook de renouvellement utilise restart pour tout, vous avez réinventé la « maintenance planifiée », mais avec plus de surprises.

Conteneurs et reverse proxies : où les rechargements meurent

Ubuntu 24.04 n’a pas inventé les conteneurs, mais il a hérité de leur propriété favorite : ils font des hypothèses sur le système de fichiers qui deviennent le problème de quelqu’un d’autre. Certbot s’exécute sur l’hôte, met à jour des fichiers sur l’hôte, et votre terminateur TLS peut être :

  • nginx sur l’hôte (simple)
  • nginx dans un conteneur (problèmes de partage de fichiers et de signalisation)
  • Traefik/HAProxy dans un conteneur (les options de reload dynamique varient)
  • un load balancer cloud (le renouvellement certbot est sans effet à moins de téléverser les certificats)

Cas conteneur 1 : certificats de l’hôte montés en lecture seule

Patron courant : monter /etc/letsencrypt/live/app.example.com dans le conteneur. Le conteneur peut lire les fichiers, mais il ne saura pas qu’ils ont changé à moins que :

  • le process interroge ou surveille les changements, ou
  • vous déclenchiez un signal de reload dans le conteneur.

Certbot a réussi le renouvellement ; le conteneur sert toujours l’ancien cert ; tout semble « ok ». Ce n’est pas un problème de cert. C’est un problème de cycle de vie.

Cas conteneur 2 : conteneur non‑root qui ne peut pas traverser /etc/letsencrypt/live

Même avec un bind mount, les permissions du dossier peuvent bloquer l’accès. Rappelez‑vous le namei plus haut montrant live en 0700. Si vous montez /etc/letsencrypt de façon large, vous pouvez quand même être bloqué à la racine du mount. La bonne approche est souvent Pattern B : copier les certificats vers un répertoire lisible par le conteneur avec des permissions strictes, et monter celui‑ci.

Mauvais alignement au niveau du reverse proxy

Autre classique : vous renouvelez les certificats sur le serveur applicatif, mais le TLS est terminé sur un proxy frontal (nginx/HAProxy) ou un load balancer. Votre application ne présente jamais de certificat aux clients, donc le renouvellement ne change rien. Pendant ce temps, le certificat important est ailleurs et expire tranquillement.

Conseil opérationnel : cartographiez le chemin du handshake. Le certificat important est celui du premier saut TLS depuis le client. Tout ce qu’il y a derrière est du trafic interne sauf si vous faites du mTLS bout‑en‑bout.

Trois mini‑histoires du monde corporate (douleur incluse)

Mini‑histoire 1 : l’incident causé par une mauvaise hypothèse

Ils avaient une configuration propre : Certbot sur Ubuntu, nginx terminant le TLS, et quelques upstreams. Quelqu’un a hardenisé plusieurs unités systemd et retiré des privilèges root. nginx en faisait partie, au nom du « least privilege ». La demande de changement avait l’air raisonnable ; elle avait même une approbation sécurité.

Le jour du renouvellement Certbot est arrivé. Le timer a tourné, écrit de nouveaux fichiers, mis à jour les symlinks, et affiché un succès. Le deploy hook a déclenché un reload. nginx a tenté de rouvrir les fichiers de certificat. Et a échoué immédiatement parce que l’unité tournait désormais en utilisateur non‑privilegié sans accès à /etc/letsencrypt/live (qui est 0700 au niveau du répertoire).

L’hypothèse était subtile : « Si les workers nginx peuvent tourner en non‑root, alors nginx peut tourner en non‑root. » Pas toujours. Le master de nginx démarre traditionnellement en root précisément pour binder des ports bas et lire le matériel de clé, puis il abdique pour les workers. Le faire tourner entièrement en non‑root change ce qu’il peut lire, et soudain le renouvellement de cert devient un événement de fiabilité.

Ils ont corrigé en revenant sur le durcissement de l’unité pour nginx (en gardant la séparation des privilèges des workers) et en rédigeant une règle explicite : les services qui terminent TLS doivent avoir une méthode claire et revue pour accéder aux clés privées. Le postmortem n’a pas blâmé Certbot. Il a blâmé l’absence d’un test bout‑en‑bout qui validait « expiration du cert servi après renouvellement ».

Mini‑histoire 2 : l’optimisation qui s’est retournée

Une équipe plateforme voulait réduire le churn de reloads. Ils avaient des dizaines de certificats sur de nombreux vhosts et ont décidé d’exécuter certbot renew chaque heure « au cas où », mais de recharger nginx une fois par jour dans un job séparé. Moins de reloads, moins de bruit, moins de risque de perturber des connexions longues. Sur le papier, c’était propre.

Puis ils ont ajouté un nouveau domaine avec un certificat qui se renouvelait plus tôt que l’heure du reload quotidien. Certbot l’a renouvelé joyeusement, mais nginx a continué à servir l’ancien cert pendant presque 24 heures. Un client avec des vérifications TLS strictes a commencé à échouer. L’équipe support voyait « renewal success » dans les logs et a supposé que c’était un problème client. Ce n’était pas le cas.

L’optimisation a cassé le contrat caché : le renouvellement doit être couplé au déploiement. Vous pouvez optimiser la fréquence des reloads seulement si votre terminateur TLS supporte le chargement dynamique des certificats par handshake (beaucoup ne le font pas) ou si vous implémentez une condition de reload plus intelligente. Sinon vous optimisez la mauvaise variable : le nombre de reloads plutôt que la justesse.

La correction a été de recharger sur les renouvellements effectifs seulement (deploy hook), et d’ajouter une garde : « le certificat servi doit avoir au moins 20 jours avant expiration » vérifié par le proxy lui‑même. Cela a remplacé des hypothèses par des mesures.

Mini‑histoire 3 : la pratique ennuyeuse mais correcte qui a sauvé la mise

Une autre org avait une règle peu glamour : chaque endpoint TLS doit avoir un script local qui affiche l’expiration du certificat servi et la compare à celle sur disque. Le script tournait comme health check quotidien et pendant les déploiements. Personne ne l’aimait. Personne n’en faisait un T‑shirt. Il était juste là.

Un matin, un renouvellement est arrivé et le proxy ne s’est pas rechargé. Le hook existait, mais une mise à jour de packaging a remplacé une override systemd et supprimé un flag --deploy-hook personnalisé. Le timer tournait toujours. Certbot a renouvelé. Le service est resté en ligne et a continué à servir l’ancien cert, donc aucun moniteur d’uptime n’a déclenché.

Le script ennuyeux l’a détecté avant les utilisateurs : « l’expiration servie ne correspond pas à l’expiration live ». L’astreinte n’avait qu’une commande à exécuter, un seul endroit à regarder, et une seule correction : restaurer le deploy hook via /etc/letsencrypt/renewal-hooks/deploy (qui a mieux survécu aux changements de packaging).

Cette pratique n’a pas empêché la mauvaise configuration, mais elle l’a transformée en ticket de maintenance calme au lieu d’une panne publique. Ennuyeux, correct, et étrangement héroïque.

Erreurs courantes : symptôme → cause racine → correction

1) « Certbot dit renouvelé, mais le navigateur montre un certificat expiré »

Symptôme : certbot renew rapporte un succès ; les clients voient encore une ancienne expiration.

Cause racine : Le service ne s’est pas rechargé, ou le endpoint TLS n’est pas le service que vous avez renouvelé (mismatch proxy/load balancer).

Correction : Ajoutez un deploy hook pour recharger le bon process ; vérifiez depuis le client avec openssl s_client contre le réel endpoint.

2) « nginx échoue à recharger après le renouvellement avec Permission denied »

Symptôme : journald montre BIO_new_file() failed ... Permission denied.

Cause racine : Chemin de clé/cert non lisible dû à la traversée de répertoire (/etc/letsencrypt/live est 0700) ou le service tourne en non‑root.

Correction : Gardez le master nginx privilégié pour la lecture des clés, ou copiez cert/key dans un répertoire contrôlé lisible par l’utilisateur du service.

3) « Le hook de reload s’exécute, mais le service sert encore l’ancien certificat »

Symptôme : Le hook a été exécuté ; la commande de reload a réussi ; le certificat servi est inchangé.

Cause racine : Le service n’utilise pas les chemins /etc/letsencrypt/live, ou vous avez un SNI/misbinding, ou il y a un autre terminateur TLS en frontal.

Correction : Confirmez les chemins de config avec nginx -T / configs vhost Apache ; vérifiez le SNI avec -servername ; tracez le chemin du handshake.

4) « Tout fonctionne manuellement, mais l’automatisation échoue »

Symptôme : Lancer certbot renew à la main marche ; le timer systemd renouvelle mais ne recharge pas.

Cause racine : Votre commande manuelle contient des flags/hooks ; l’unité timer ne les a pas. Ou les scripts de hook ne sont pas exécutables dans le contexte du timer.

Correction : Placez les hooks dans /etc/letsencrypt/renewal-hooks/deploy et assurez‑vous de chmod 0755. Validez avec certbot renew --dry-run.

5) « Après le renouvellement, certains clients échouent avec des erreurs de chaîne »

Symptôme : Échecs intermittents, « unable to get local issuer certificate », alors que certains clients fonctionnent.

Cause racine : Vous avez configuré ssl_certificate sur cert.pem au lieu de fullchain.pem, ou chaîne mixte entre proxies.

Correction : Servez fullchain.pem pour les configurations typiques nginx/HAProxy ; testez avec openssl s_client -showcerts.

6) « Le renouvellement fonctionne, mais le conteneur sert encore l’ancien cert »

Symptôme : Les fichiers hôtes sont mis à jour ; le trafic depuis le conteneur montre l’ancien cert.

Cause racine : Le process containerisé ne se recharge pas, ou les mount points/permissions empêchent de voir les mises à jour.

Correction : Implémentez un hook qui signale le conteneur (ou le redémarre) après renouvellement, ou basculez vers un proxy qui supporte le reload dynamique des certs.

Checklists / plan étape par étape

Checklist A : Arrêter l’hémorragie (restaurer un TLS valide maintenant)

  1. Vérifiez l’expiration du cert servi depuis le endpoint que l’utilisateur frappe (openssl s_client avec SNI).
  2. Si le cert servi est ancien : rechargez le terminateur TLS (systemctl reload nginx après nginx -t).
  3. Si le reload échoue : lisez journald pour des erreurs de permissions et corrigez le modèle d’accès (ne pas chmod les clés privées largement).
  4. Re‑vérifiez le cert servi après reload. Fermez l’incident seulement alors.

Checklist B : Faire en sorte que le renouvellement déploie réellement (pour que ça ne se reproduise pas)

  1. Décidez votre modèle de permissions : A (reload root), B (copie contrôlée), ou C (ACLs).
  2. Créez un script de deploy hook dans /etc/letsencrypt/renewal-hooks/deploy/.
  3. Dans le hook : testez la config (nginx -t / apachectl configtest) avant reload/restart.
  4. Exécutez certbot renew --dry-run et vérifiez l’exécution du hook (timestamps journald).
  5. Ajoutez un health check qui compare l’expiration servie à l’expiration sur disque (capturer les hooks cassés après mises à jour).

Checklist C : Modèle de permissions pour services non‑root (Pattern B)

  1. Créez un répertoire restreint détenu par l’utilisateur du service (ou un groupe dédié) avec 0750 ou plus strict.
  2. Utilisez install dans un deploy hook pour copier cert et clé avec mode/propriétaire explicites.
  3. Pointez l’app vers les chemins copiés, pas vers le dossier live de Let’s Encrypt.
  4. Rechargez/restart le service après la copie.
  5. Auditez : confirmez que seuls les principes prévus peuvent lire la clé privée.

Example Pattern B hook: copy files and reload

cr0x@server:~$ sudo tee /etc/letsencrypt/renewal-hooks/deploy/publish-app-cert >/dev/null <<'EOF'
#!/bin/bash
set -euo pipefail

DOMAIN="app.example.com"
SRC_DIR="/etc/letsencrypt/live/${DOMAIN}"
DST_DIR="/etc/ssl/${DOMAIN}"

install -d -m 0750 -o root -g appsvc "${DST_DIR}"

# Copy with explicit permissions. Private key is readable only by root and group appsvc.
install -m 0644 -o root -g appsvc "${SRC_DIR}/fullchain.pem" "${DST_DIR}/fullchain.pem"
install -m 0640 -o root -g appsvc "${SRC_DIR}/privkey.pem"   "${DST_DIR}/privkey.pem"

# Validate service config if applicable, then reload.
if systemctl is-active --quiet appsvc; then
  systemctl reload appsvc || systemctl restart appsvc
fi
EOF
cr0x@server:~$ sudo chmod 0755 /etc/letsencrypt/renewal-hooks/deploy/publish-app-cert

Point de décision : Faites de appsvc un groupe serré avec uniquement le compte de service. Ne réutilisez pas un groupe partagé juste parce qu’il existe.

FAQ

1) Pourquoi Certbot renouvelle avec succès mais mon site affiche toujours l’ancien certificat ?

Parce que le renouvellement met à jour des fichiers sur disque, pas le process en cours. Votre terminateur TLS doit recharger ou redémarrer pour relire le cert/key.

2) Dois‑je pointer nginx vers /etc/letsencrypt/archive pour éviter les symlinks ?

Non. Les noms d’archive s’incrémentent à chaque renouvellement (fullchain4.pem, fullchain5.pem). Utilisez /etc/letsencrypt/live pour que les mises à jour suivent les symlinks.

3) Est‑ce sûr de rendre /etc/letsencrypt/live lisible par www-data ?

Généralement non. Cela augmente le nombre de principes pouvant lire la clé privée. Préférez des reloads effectués par root (Pattern A) ou une copie contrôlée vers un répertoire dédié avec un groupe dédié (Pattern B).

4) Quelle est la différence entre un deploy hook et un post hook ?

Un deploy hook s’exécute uniquement quand un cert est effectivement renouvelé/émis. Un post hook s’exécute après chaque exécution de Certbot, même si rien n’a changé. Pour les reloads, les deploy hooks sont la valeur par défaut sensée.

5) Pourquoi certbot renew --dry-run est important ?

Il valide le flux ACME et exécute vos hooks sans attendre la vraie expiration. C’est le moyen le plus rapide pour détecter un « hook non exécutable » ou une commande de reload qui échoue.

6) Mon service tourne dans Docker. Comment le recharger depuis Certbot ?

Soit (a) montez un répertoire de certificats publié dans le conteneur et envoyez un signal/redémarrage via le runtime du conteneur depuis un deploy hook, soit (b) terminez le TLS en dehors du conteneur sur un proxy hôte.

7) J’ai rechargé nginx mais les clients échouent encore en TLS. Et maintenant ?

Vérifiez la configuration de la chaîne (servez fullchain.pem), un mismatch SNI (utilisez -servername dans les tests), et si un proxy frontal/load balancer sert un certificat différent.

8) Ubuntu 24.04 change‑t‑il quelque chose concernant Certbot spécifiquement ?

Le changement majeur est dans le packaging et les attentes d’automatisation : les timers systemd sont courants, Snap vs apt peut différer, et le confinement peut modifier les hypothèses sur le système de fichiers. Vos hooks doivent correspondre à votre méthode d’installation.

9) Comment prouver que le process en cours a chargé le nouveau certificat ?

Comparez le numéro de série/la date d’expiration du certificat servi (via openssl s_client) avec le certificat sur disque live. S’ils correspondent, le process l’a chargé. Sinon, il ne l’a pas fait.

Conclusion : prochaines étapes durables

Le chemin fiable sur Ubuntu 24.04 est brutal et efficace :

  1. Vérifiez ce que les clients voient, pas ce que Certbot affirme.
  2. Gardez les clés privées verrouillées. Corrigez l’accès par conception, pas par panique chmod.
  3. Raccordez le renouvellement au déploiement avec un deploy hook qui teste la config et recharge le bon service.
  4. Ajoutez un health check qui compare l’expiration servie à l’expiration sur disque afin que des mises à jour de packaging ou des refactors ne puissent pas vous casser discrètement.

Si vous ne faites qu’une chose après avoir lu ceci : créez un deploy hook dans /etc/letsencrypt/renewal-hooks/deploy/ qui recharge en toute sécurité votre terminateur TLS, puis prouvez‑le avec certbot renew --dry-run. C’est comme ça que vous transformez « les certificats sont automatisés » d’un slogan en une propriété de votre production.

← Précédent
Ubuntu 24.04 : montage CIFS « Permission denied » — les options exactes qui résolvent le problème
Suivant →
Pilotes qui dégradent les performances : le rituel post‑mise à jour

Laisser un commentaire