Aller au contenu
English
Compte : non connecté, synchronisation possible Mon apprentissage
Selectionner le thème
English
Read in English

Diagnostic TLS : déboguer les erreurs SSL avec openssl

Par mis à jour le

Améliorer cette page

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.

  • 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.
  • Connaissances de base sur HTTPS et TLS (voir le guide HTTP et HTTPS).
  • Un terminal Linux, macOS ou WSL avec openssl et curl installé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 :

Fenêtre de terminal
echo | openssl s_client -connect exemple.com:443 -servername exemple.com

Deux 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 -connect porte 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.3
Cipher : TLS_AES_256_GCM_SHA384
Verify 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.

Un certificat n'est jamais seul. Il s'inscrit dans une chaîne de confiance :

Certificat serveur (feuille) -> Intermédiaire(s) -> CA racine

Le 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 :

CodeMessage exactCause
0okchaîne complète et valide
10certificate has expireddate notAfter dépassée
18self-signed certificatecertificat auto-signé (feuille)
19self-signed certificate in certificate chainracine non fiable (CA inconnue)
20unable to get local issuer certificateémetteur introuvable (intermédiaire ou CA absente)
21unable to verify the first certificatechaî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.

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 :

Fenêtre de terminal
# 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 certificat
echo | openssl s_client -connect incomplete-chain.badssl.com:443 \
-servername incomplete-chain.badssl.com -showcerts 2>/dev/null \
| grep -c "BEGIN CERTIFICATE"
# 1

Le 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.

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 -brief
Peer certificate: CN = certificat-sni-localhost
$ openssl s_client -connect whoami.local.test:4443 -noservername -brief
Peer certificate: CN = certificat-par-defaut
$ openssl s_client -connect 127.0.0.1:4443 -brief
Peer certificate: CN = certificat-par-defaut
$ openssl s_client -connect 127.0.0.1:4443 -servername whoami.local.test -brief
Peer certificate: CN = certificat-sni-localhost

Le 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 :

Fenêtre de terminal
# Explicite : le SNI demande le bon site, quelle que soit la cible de -connect
echo | openssl s_client -connect exemple.com:443 -servername exemple.com 2>/dev/null \
| openssl x509 -noout -subject

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 :

Fenêtre de terminal
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 :

Fenêtre de terminal
# 1. Demander explicitement la vérification du nom à openssl
echo | 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éfaut
curl -Iv https://wrong.host.badssl.com
# curl: (60) SSL: no alternative certificate subject name matches target host name

Retenez : 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).

Un certificat expiré rend le code 10. Pour surveiller l'échéance, deux commandes :

Fenêtre de terminal
# 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.

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 :

Fenêtre de terminal
# Doit RÉUSSIR
echo | openssl s_client -connect exemple.com:443 -servername exemple.com -tls1_3 2>/dev/null | grep Protocol
# Protocol : TLSv1.3
echo | 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 available

Un é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.

Le service badssl.com expose des sous-domaines conçus pour déclencher chaque erreur. Reproduisez-les pour ancrer les codes :

  1. 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"; }
  2. 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)
  3. 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.comCode verifyErreur démontrée
expired10certificat expiré
self-signed18auto-signé (feuille)
untrusted-root19racine non fiable
incomplete-chain21chaîne incomplète
wrong.host0hostname non vérifié par openssl

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 :

OutilUsage
testssl.shaudit 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-ciphersnote chaque suite de A à F, lisible pour un rapport
curl -v / --cert-statusvoir le handshake et vérifier l'OCSP pendant une requête réelle
Fenêtre de terminal
# Audit complet d'un serveur
testssl.sh exemple.com
# Énumération rapide des suites avec notation
nmap --script ssl-enum-ciphers -p 443 exemple.com

testssl.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.

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.

  • 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 -servername est indispensable quand vous visez une adresse IP.
  • Le Verify return code pointe 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 0 trompeur) : utilisez curl ou -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.

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

8 questions
5 min.
70% requis

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

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.

Ce projet vous est utile ?

Les guides, formations, quiz et labs restent gratuits. Si vous les utilisez régulièrement, vous pouvez contribuer à leur maintenance et à leur développement.

Devenir membre de soutien (nouvel onglet) Soutenir une fois (nouvel onglet) En savoir plus

Suivez aussi le projet sur LinkedIn