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 structurent la commande :
-connect host:443: la cible (hôte et port).-servername host: fixe le SNI. OpenSSL l'envoie déjà de lui-même quand-connectporte un nom DNS, mais il devient indispensable dès que vous visez une adresse IP (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. Le 20 apparaît quand l'émetteur manque dans le magasin local, par exemple une CA interne pas encore installée sur le client ; le 21 est le code final d'une chaîne incomplète côté serveur, détaillée ci-dessous.
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, Authority Information Access, l'URL où télécharger l'intermédiaire). 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 + intermédiaires)echo | openssl s_client -connect exemple.com:443 -servername exemple.com -showcerts 2>/dev/null \ | grep -c "BEGIN CERTIFICATE"# 4
# Chaîne incomplète : 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).
Depuis OpenSSL 1.1.1, s_client remplit le SNI tout seul avec le nom passé à -connect, à condition que ce soit un nom DNS : la page de manuel d'OpenSSL 3.0 le dit, et la mesure ci-dessous le confirme. Il n'envoie en revanche aucun SNI pour une adresse IP, ni pour un nom sans point comme localhost, et -noservername le supprime volontairement. Dans ces cas, le serveur répond avec son certificat par défaut, souvent celui d'un autre domaine, et vous diagnostiquez un faux « mauvais certificat ».
Mesuré avec OpenSSL 3.0.13, contre un openssl s_server qui présente un second certificat, de CN certificat-sni-localhost (le CN est un nom de lab, sans lien avec localhost), quand le SNI vaut whoami.local.test, et son certificat par défaut sinon. Seules les lignes Peer certificate sont reproduites :
$ openssl s_client -connect whoami.local.test:4443 -briefPeer certificate: CN = certificat-sni-localhost$ openssl s_client -connect whoami.local.test:4443 -noservername -briefPeer certificate: CN = certificat-par-defaut$ openssl s_client -connect 127.0.0.1:4443 -briefPeer certificate: CN = certificat-par-defaut$ openssl s_client -connect 127.0.0.1:4443 -servername whoami.local.test -briefPeer certificate: CN = certificat-sni-localhostLe cas qui piège en production est le test d'un backend ou d'un load balancer précis par son adresse IP : sans -servername, vous lisez le certificat par défaut de la machine, pas celui du site. Écrire -servername à chaque fois ne coûte rien et rend la commande juste quelle que soit la forme de -connect :
# Explicite : le SNI demande le bon site, quelle que soit la cible de -connectecho | 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 chaîne 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 vérification du nom à 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 vérifie le hostname par défautcurl -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 validité (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 sous 24h ou déjà expiréecho | 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 RÉUSSIRecho | 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 ÉCHOUER (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 vient du client : votre OpenSSL refuse lui-même la version, par son niveau de sécurité par défaut (SECLEVEL 2 sur Debian et Ubuntu) ou un MinProtocol dans openssl.cnf ; 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) -> piège : chaîne 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
# Énumération 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.
Mettre en pratique
Section intitulée « Mettre en pratique »Une sortie openssl se lit d'autant mieux qu'on l'a décortiquée soi-même : ce lab vous fait inspecter un certificat TLS réel, sa chaîne et ses dates de validité, directement dans un shell jetable.
Le second lab déplace la question sur un service publié : au lieu de lire un certificat, vous vérifiez l'identité qu'un point d'entrée présente et la chaîne qu'il sert réellement, depuis un client qui ne connaît que l'autorité attendue. Il demande une machine complète, et il appartient à la formation Publier un service web, qui reprend cette leçon à son compte.
À retenir
Section intitulée « À retenir »- La commande de référence est
openssl s_client -connect host:443 -servername host; OpenSSL envoie le SNI seul pour un nom DNS, mais-servernameest indispensable quand vous visez une adresse IP. - Le
Verify return codepointe la cause : 10 expiré, 18 auto-signé, 19 racine non fiable, 20 émetteur introuvable, 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.
Testez vos connaissances
Section intitulée « Testez vos connaissances »Huit questions sur les codes verify qui reviennent en incident, la chaîne incomplète qui passe au navigateur mais pas en ligne de commande, et ce qu'openssl s_client fait, ou ne fait pas, à votre place : envoyer le SNI et vérifier le nom d'hôte. Objectif évalué dans le parcours LFCS.
Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
Informations
- Le chronomètre démarre au clic sur Démarrer
- Questions à choix multiples, vrai/faux et réponses courtes
- Vous pouvez naviguer entre les questions
- Les résultats détaillés sont affichés à la fin
Lance le quiz et démarre le chronomètre
Vérification
(0/0)Score par domaine
Un domaine regroupe plusieurs guides et reçoit plusieurs questions : son score est plus fiable qu'un score par thème, qui porte souvent sur une seule question.
Bilan
Points forts
Aucun sujet réussi à 80 % sur au moins deux questions pour l’instant.
Points faibles
Aucun sujet sous le seuil de réussite.
À faire ensuite
Des indices pour retenter votre chance ?
Continuer le cours : Générer des certificats TLS avec openssl : clé, CSR et CA
Continuer le cours : Définir une politique de sécurité du point d'entrée
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications
FAQ : questions fréquentes sur le diagnostic TLS
Section intitulée « FAQ : questions fréquentes sur le diagnostic TLS »Les questions reviennent surtout sur la chaîne incomplète et sur l'écart entre navigateur et ligne de commande. La règle de la leçon tranche presque toujours : lisez d'abord le Verify return code, puis comptez les certificats envoyés.
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 fixe le SNI. Depuis OpenSSL 1.1.1, il est envoyé seul quand -connect porte un nom DNS, mais pas pour une adresse IP : un serveur mutualisé ou un CDN renvoie alors 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 ». Depuis OpenSSL 1.1.1,s_client l'envoie seul quand -connect porte un nom DNS, mais pas pour une adresse IP. -servername le fixe explicitement :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 = expiré ou expire sous 24 h
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, qu'il vienne du serveur (alerte protocol version) ou de votre propre client (no protocols available).