Un curl: (60) SSL certificate problem ou un certificat qui « marche dans le navigateur mais pas en ligne de commande » ? La réponse tient en une commande : openssl s_client -connect host:443 -servername host. Elle révèle la version TLS négociée, la chaîne de certificats et un code de vérification qui pointe la cause exacte. Ce guide vous apprend à lire cette sortie, à reconnaître les erreurs les plus fréquentes (chaîne incomplète, certificat expiré, mauvais SNI, hostname qui ne correspond pas) et à les reproduire dans un lab avec badssl.com.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Lire la sortie d'
openssl s_client: version TLS, suite de chiffrement, code de vérification. - Comprendre la chaîne de certificats et les codes verify (0, 10, 18, 19, 20, 21).
- Diagnostiquer le piège n°1 : la chaîne incomplète qui « marche dans Chrome mais pas avec curl ».
- Maîtriser le SNI (et son successeur chiffré ECH) pour obtenir le bon certificat.
- Éviter le faux positif du hostname : openssl ne vérifie pas le nom d'hôte par défaut.
- Surveiller l'expiration et tester les versions TLS acceptées par un serveur.
Prérequis
Section intitulée « Prérequis »- Connaissances de base sur HTTPS et TLS (voir le guide HTTP et HTTPS).
- Un terminal Linux, macOS ou WSL avec
openssletcurlinstallés. - Une connexion Internet pour atteindre les serveurs de test badssl.com.
openssl s_client : voir un certificat en une commande
Section intitulée « openssl s_client : voir un certificat en une commande »openssl s_client ouvre une connexion TLS vers un serveur et affiche tout ce qui s'y passe. C'est l'outil de diagnostic de référence :
echo | openssl s_client -connect exemple.com:443 -servername exemple.comDeux options sont indispensables :
-connect host:443: la cible (hôte et port).-servername host: envoie le SNI, sans quoi un serveur mutualisé ou un CDN renvoie le mauvais certificat (voir plus bas).
Le echo | ferme l'entrée standard pour que la commande ne reste pas bloquée après le handshake.
Lire la sortie : version, cipher, code de vérification
Section intitulée « Lire la sortie : version, cipher, code de vérification »Trois lignes résument l'état de la connexion. Voici la sortie réelle pour un site sain :
Protocol : TLSv1.3Cipher : TLS_AES_256_GCM_SHA384Verify return code: 0 (ok)Protocol: la version TLS réellement négociée (ici TLS 1.3).Cipher: la suite de chiffrement retenue.Verify return code: le résultat de la validation de la chaîne.0 (ok)= tout va bien ; tout code différent de 0 signale un problème.
Ce code de retour est le cœur du diagnostic. Apprenez à le lire et vous identifiez la cause en quelques secondes.
La chaîne de certificats et les codes verify
Section intitulée « La chaîne de certificats et les codes verify »Un certificat n'est jamais seul. Il s'inscrit dans une chaîne de confiance :
Certificat serveur (feuille) -> Intermédiaire(s) -> CA racineLe serveur doit envoyer la feuille et les intermédiaires ; la racine est déjà dans le magasin de confiance du système. Si un maillon manque ou n'est pas fiable, la validation échoue avec un code précis :
| Code | Message exact | Cause |
|---|---|---|
| 0 | ok | chaîne complète et valide |
| 10 | certificate has expired | date notAfter dépassée |
| 18 | self-signed certificate | certificat auto-signé (feuille) |
| 19 | self-signed certificate in certificate chain | racine non fiable (CA inconnue) |
| 20 | unable to get local issuer certificate | émetteur introuvable (intermédiaire ou CA absente) |
| 21 | unable to verify the first certificate | chaîne incomplète (intermédiaire manquant) |
Ces six codes couvrent la quasi-totalité des incidents TLS du quotidien. Les codes 20 et 21 pointent tous deux un problème de chaîne ; la nuance suit.
Le piège n°1 : la chaîne incomplète
Section intitulée « Le piège n°1 : la chaîne incomplète »C'est l'erreur la plus fréquente en production, et la plus déroutante : le site fonctionne dans le navigateur mais échoue avec curl ou openssl.
La raison : un navigateur sait compléter la chaîne tout seul (cache, récupération de l'intermédiaire via le champ AIA du certificat). curl et openssl, eux, ne le font pas : si le serveur n'envoie pas l'intermédiaire, ils ne peuvent pas remonter jusqu'à la racine.
Comptez les certificats envoyés avec -showcerts :
# Serveur sain : plusieurs certificats (feuille + intermediaires)echo | openssl s_client -connect exemple.com:443 -servername exemple.com -showcerts 2>/dev/null \ | grep -c "BEGIN CERTIFICATE"# 4
# Chaine incomplete : un seul certificatecho | openssl s_client -connect incomplete-chain.badssl.com:443 \ -servername incomplete-chain.badssl.com -showcerts 2>/dev/null \ | grep -c "BEGIN CERTIFICATE"# 1Le serveur défaillant n'envoie qu'un seul certificat et openssl rend :
Verify return code: 21 (unable to verify the first certificate)Correctif côté serveur : servir le fullchain (feuille + intermédiaires), pas seulement le certificat feuille. C'est exactement ce que recommandent les guides Nginx (ssl_certificate pointant sur le fullchain.pem) et Traefik avec Let's Encrypt, qui produisent une chaîne complète automatiquement.
SNI : obtenir le bon certificat
Section intitulée « SNI : obtenir le bon certificat »Le SNI (Server Name Indication) est le nom d'hôte envoyé dans le premier message du handshake pour que le serveur sache quel certificat présenter quand plusieurs sites partagent une IP (mutualisé, CDN, reverse proxy).
Sans -servername, openssl n'envoie aucun SNI : le serveur répond avec son certificat par défaut, souvent celui d'un autre domaine. Vous obtenez alors un faux « mauvais certificat ». Précisez toujours -servername pour reproduire le comportement d'un navigateur :
# Correct : le SNI demande explicitement le bon siteecho | openssl s_client -connect exemple.com:443 -servername exemple.com 2>/dev/null \ | openssl x509 -noout -subjectLe faux positif du hostname
Section intitulée « Le faux positif du hostname »Piège subtil et important : openssl ne vérifie pas le nom d'hôte par défaut. Il valide la chaîne, pas la correspondance entre le domaine demandé et le certificat.
Démonstration avec wrong.host.badssl.com, dont le certificat n'est valide que pour *.badssl.com :
echo | openssl s_client -connect wrong.host.badssl.com:443 \ -servername wrong.host.badssl.com 2>/dev/null | grep "Verify return code"# Verify return code: 0 (ok) <- la chaine est valide... mais le hostname ne correspond pas !Le code 0 ici est trompeur : la chaîne est bonne, mais le certificat ne couvre pas ce nom. Deux façons de détecter le hostname mismatch :
# 1. Demander explicitement la verification du nom a opensslecho | openssl s_client -connect wrong.host.badssl.com:443 \ -servername wrong.host.badssl.com -verify_hostname wrong.host.badssl.com 2>/dev/null \ | grep "Verify return code"
# 2. Utiliser curl, qui verifie le hostname par defautcurl -Iv https://wrong.host.badssl.com# curl: (60) SSL: no alternative certificate subject name matches target host nameRetenez : pour valider qu'un certificat couvre bien un domaine, fiez-vous à curl ou ajoutez -verify_hostname à openssl. Le nom doit figurer dans le CN ou, surtout, dans les SAN (Subject Alternative Names).
Vérifier l'expiration
Section intitulée « Vérifier l'expiration »Un certificat expiré rend le code 10. Pour surveiller l'échéance, deux commandes :
# Dates de validite (notBefore / notAfter)echo | openssl s_client -connect exemple.com:443 -servername exemple.com 2>/dev/null \ | openssl x509 -noout -dates# notBefore=May 31 21:39:12 2026 GMT# notAfter=Aug 29 21:41:26 2026 GMT
# Expire-t-il dans les 24h ? code de sortie 0 = encore valide, 1 = expire ou expireecho | openssl s_client -connect exemple.com:443 -servername exemple.com 2>/dev/null \ | openssl x509 -noout -checkend 86400 && echo "valide > 24h"-checkend N renvoie un code de sortie exploitable : c'est l'outil idéal d'un script de supervision qui alerte avant l'expiration. Avec l'arrivée des certificats à durée de vie courte (Let's Encrypt descend vers 6 à 45 jours), cette surveillance devient indispensable. Pour automatiser l'émission et la rotation, voir Vault PKI.
Tester les versions TLS acceptées
Section intitulée « Tester les versions TLS acceptées »En 2026, TLS 1.3 est la cible et TLS 1.2 reste accepté. TLS 1.0 et 1.1 sont dépréciés (RFC 8996, 2021). Un serveur correctement durci doit refuser les vieilles versions. Vérifiez-le en forçant chaque version :
# Doit REUSSIRecho | openssl s_client -connect exemple.com:443 -servername exemple.com -tls1_3 2>/dev/null | grep Protocol# Protocol : TLSv1.3echo | openssl s_client -connect exemple.com:443 -servername exemple.com -tls1_2 2>/dev/null | grep Protocol# Protocol : TLSv1.2
# Doit ECHOUER (serveur ou client qui n'accepte plus TLS 1.1)echo | openssl s_client -connect exemple.com:443 -tls1_1 2>&1 | grep -iE "no protocol|alert"# ...:no protocols availableUn échec sur -tls1_1 est donc le résultat attendu, pas un bug. Le message no protocols available apparaît quand votre openssl a été compilé sans cette version ; un serveur distant qui la refuse renvoie plutôt une alerte tlsv1 alert protocol version.
Lab : reproduire chaque erreur avec badssl.com
Section intitulée « Lab : reproduire chaque erreur avec badssl.com »Le service badssl.com expose des sous-domaines conçus pour déclencher chaque erreur. Reproduisez-les pour ancrer les codes :
-
Préparez la commande de test
Fenêtre de terminal verif() { echo | openssl s_client -connect "$1":443 -servername "$1" 2>/dev/null | grep "Verify return code"; } -
Déclenchez chaque erreur
Fenêtre de terminal verif expired.badssl.com # 10 (certificate has expired)verif self-signed.badssl.com # 18 (self-signed certificate)verif untrusted-root.badssl.com # 19 (self-signed certificate in certificate chain)verif incomplete-chain.badssl.com # 21 (unable to verify the first certificate) -
Observez le faux positif du hostname
Fenêtre de terminal verif wrong.host.badssl.com # 0 (ok) -> piege : chaine valide, hostname faux
| Sous-domaine badssl.com | Code verify | Erreur démontrée |
|---|---|---|
expired | 10 | certificat expiré |
self-signed | 18 | auto-signé (feuille) |
untrusted-root | 19 | racine non fiable |
incomplete-chain | 21 | chaîne incomplète |
wrong.host | 0 | hostname non vérifié par openssl |
Outils d'audit complets
Section intitulée « Outils d'audit complets »openssl s_client est parfait pour un diagnostic ciblé. Pour un audit complet d'un serveur (protocoles, suites, vulnérabilités), des outils dédiés vont plus loin :
| Outil | Usage |
|---|---|
| testssl.sh | audit TLS de référence : script Bash autonome, teste versions, suites, vulnérabilités, chaîne, OCSP |
| sslscan | énumère rapidement protocoles et suites supportés |
| nmap --script ssl-enum-ciphers | note chaque suite de A à F, lisible pour un rapport |
curl -v / --cert-status | voir le handshake et vérifier l'OCSP pendant une requête réelle |
# Audit complet d'un serveurtestssl.sh exemple.com
# Enumeration rapide des suites avec notationnmap --script ssl-enum-ciphers -p 443 exemple.comtestssl.sh est l'outil à connaître pour un audit reproductible : aucune dépendance lourde, sortie détaillée, et il détecte les vulnérabilités historiques (Heartbleed, ROBOT) en plus de la configuration.
À retenir
Section intitulée « À retenir »- La commande de référence est
openssl s_client -connect host:443 -servername host; le-servername(SNI) est indispensable. - Le
Verify return codepointe la cause : 10 expiré, 18 auto-signé, 19 racine non fiable, 20/21 chaîne incomplète. - « Marche dans le navigateur, pas avec curl » = presque toujours une chaîne incomplète : servez le fullchain.
- openssl ne vérifie pas le hostname par défaut (code
0trompeur) : utilisezcurlou-verify_hostname. - Surveillez l'expiration avec
openssl x509 -checkend 86400(code de sortie exploitable en supervision). - En 2026, ciblez TLS 1.3, acceptez TLS 1.2, refusez TLS 1.0/1.1 (RFC 8996).
- Pour un audit complet, testssl.sh est l'outil de référence.
FAQ : questions fréquentes sur le diagnostic TLS
Section intitulée « FAQ : questions fréquentes sur le diagnostic TLS »openssl s_client
# Connexion TLS complete (version, chaine, code de verification)
echo | openssl s_client -connect exemple.com:443 -servername exemple.com
# Juste l'essentiel du certificat
echo | openssl s_client -connect exemple.com:443 -servername exemple.com 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
Le echo | ferme l'entrée pour éviter que la commande reste bloquée. L'option -servername envoie le SNI : sans elle, un serveur mutualisé ou un CDN renvoie son certificat par défaut, souvent celui d'un autre domaine. Ajoutez -showcerts pour voir toute la chaîne envoyée.Chaîne incomplète
La cause numéro un : une chaîne incomplète. Le serveur envoie le certificat feuille mais oublie l'intermédiaire. Un navigateur sait le récupérer seul (cache, champ AIA) ; curl et openssl ne le font pas et échouent.Diagnostic : comptez les certificats envoyés.echo | openssl s_client -connect site:443 -servername site -showcerts 2>/dev/null \
| grep -c "BEGIN CERTIFICATE"
# 1 -> chaine incomplete (un seul certificat)
Le code de retour confirme : 20 (unable to get local issuer certificate) ou 21 (unable to verify the first certificate).Correctif : configurer le serveur pour servir le fullchain (feuille + intermédiaires), pas le seul certificat feuille.Le bon certificat sur une IP partagée
Le SNI (Server Name Indication) est le nom de domaine envoyé dans le premier message du handshake TLS, pour que le serveur choisisse quel certificat présenter quand plusieurs sites partagent une même IP (mutualisé, CDN, reverse proxy).Sans SNI, le serveur renvoie son certificat par défaut, souvent un autre domaine, d'où de faux « mauvais certificat ». Avec openssl, on l'envoie via-servername :echo | openssl s_client -connect exemple.com:443 -servername exemple.com
Le SNI circule en clair. Son successeur ECH (Encrypted Client Hello, RFC 9849, mars 2026) le chiffre, mais reste encore peu déployé côté serveur auto-hébergé.Les dates et checkend
# Lire les dates de validite
echo | openssl s_client -connect exemple.com:443 -servername exemple.com 2>/dev/null \
| openssl x509 -noout -dates
# notBefore=... / notAfter=...
# Alerte si le certificat expire dans 24h (86400 s)
echo | openssl s_client -connect exemple.com:443 -servername exemple.com 2>/dev/null \
| openssl x509 -noout -checkend 86400
# code de sortie 0 = encore valide, 1 = expire ou expire
Un certificat expiré donne Verify return code: 10 (certificate has expired). L'option -checkend renvoie un code de sortie exploitable : parfaite pour un script de supervision qui alerte avant l'échéance, d'autant plus utile avec les certificats à durée de vie courte (45 jours, voire 6).TLS 1.3 en cible, 1.2 en repli
| Version | État 2026 |
|---|---|
| TLS 1.3 | standard, handshake rapide |
| TLS 1.2 | accepté, repli pour clients anciens |
| TLS 1.0 / 1.1 | dépréciés (RFC 8996, 2021) |
| SSL (toutes versions) | mort, à désactiver |
# Doit echouer (resultat attendu)
echo | openssl s_client -connect exemple.com:443 -tls1_1
# Doit reussir
echo | openssl s_client -connect exemple.com:443 -servername exemple.com -tls1_3
Un échec sur -tls1_1 est le bon résultat : le serveur applique correctement la dépréciation.