À 02:13, votre téléphone d’astreinte vibre avec la peur spécifique de « les clients ne peuvent pas se connecter ». Vous ouvrez le tableau de bord et voyez la situation : échecs de la négociation TLS, une cascade d’erreurs 525/526, et un certificat qui a expiré… hier. Le service fonctionne. Le pipeline de certificats, non. C’est ainsi que le « HTTPS simple » devient un incident de production.
Docker n’a pas causé ça. Mais Docker a facilité la dissimulation des bords tranchants : clés privées dans des systèmes de fichiers éphémères, renouvellements exécutés dans le mauvais espace de noms, défis ACME coincés derrière un proxy, et une approche « montez /etc/letsencrypt quelque part » qui se transforme en cirque de permissions et de rotation.
La décision : où appartient Let’s Encrypt
Vous avez deux grandes options :
- Le client ACME tourne sur l’hôte (ou sur une VM/namespace dédiée « certificat ») et écrit les certificats dans un emplacement contrôlé ; les conteneurs les consomment en lecture seule.
- Le client ACME tourne à l’intérieur d’un conteneur (Certbot, lego, Traefik, Caddy, compagnon nginx-proxy, etc.) et écrit les certificats dans un volume que lisent les autres conteneurs.
Si vous exploitez des systèmes en production et que vous vous souciez du rayon d’impact, la valeur par défaut sûre est : terminer le TLS sur un conteneur reverse proxy dédié, et exécuter l’émission/renouvellement des certificats dans cette même frontière (ACME natif du proxy comme Traefik/Caddy) ou sur l’hôte avec des permissions de fichiers strictes. Tout le reste peut utiliser HTTP simple sur un réseau interne.
Ce qu’il faut éviter, c’est le modèle « chaque conteneur d’application gère son propre Let’s Encrypt ». Cela paraît modulaire. En réalité, c’est un générateur de déni de service (limites de taux), un cauchemar d’observabilité, et un cadeau pour quiconque veut vos clés privées dispersées sur des volumes en écriture.
Règle opinionnée : centralisez les certificats par point d’entrée. Si l’Internet public atteint un seul endroit, cet endroit possède le TLS. Vos conteneurs applicatifs ne devraient pas avoir à connaître ACME.
Faits et historique qui changent votre manière d’opérer
- Let’s Encrypt a été lancé en 2015 et a fait de l’automatisation une attente, pas un luxe. Le seuil opérationnel a bougé : « on renouvelle avec un rappel calendaire » n’est plus acceptable.
- ACME est devenu une norme IETF (RFC 8555) en 2019. Cela compte car les clients sont remplaçables ; le flux de travail n’est pas un tour de fournisseur.
- Les certificats ont une courte durée de vie par conception (90 jours pour Let’s Encrypt). Ce n’est pas de l’avarice ; ça réduit la fenêtre de dégâts si une clé privée fuit.
- Les limites de taux font partie du modèle de sécurité. Elles punissent aussi les « boucles de retry » lorsque votre déploiement échoue aux challenges toutes les minutes.
- La validation HTTP-01 nécessite la joignabilité du port 80 pour le domaine. Si vous forcez HTTPS exclusivement sans une exception bien planifiée, vous casserez l’émission au pire moment.
- La validation DNS-01 ne requiert pas de ports entrants. C’est la solution courante dans des environnements verrouillés, mais cela transfère votre risque vers les identifiants API DNS.
- Les certificats wildcard exigent DNS-01 chez Let’s Encrypt. Si votre plan dépend des wildcard, vous avez déjà choisi votre type de challenge.
- La terminaison TLS est aussi une frontière de confiance. C’est l’endroit où vous décidez des suites de chiffrement, HSTS, authentification par certificat client, et où résident les clés privées.
- Recharger un serveur n’est pas la même chose que redémarrer un conteneur. Certains proxies peuvent recharger les certificats à chaud ; d’autres nécessitent un redémarrage ; certains un signal ; d’autres une requête API.
Une idée paraphrasée de l’équipe du livre SRE de Google (Beyer, Jones, Petoff, Murphy) : L’espoir n’est pas une stratégie ; la fiabilité vient de systèmes conçus et de boucles de rétroaction.
Cela s’applique douloureusement bien aux renouvellements de certificats.
Trois modèles, classés par sécurité
Modèle A (meilleur choix par défaut) : le reverse proxy possède le TLS + ACME, les apps restent HTTP-only
C’est l’approche « l’ingress est un produit ». Vous exécutez Traefik ou Caddy (ou nginx avec un compagnon) comme le seul conteneur exposé publiquement. Il demande les certificats, les stocke, les renouvelle et les sert. Les conteneurs applicatifs ne touchent jamais aux clés privées.
Pourquoi c’est sûr :
- Un seul endroit à durcir et à observer.
- Un seul endroit pour recharger correctement les certificats.
- Les applications peuvent être mises à l’échelle/redéployées sans toucher à l’état TLS.
- Les limites de taux sont plus faciles à respecter.
Où ça vous mord : vous devez protéger le stockage ACME du proxy (clés privées et clés de compte). Si votre conteneur proxy est compromis, c’est votre identité d’autorité pour cet ensemble de domaines.
Modèle B (très bon) : client ACME sur l’hôte, certificats montés en lecture seule dans le conteneur proxy
C’est le modèle Unix ennuyeux. Certbot (ou lego) tourne sur l’hôte via des timers systemd, écrit dans /etc/letsencrypt, et votre conteneur proxy lit les certificats depuis un bind mount en lecture seule. Le rechargement se fait via un hook contrôlé.
Pourquoi c’est sûr :
- La planification et le logging au niveau hôte sont prévisibles.
- Vous pouvez utiliser les contrôles de sécurité de l’OS (permissions, SELinux/AppArmor) plus naturellement.
- Votre conteneur proxy n’a pas besoin des identifiants API DNS ni des clés de compte ACME.
Où ça vous mord : les challenges HTTP-01 peuvent être maladroits si votre proxy est aussi conteneurisé. Vous avez besoin d’un chemin propre depuis Internet vers le répondeur de challenge.
Modèle C (acceptable seulement quand contraint) : client ACME dans un conteneur écrivant dans un volume partagé
C’est le pattern « conteneur Certbot + conteneur nginx » en Compose. Ça peut fonctionner. Mais ça a tendance à vieillir mal : dérive des permissions, volumes copiés entre hôtes, et les renouvellements deviennent invisibles jusqu’à l’échec.
Quand c’est justifié :
- Vous ne pouvez rien installer sur l’hôte (environnements managés, images durcies).
- Vous êtes dans des contraintes de type Kubernetes mais restez sur Docker.
- Vous avez un hôte à usage unique et des politiques d’isolation des conteneurs strictes.
Ce qu’il faut faire si vous le choisissez : traitez le volume de certificats comme un magasin de secrets. Montages en lecture seule partout sauf pour l’écrivain ACME. Propriété stricte des fichiers. Pas de « 777 parce que ça marche ».
Blague n°1 : Les certificats sont comme le lait. Ils vont bien jusqu’à ce que vous oubliiez la date d’expiration, et alors l’odeur atteint la direction.
Le modèle que vous ne devriez pas déployer : chaque service exécute son propre Certbot
Plusieurs services se disputant le port 80, chacun écrivant dans son volume, chacun renouvelant selon son calendrier, certains utilisant staging vs production différemment. C’est une belle façon d’apprendre les limites de taux de Let’s Encrypt en temps réel.
Stockage des certificats : volumes, permissions et le problème de la clé privée
La plupart des postmortems sur TLS ne portent pas vraiment sur TLS. Ils portent sur l’état : où vivent les clés, qui peut les lire, et si cet état survit aux redéploiements.
Ce qui doit être persistant
- Clés privées (
privkey.pem) : si elles sont perdues, vous pouvez réémettre, mais vous provoquerez une indisponibilité et potentiellement vous verrouillerez hors de flux de pinning/HSTS. - Chaîne de certificats (
fullchain.pem) : nécessaire au serveur pour présenter une chaîne valide. - Clé de compte ACME : utilisée par le client pour s’authentifier auprès de Let’s Encrypt. La perdre permet toujours de se réenregistrer, mais vous perdez la continuité et parfois des surprises opérationnelles.
Bind mount vs volume nommé
Bind mount est simple et auditable : vous pouvez inspecter les fichiers sur l’hôte, les sauvegarder et appliquer des permissions d’hôte. Pour du matériel sensible, les bind mounts sont généralement plus faciles à raisonner.
Les volumes nommés sont portables au sein des outils Docker, mais ils peuvent devenir une boîte noire. Ils conviennent si vous les traitez comme un datastore géré et si vous savez comment ils sont sauvegardés et restaurés.
Permissions : moindre privilège, pas moindre effort
Votre proxy a besoin d’un accès en lecture à la clé. Votre client ACME a besoin d’un accès en écriture. Personne d’autre n’en a besoin. Ne montez pas /etc/letsencrypt en lecture-écriture dans une demi-douzaine de conteneurs applicatifs parce que c’est pratique.
Décidez le modèle de confiance :
- Hôte unique : stockez les certificats sur le système de fichiers de l’hôte, possédés par root, lisibles par groupe par un groupe spécifique que le conteneur proxy utilise.
- Plusieurs hôtes : évitez NFS pour les clés privées sauf si vous êtes très sûr du verrouillage de fichiers et de la sécurité. Préférez l’émission par hôte (DNS-01) ou un mécanisme de distribution de secrets avec des sémantiques de rotation.
Clés dans les images : ne le faites simplement pas
Incorporer des clés dans les images limite la carrière. Les images sont poussées vers des registres, mises en cache sur des ordinateurs portables, scannées par CI et parfois fuitées. Gardez les clés hors du contexte de build, hors des couches, hors de l’historique.
Renouvellement et rechargement : ce que « automatisation » signifie réellement
Le renouvellement a trois tâches :
- Obtenir un nouveau certificat avant l’expiration.
- Le placer là où le serveur l’attend.
- Faire en sorte que le serveur l’utilise sans interrompre le trafic.
Rechargement à chaud vs redémarrage
Certains proxies peuvent recharger les certificats sans couper les connexions. D’autres ne le peuvent pas. Vous devez savoir lequel vous avez, et vous devez le tester. « Redémarrer le conteneur chaque semaine » n’est pas une stratégie ; c’est une roulette russe avec un meilleur habillage marketing.
Les hooks sont vos amis
Si vous utilisez Certbot sur l’hôte, utilisez des deploy hooks pour recharger nginx/Traefik en douceur. Si vous utilisez une implémentation ACME native du proxy, confirmez comment elle persiste l’état ACME et comment elle gère le rechargement.
Blague n°2 : Rien ne construit la confiance comme un job de renouvellement TLS qui ne s’exécute que quand quelqu’un se souvient qu’il existe.
Tâches pratiques : commandes, sorties et décisions
Ceci n’est pas des extraits « copier-coller et prier ». Chaque tâche inclut ce que la sortie signifie et quelle décision vous prenez ensuite. Exécutez-les sur l’hôte sauf indication contraire.
Tâche 1 : Confirmer ce qui écoute réellement sur les ports 80/443
cr0x@server:~$ sudo ss -lntp | egrep ':80|:443'
LISTEN 0 4096 0.0.0.0:80 0.0.0.0:* users:(("docker-proxy",pid=1123,fd=4))
LISTEN 0 4096 0.0.0.0:443 0.0.0.0:* users:(("docker-proxy",pid=1144,fd=4))
Sens : Docker publie les deux ports. Cela implique qu’un conteneur est votre ingress. Si vous attendiez que nginx de l’hôte possède 80/443, vous avez déjà trouvé le conflit.
Décision : Identifiez quel conteneur mappe ces ports et confirmez qu’il s’agit du point unique de terminaison TLS.
Tâche 2 : Identifier le conteneur publiant les ports
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'
NAMES IMAGE PORTS
edge-proxy traefik:v3.1 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp
api myco/api:1.9.2 127.0.0.1:9000->9000/tcp
Sens : edge-proxy est le point d’entrée public. Bien. L’API est seulement locale.
Décision : Assurez-vous que tout le TLS public passe par edge-proxy et retirez toute exposition directe du 443 ailleurs.
Tâche 3 : Vérifier le certificat servi actuellement sur Internet
cr0x@server:~$ echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -subject -issuer -dates
subject=CN = example.com
issuer=C = US, O = Let's Encrypt, CN = R3
notBefore=Dec 1 03:12:10 2025 GMT
notAfter=Feb 29 03:12:09 2026 GMT
Sens : Le certificat en ligne expire le 29 février. C’est votre date butoir. Confirme aussi que le SNI est correct.
Décision : Si l’expiration est dans moins de 14 jours et que vous n’avez pas de pipeline de renouvellement vérifié, arrêtez ce que vous faites et corrigez ça d’abord.
Tâche 4 : Valider la chaîne complète et la qualité de la négociation
cr0x@server:~$ openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null | egrep 'Verify return code|subject=|issuer='
subject=CN = example.com
issuer=C = US, O = Let's Encrypt, CN = R3
Verify return code: 0 (ok)
Sens : La chaîne est bonne et les clients devraient la valider.
Décision : Si le code de vérification n’est pas 0, vérifiez si vous servez fullchain.pem vs cert.pem et si votre proxy est configuré pour le bon bundle.
Tâche 5 : Si vous utilisez Certbot sur l’hôte, lister les certificats et expirations
cr0x@server:~$ sudo certbot certificates
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Found the following certs:
Certificate Name: example.com
Domains: example.com www.example.com
Expiry Date: 2026-02-29 03:12:09+00:00 (VALID: 57 days)
Certificate Path: /etc/letsencrypt/live/example.com/fullchain.pem
Private Key Path: /etc/letsencrypt/live/example.com/privkey.pem
Sens : La vue de Certbot sur l’état. Si cela diffère de ce que montre openssl s_client, votre proxy ne lit pas les fichiers attendus.
Décision : Alignez la configuration du proxy avec les chemins live sous /etc/letsencrypt/live et assurez-vous que ces symlinks sont accessibles à l’intérieur du conteneur.
Tâche 6 : Renouvellements en dry-run (staging) pour vérifier le pipeline
cr0x@server:~$ sudo certbot renew --dry-run
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Processing /etc/letsencrypt/renewal/example.com.conf
Simulating renewal of an existing certificate for example.com and www.example.com
Congratulations, all simulated renewals succeeded:
/etc/letsencrypt/live/example.com/fullchain.pem (success)
Sens : Votre chemin de challenge, vos identifiants et hooks fonctionnent en staging. C’est le plus proche d’un test unitaire que vous obtenez.
Décision : Si cela échoue, ne prenez pas le risque d’attendre le renouvellement en production. Corrigez la défaillance maintenant.
Tâche 7 : Inspecter la joignabilité du challenge pour HTTP-01
cr0x@server:~$ curl -i http://example.com/.well-known/acme-challenge/ping
HTTP/1.1 404 Not Found
Server: traefik
Date: Sat, 03 Jan 2026 10:21:42 GMT
Content-Type: text/plain; charset=utf-8
Sens : Vous pouvez atteindre l’hôte et le proxy répond sur le port 80. Un 404 est acceptable pour cette URL synthétique ; ce qui compte est qu’il ne redirige pas vers HTTPS d’une manière que votre client ACME ne peut pas gérer.
Décision : Si vous obtenez un timeout de connexion, votre firewall/NAT/publication de port est incorrect. Si vous obtenez un 301 vers HTTPS, confirmez que votre client/proxy ACME le supporte en toute sécurité, ou créez une exception pour le chemin de challenge.
Tâche 8 : Vérifier les montages de volumes Docker pour le matériel des certificats
cr0x@server:~$ docker inspect edge-proxy --format '{{json .Mounts}}'
[{"Type":"bind","Source":"/etc/letsencrypt","Destination":"/etc/letsencrypt","Mode":"ro","RW":false,"Propagation":"rprivate"}]
Sens : Le proxy lit /etc/letsencrypt depuis l’hôte, en lecture seule. C’est la forme souhaitée.
Décision : Si c’est en RW alors que ça ne devrait pas, verrouillez-le. Si la source est un volume nommé, confirmez que vous pouvez le sauvegarder et le restaurer intentionnellement.
Tâche 9 : Confirmer les permissions et la propriété des fichiers des clés privées
cr0x@server:~$ sudo ls -l /etc/letsencrypt/live/example.com/privkey.pem
-rw------- 1 root root 1704 Dec 1 03:12 /etc/letsencrypt/live/example.com/privkey.pem
Sens : Seul root peut le lire. Si votre proxy tourne en non-root à l’intérieur du conteneur, il peut échouer à charger la clé.
Décision : Soit exécuter le proxy avec un utilisateur qui peut lire la clé via des permissions de groupe, soit utiliser un mécanisme contrôlé (comme un groupe dédié et chmod 640) plutôt que d’ouvrir l’accès à tout le monde.
Tâche 10 : Vérifier les logs du proxy pour les événements ACME et de rechargement des certificats
cr0x@server:~$ docker logs --since 2h edge-proxy | egrep -i 'acme|certificate|renew|challenge' | tail -n 20
time="2026-01-03T08:01:12Z" level=info msg="Renewing certificate from LE : {Main:example.com SANs:[www.example.com]}"
time="2026-01-03T08:01:15Z" level=info msg="Server responded with a certificate."
time="2026-01-03T08:01:15Z" level=info msg="Adding certificate for domain(s) example.com, www.example.com"
Sens : Le renouvellement a eu lieu et le proxy pense avoir chargé le nouveau certificat.
Décision : Si les logs montrent un renouvellement réussi mais que les clients voient toujours l’ancien certificat, vous avez probablement plusieurs instances d’ingress ou une couche de cache/load balancer qui sert un certificat différent.
Tâche 11 : Si vous utilisez des timers systemd pour Certbot, vérifier la planification et la dernière exécution
cr0x@server:~$ systemctl list-timers | grep -i certbot
Sun 2026-01-04 03:17:00 UTC 15h left Sat 2026-01-03 03:17:02 UTC 5h ago certbot.timer certbot.service
Sens : Le timer existe et a été exécuté récemment.
Décision : S’il n’existe pas, vous n’avez pas d’automatisation. S’il existe mais ne s’est pas exécuté, vérifiez si l’hôte était down ou si le timer est mal configuré.
Tâche 12 : Valider que les deploy hooks ont bien rechargé le proxy
cr0x@server:~$ sudo grep -R "deploy-hook" -n /etc/letsencrypt/renewal | head
/etc/letsencrypt/renewal/example.com.conf:12:deploy_hook = docker kill -s HUP edge-proxy
Sens : Après renouvellement, Certbot envoie HUP au conteneur proxy. C’est un pattern de rechargement contrôlé.
Décision : Confirmez que le proxy supporte le rechargement par SIGHUP. S’il ne le fait pas, remplacez le hook par la commande de rechargement correcte (ou un appel API), et testez-le en heures ouvrables.
Tâche 13 : Confirmer quel fichier de certificat le proxy est configuré pour utiliser
cr0x@server:~$ docker exec -it edge-proxy sh -c 'grep -R "fullchain.pem\|privkey.pem" -n /etc/traefik /etc/nginx 2>/dev/null | head'
/etc/nginx/conf.d/https.conf:8:ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
/etc/nginx/conf.d/https.conf:9:ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
Sens : Vous servez la chaîne complète et la clé depuis les chemins live standards.
Décision : Si vous voyez des chemins sous /tmp ou des répertoires spécifiques à l’app, attendez-vous à des surprises lors des redéploiements.
Tâche 14 : Vérifier le risque de limites de taux en comptant les tentatives échouées récentes
cr0x@server:~$ sudo awk '/urn:ietf:params:acme:error/ {count++} END {print count+0}' /var/log/letsencrypt/letsencrypt.log
0
Sens : Aucune entrée d’erreur ACME dans le log. Bien.
Décision : Si ce nombre augmente, arrêtez les retries automatisés et corrigez le problème de validation sous-jacent avant d’atteindre les limites de taux.
Tâche 15 : Confirmer que l’heure du conteneur est correcte (oui, cela compte)
cr0x@server:~$ docker exec -it edge-proxy date -u
Sat Jan 3 10:23:01 UTC 2026
Sens : L’heure est correcte. Une mauvaise heure peut faire échouer la validation de certificats de façons qui ressemblent à des « erreurs TLS aléatoires ».
Décision : Si l’heure est fausse, corrigez NTP sur l’hôte d’abord. Les conteneurs héritent de l’heure de l’hôte ; si elle est mauvaise, tout est mauvais.
Playbook de diagnostic rapide
Quand le TLS brûle, vous n’« enquêtez » pas. Vous triez. Voici l’ordre qui trouve le goulot rapidement.
Premier : est-ce le mauvais certificat qui est servi, ou aucun certificat ?
- Exécutez
openssl s_clientcontre le point d’accès public et vérifieznotAfter,subjectetissuer. - Si c’est expiré : vous avez une défaillance de renouvellement ou de rechargement.
- Si c’est le mauvais CN/SAN : vous touchez la mauvaise instance d’ingress, un mauvais routage SNI, ou un certificat par défaut.
Deuxième : le client ACME croit-il qu’il a renouvelé ?
- Vérifiez les logs Certbot/ACME pour des entrées de succès et des horodatages.
- Vérifiez les timestamps sur
fullchain.pemetprivkey.pemsur le système de fichiers. - Si les fichiers sont mis à jour mais que le certificat servi est ancien : c’est un problème de rechargement/distribution.
Troisième : le challenge peut-il être satisfait maintenant ?
- Pour HTTP-01 : confirmez la joignabilité du port 80 et que
/.well-known/acme-challenge/est routé vers le bon répondeur. - Pour DNS-01 : confirmez que les identifiants API DNS sont présents et valides, et vérifiez les délais de propagation.
Quatrième : confirmez qu’il n’y a qu’une seule source de vérité
- Cherchez plusieurs conteneurs ingress ou plusieurs hôtes derrière un load balancer qui ne partagent pas l’état des certificats intentionnellement.
- Assurez-vous que staging vs production ne sont pas mélangés.
Cinquième : limites de taux et tempêtes de retry
- Si vous voyez des échecs répétés, arrêtez temporairement le job de renouvellement. Les limites de taux sont impitoyables et prolongeront l’incident.
- Corrigez le routage/DNS sous-jacent, puis effectuez un retry contrôlé.
Erreurs courantes : symptôme → cause racine → correction
1) Symptom : le renouvellement « a réussi », mais les navigateurs affichent toujours l’ancien certificat
Cause racine : Le proxy n’a jamais rechargé les nouveaux fichiers, ou vous avez mis à jour les certificats sur un hôte pendant que le trafic atteignait un autre.
Correction : Ajoutez un deploy hook pour recharger le proxy (signal/API), et vérifiez quelle instance sert le trafic avec openssl s_client depuis plusieurs points de vue.
2) Symptom : Certbot échoue en HTTP-01 avec « connection refused » ou « timeout »
Cause racine : Le port 80 n’est pas joignable (firewall, NAT, mauvaise publication Docker) ou un autre service le lie.
Correction : Assurez-vous que le port 80 est publié par le conteneur ingress et autorisé via les groupes de sécurité/firewalls. Exécutez ss -lntp pour confirmer.
3) Symptom : HTTP-01 échoue avec « unauthorized » et le contenu du token est incorrect
Cause racine : Le chemin de challenge est redirigé/routé vers l’application, pas vers le répondeur ACME. Souvent causé par des règles « forcer HTTPS » appliquées trop tôt ou une règle de reverse proxy trop gourmande.
Correction : Ajoutez une route spécifique pour /.well-known/acme-challenge/ qui bypass les redirections et pointe vers le répondeur ACME.
4) Symptom : Vous atteignez les limites de taux de Let’s Encrypt pendant un incident
Cause racine : Les retries automatisés martèlent l’émission en production après des échecs répétés de challenge.
Correction : Utilisez --dry-run en staging pour les tests ; implémentez un backoff ; alertez sur les échecs. Pendant un incident, arrêtez le job et corrigez la joignabilité d’abord.
5) Symptom : Le proxy ne peut pas lire privkey.pem dans le conteneur
Cause racine : Les permissions sont en lecture uniquement pour root et le conteneur exécute le processus en non-root, ou l’étiquetage SELinux bloque l’accès.
Correction : Utilisez des permissions lisibles par un groupe dédié, exécutez le proxy avec ce groupe, et si SELinux est activé appliquez les labels corrects sur le bind mount.
6) Symptom : Après un redéploiement, les certificats disparaissent et le proxy sert un certificat par défaut/autosigné
Cause racine : Le stockage ACME était dans le système de fichiers du conteneur (éphémère) ou dans un volume non sauvegardé qui a été recréé.
Correction : Persistez le stockage ACME dans un volume nommé ou un bind mount avec sauvegardes. Traitez-le comme des données d’état.
7) Symptom : Les demandes de certificats wildcard échouent alors que HTTP-01 fonctionne
Cause racine : Les wildcards exigent DNS-01, pas HTTP-01.
Correction : Implémentez DNS-01 via l’API du fournisseur DNS, verrouillez les identifiants, et testez le comportement de propagation.
8) Symptom : « Ça marche sur un hôte mais pas sur un autre »
Cause racine : Split-brain : plusieurs nœuds d’ingress chacun émettant indépendamment, ou heure incohérente, ou configuration inconsistante.
Correction : Adoptez un modèle de propriété unique (émission par hôte avec DNS-01, ou ingress centralisé avec distribution de secrets) et imposez-le via la gestion de configuration.
Trois mini-récits d’entreprise depuis le terrain
Mini-récit 1 : L’incident causé par une mauvaise hypothèse
Ils avaient une pile Docker Compose propre : un reverse proxy, quelques APIs, un frontend et un service « certbot ». L’hypothèse était simple : le conteneur certbot renouvelle les certificats et le proxy commence magiquement à les utiliser. Personne n’a écrit la partie « magiquement ».
Le jour du renouvellement arriva. Certbot a renouvelé. Les fichiers sur le disque se sont mis à jour. Mais le processus proxy avait chargé le certificat au démarrage et n’a jamais vérifié à nouveau. Il servait joyeusement l’ancien certificat depuis la mémoire alors que le disque contenait le nouveau, comme un bibliothécaire qui refuse d’accepter les nouvelles éditions.
L’équipe a poursuivi des fausses pistes : DNS, règles de firewall, pannes Let’s Encrypt. Pendant ce temps les navigateurs criaient « certificat expiré » et les clients ont supposé une compromission. La sécurité est intervenue. La direction est intervenue. Le sommeil a quitté le bâtiment.
La correction a pris quelques minutes une fois identifiée : un deploy hook envoyant le signal de reload correct au conteneur proxy, plus une étape de validation qui comparait le certificat servi au certificat sur le système de fichiers après le renouvellement. La correction plus large a pris une semaine : ils ont ajouté une alerte « certificat expire dans 14 jours » et écrit un runbook qui commence par openssl s_client.
Mini-récit 2 : L’optimisation qui s’est retournée contre eux
Une autre entreprise voulait des déploiements plus rapides et moins de pièces en mouvement. Quelqu’un proposa : « Que chaque conteneur de service demande son propre certificat. Ainsi la scalabilité est facile et les équipes sont autonomes. » Cela sonnait moderne et aussi comme quelque chose que l’on peut dire en réunion sans être contredit.
Ça a marché un temps, comme une cuisine fonctionne quand personne ne cuisine. Puis une migration a eu lieu : des dizaines de services redéployés sur quelques heures. Chacun a essayé d’émettre un certificat. Certains ont utilisé staging, d’autres production, et quelques-uns avaient des noms de domaine mal configurés qui échouaient et retentaient agressivement.
Les limites de taux ont frappé. Certains services n’ont pas obtenu de certificats, et ont servi des fallback autosignés. Les clients qui utilisaient du pinning ont échoué sévèrement. Les tickets support ont explosé. L’incident était techniquement « juste des certificats », mais opérationnellement c’était un système distribué qui s’était auto-administré.
Ils sont revenus à un ingress centralisé qui émettait un petit ensemble de certificats et routait en interne. L’autonomie est revenue sous une meilleure forme : les équipes géraient les routes et les en-têtes, pas le cycle de vie des clés privées. L’optimisation avait été « supprimer le goulot ». Ce qu’ils avaient supprimé était le seul endroit où quelqu’un regardait.
Mini-récit 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise
Une entreprise plutôt régulée avait une règle peu glamour : tout TLS exposé sur Internet termine sur un cluster de proxy hardened, et l’état des certificats est sauvegardé dans l’état d’infrastructure. Les ingénieurs râlaient. C’était lent. Ça ressemblait à de la paperasserie.
Puis un changement inattendu de chaîne d’autorité de certification est arrivé dans l’écosystème et un sous-ensemble de clients anciens a mal réagi. L’équipe n’a pas eu à se précipiter dans 40 repos applicatifs pour chercher les réglages TLS. Ils ont ajusté la configuration d’edge, vérifié la présentation de la chaîne, et déployé un changement contrôlé avec un canari. Les apps n’ont pas bougé.
Plus tard la même année, un hôte est mort. L’hôte de remplacement est monté, la configuration appliquée, les certificats restaurés et le trafic a repris. Pas d’émission de dernière minute, pas de drame de limites de taux, pas de mystère « pourquoi le volume est vide ? ».
La manœuvre salvatrice n’a pas été des actes héroïques. Ce furent des frontières de propriété, des sauvegardes, et des hooks de reload testés trimestriellement. C’était ennuyeux comme des ceintures de sécurité, et tout aussi utile.
Listes de contrôle / plan pas à pas
Choisir un modèle (faites-le avant d’écrire des fichiers Compose)
- Hôte unique, ingress simple : Modèle A (ACME natif du proxy) ou Modèle B (Certbot sur hôte + proxy lit en lecture seule).
- Plusieurs hôtes derrière un LB : Préférez DNS-01 et émission par hôte, ou une approche centralisée de distribution de certificats. Évitez « NFS partagé de /etc/letsencrypt » sauf si vous comprenez vraiment les modes de défaillance.
- Hôtes fortement verrouillés : Modèle A avec un mécanisme de stockage ACME bien compris, plus sauvegardes et contrôles d’accès.
Checklist de durcissement (les trucs que vous regrettez d’avoir sautés)
- Une seule ingress publique publie les ports 80/443.
- Les certificats et clés sont persistés et sauvegardés.
- Les clés privées ne sont lisibles que par l’ingress (et le renouvelleur si séparé).
- Le dry-run en staging est testé et planifié.
- Le mécanisme de reload est implémenté et vérifié (signal/API/reload gracieux).
- Monitoring : alerter sur l’expiration des certificats, les échecs de renouvellement et les erreurs ACME.
- Runbook : la première commande est
openssl s_client, pas « vérifiez Grafana ».
Pas à pas : Certbot sur hôte + nginx/Traefik conteneurisé lisant en lecture seule
- Installez Certbot sur l’hôte et obtenez le certificat initial en utilisant une méthode compatible avec votre routage (standalone/webroot/DNS).
- Stockez les certificats dans
/etc/letsencryptsur l’hôte. - Bind-montez
/etc/letsencryptdans le conteneur proxy en lecture seule. - Configurez le proxy pour utiliser
fullchain.pemetprivkey.pem. - Ajoutez un deploy hook Certbot pour recharger le proxy en douceur.
- Activez et vérifiez un timer systemd pour les renouvellements.
- Exécutez
certbot renew --dry-runet vérifiez que le certificat servi en direct correspond aux fichiers après le reload.
Pas à pas : ACME natif du proxy (style Traefik/Caddy)
- Persistez l’état ACME sur un volume/bind mount (ce n’est pas optionnel).
- Verrouillez les permissions du stockage ACME (les clés de compte y vivent).
- N’utilisez HTTP-01 que si le port 80 est fiablement joignable ; sinon utilisez DNS-01 avec des identifiants API DNS scopiés.
- Testez le comportement de renouvellement et observez les logs pour les événements de renouvellement.
- Sauvegardez l’état ACME et testez la restauration sur une instance non production.
FAQ
Dois-je exécuter Certbot dans un conteneur ?
Vous pouvez, mais ce n’est pas recommandé par défaut. Si l’hôte peut exécuter Certbot, les renouvellements basés sur l’hôte plus les montages en lecture seule dans le proxy sont plus faciles à auditer et à récupérer.
Est-ce que l’ACME de Traefik/Caddy est « sûr » ?
Oui, si vous persistez et protégez le stockage ACME. La version dangereuse est de laisser les données ACME dans le système de fichiers éphémère du conteneur ou de les monter en RW partout.
Pourquoi ne pas terminer le TLS dans chaque conteneur applicatif ?
Parce que les clés privées se dispersent, les renouvellements se multiplient et le débogage devient une chasse au trésor. Centralisez le TLS à l’edge sauf si vous avez une exigence de conformité ou d’architecture spécifique.
Quel est le type de challenge le plus sûr avec Docker ?
DNS-01 est le plus adapté à l’infrastructure quand le port 80 est problématique (load balancers, firewalls verrouillés, ingresses multiples). Mais il transfère le risque aux identifiants API DNS et aux délais de propagation.
Dois-je ouvrir le port 80 si j’utilise HTTPS partout ?
Si vous utilisez HTTP-01, oui. Le serveur ACME doit récupérer le token via HTTP. La solution habituelle est : autoriser HTTP uniquement pour /.well-known/acme-challenge/ et rediriger tout le reste vers HTTPS.
Comment éviter les temps d’arrêt lors des renouvellements de certificats ?
Utilisez un proxy qui supporte le reload gracieux, et déclenchez-le via un deploy hook (ou fiez-vous au reload intégré du proxy). Vérifiez avec openssl s_client après le renouvellement.
Où devrais-je stocker les certificats sur le disque ?
Sur l’hôte dans un répertoire protégé (classiquement /etc/letsencrypt) si l’hôte exécute ACME. Ou dans un volume dédié et sauvegardé si le proxy exécute ACME. Gardez la clé privée lisible uniquement par ce qui doit la servir.
Que faire si j’ai plusieurs hôtes Docker derrière un load balancer ?
Choisissez une option : émission par hôte (DNS-01 est courant) ou une approche centralisée de distribution des certificats. Évitez les systèmes de fichiers partagés ad-hoc à moins d’avoir testé le verrouillage, les sauvegardes et le comportement de basculement.
Comment savoir si je suis proche des limites de taux de Let’s Encrypt ?
Regardez les erreurs ACME répétées dans vos logs et stoppez les tempêtes de retry. Quelques retries contrôlés sont acceptables ; des boucles serrées pendant une panne sont la manière dont vous vous retrouvez à attendre la période de refroidissement.
Les secrets Docker sont-ils un bon endroit pour les clés privées TLS ?
Dans Swarm, les secrets Docker peuvent être un bon primitif, mais vous avez toujours besoin de rotation et de sémantiques de reload. Dans un simple Docker Compose, les « secrets » se dégradent souvent en fichiers montés sans gestion du cycle de vie.
Conclusion : les prochaines étapes pratiques
Si vous ne prenez qu’une décision : centralisez la responsabilité TLS à l’ingress. Puis choisissez comment les certificats sont émis :
- Utilisez ACME natif du proxy quand vous pouvez persister et protéger proprement son état.
- Utilisez Certbot sur l’hôte quand vous voulez une planification, un journal et un contrôle du système de fichiers prévisibles.
- Utilisez Certbot en conteneur seulement quand les installations sur hôte sont interdites, et traitez le volume de certificats comme un magasin de secrets.
Prochaines étapes que vous pouvez faire cette semaine :
- Exécutez la vérification du « certificat servi » avec
openssl s_clientet enregistrez la date d’expiration quelque part de visible. - Faites un test de renouvellement en dry-run et corrigez les échecs pendant que vous n’êtes pas sous pression.
- Confirmez les sémantiques de reload et implémentez un deploy hook qui a fait ses preuves.
- Ajoutez une alerte : « certificat expire dans 14 jours ». Alerte ennuyeuse. Alerte salvatrice.
Après cela, vous pouvez discuter des suites de chiffrement et d’HTTP/3 comme des gens civilisés. D’abord, arrêtez que les certificats expirent.