Aller au contenu
Réseaux medium

Diagnostic TLS : déboguer les erreurs SSL avec openssl

16 min de lecture

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 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.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 ; la nuance suit.

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 :

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

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 :

Fenêtre de terminal
# Correct : le SNI demande explicitement le bon site
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 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 :

Fenêtre de terminal
# 1. Demander explicitement la verification du nom a 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 verifie le hostname par defaut
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 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 expire
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 REUSSIR
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 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 available

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

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) -> piege : chaine 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
# Enumeration 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.

  • La commande de référence est openssl s_client -connect host:443 -servername host ; le -servername (SNI) est indispensable.
  • Le Verify return code pointe 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 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.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn