Ce que TLS protège vraiment sur une connexion HTTPS

Chargez une page en http:// en clair sur un réseau que vous ne contrôlez pas, un aéroport ou le routeur d’un café, et tout ce que vous envoyez circule en texte lisible. Le chemin de l’URL, les cookies, les champs du formulaire, le mot de passe dans un POST de connexion : n’importe qui sur ce réseau avec un sniffer de paquets les lit au passage. TLS, Transport Layer Security, est le protocole qui bloque cela. Il se place entre TCP et le protocole applicatif, si bien que quand HTTP commence à parler, la connexion est déjà chiffrée et rattachée à une identité serveur vérifiée. HTTPS, c’est HTTP exécuté sur cette connexion au lieu d’une connexion en clair.

TLS combine trois garanties, qui ne tiennent qu’ensemble. Le chiffrement signifie qu’un observateur du réseau ne voit que du texte chiffré. L’intégrité signifie qu’un bit modifié en transit est détecté et que la connexion tombe au lieu de livrer des données altérées. L’authentification signifie que le client peut confirmer qu’il a atteint example.com et non celui qui a répondu en premier sur cette adresse IP. Perdez l’une des trois et les deux autres ne valent plus grand-chose : un canal chiffré vers un imposteur n’est pas un canal sécurisé.

Comment le handshake TLS établit un canal chiffré

Avant qu’une requête HTTP ne parte, le client et le serveur exécutent un handshake qui met d’accord sur une clé secrète partagée sans jamais la faire transiter sur le réseau. TLS 1.3, la version actuelle et celle que vous devriez utiliser, fait cela en un seul aller-retour, contre deux pour TLS 1.2.

1
2
3
4
5
6
7
8
9
Client                                           Server
  |--- ClientHello + key_share ------------------->|
  |    (versions supportées, cipher suites,        |
  |     une estimation du groupe d'échange)         |
  |<-- ServerHello + key_share --------------------|
  |<-- EncryptedExtensions, Certificate,           |
  |    CertificateVerify, Finished ----------------|
  |--- Finished ---------------------------------->|
  |======== données applicatives, chiffrées =======|

Le client envoie un ClientHello : les versions TLS et les cipher suites qu’il supporte, un nonce aléatoire, et une estimation du groupe d’échange de clés que le serveur choisira, envoyée en tant que key_share dans le même message. Cette estimation est le raccourci de TLS 1.3. Le serveur répond avec son propre key_share dans le ServerHello, et les deux côtés exécutent un échange Diffie-Hellman sur ces shares pour dériver indépendamment la même clé de session symétrique. La clé elle-même ne traverse jamais le réseau.

À partir de là, tout le reste est déjà chiffré sous cette clé de session : le certificat du serveur, sa preuve de détenir la clé privée correspondante, les messages Finished qui font le checksum de tout le handshake. Un aller-retour, et la connexion est active.

TLS 1.2 nécessitait un second aller-retour parce que l’échange de clés et la négociation du chiffrement s’exécutaient comme des étapes séparées au lieu de se chevaucher. Ce trajet supplémentaire, c’est de la latence pure. Sur une liaison avec 150ms de RTT, cela ajoute 150ms à chaque nouvelle connexion HTTPS, et c’est pourquoi TLS 1.3 s’est vu dans les temps de chargement des pages, pas seulement dans la posture de sécurité.

TLS 1.2TLS 1.3
Allers-retours jusqu’au premier octet chiffré21
Échange de clésNégocié après la cipher suiteEstimé et envoyé avec ClientHello
Renégociation en cours de sessionAutoriséeSupprimée
Échange de clés RSA statiqueAutoriséSupprimé (forward secrecy obligatoire)
Chiffrements faibles (RC4, CBC legacy)AutorisésSupprimés

Ce qu’un certificat prouve et comment fonctionne la chaîne de confiance

L’échange de clés s’occupe du chiffrement. Prouver que le serveur est vraiment example.com, c’est le travail du certificat. Un certificat lie une clé publique à un nom de domaine et porte la signature d’une autorité de certification (CA), une organisation dont la propre clé publique est déjà présente dans les magasins de confiance de votre système et de votre navigateur.

En pratique, la chaîne compte trois niveaux. Un certificat root CA, auto-signé et préinstallé partout, signe un certificat intermediate CA. L’intermédiaire signe le certificat feuille, celui de votre domaine. Les serveurs ne présentent presque jamais un certificat signé directement par une root : ils servent la feuille plus l’intermédiaire, et le client reconstruit la chaîne jusqu’à une root en laquelle il a déjà confiance.

Oublier de servir l’intermédiaire produit une panne qui ressemble à un fantôme. Les navigateurs ayant une copie en cache de cet intermédiaire se connectent sans problème, donc le site fonctionne sur votre portable ; tous les autres reçoivent une erreur de confiance. C’est l’une des erreurs de configuration TLS les plus courantes en production, et elle ne se reproduit jamais sur la machine depuis laquelle vous avez testé.

Un certificat porte aussi une fenêtre de validité (notBefore / notAfter) et les hostnames qu’il couvre, listés dans l’extension Subject Alternative Name (SAN). SAN est le champ que les clients lisent vraiment aujourd’hui ; l’ancien champ Common Name est obsolète. Un certificat pour example.com ne couvre pas api.example.com, sauf si ce nom figure aussi dans la liste SAN, ou si le certificat est un wildcard pour *.example.com.

Comment inspecter un certificat avec openssl s_client

Pas besoin de navigateur pour voir tout cela. openssl s_client ouvre une connexion TLS brute et affiche ce qui a été négocié :

1
openssl s_client -connect example.com:443 -servername example.com </dev/null

-servername définit le SNI (Server Name Indication), le hostname envoyé en clair dans le ClientHello pour qu’un serveur hébergeant plusieurs domaines sur une seule IP sache quel certificat présenter. Omettez-le et un serveur multi-tenant n’a aucun moyen de choisir.

Pour n’obtenir que la date d’expiration plutôt que tout le dump du handshake, redirigez vers openssl x509 :

1
2
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -dates -subject -issuer

Cela affiche notBefore, notAfter, le subject (le domaine pour lequel le certificat a été émis) et l’issuer (la CA qui l’a signé). Pour un script de monitoring qui n’a besoin que d’un pass/fail, -checkend prend une fenêtre en secondes et fixe le code de sortie :

1
2
3
4
openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -checkend 2592000 \
  && echo "OK : valide au moins 30 jours de plus" \
  || echo "ATTENTION : expire dans moins de 30 jours"

curl -v est plus rapide pour une vérification rapide, car il affiche la version TLS négociée et la chaîne de certificats dans sa trace détaillée du handshake :

1
curl -v https://example.com 2>&1 | grep -E "SSL connection|subject:|expire date"

Comment obtenir HTTPS sur localhost avec mkcert

Un certificat auto-signé brut, que vous générez et signez vous-même au lieu de l’obtenir d’une CA, vous donne le chiffrement mais pas l’authentification. Personne ne le garantit, donc chaque navigateur affiche un avertissement. Cela suffit pour un test rapide avec openssl. Agaçant pour le développement local, où vous voulez que https://myapp.local se charge sans avoir à cliquer pour passer l’avertissement.

mkcert résout cela en générant une CA locale, en l’installant dans les magasins de confiance du système et du navigateur, et en émettant des certificats signés par cette CA :

1
2
mkcert -install                          # génère et fait confiance à une CA locale
mkcert localhost 127.0.0.1 myapp.local   # émet un certificat pour ces noms

Vous obtenez une paire certificat/clé .pem dans le répertoire courant, en laquelle chaque navigateur de la machine a confiance parce qu’il fait maintenant confiance à la CA installée par mkcert. Pointez la configuration TLS de votre serveur de développement vers ces deux fichiers et l’avertissement disparaît.

Gardez rootCA-key.pem hors du contrôle de version. Quiconque détient la clé privée de la CA locale de mkcert peut générer un certificat pour n’importe quel domaine auquel votre machine fera confiance, la même classe d’exposition que celle décrite dans Variables d’Environnement et Secrets dans Docker pour les secrets dans les variables d’environnement : une clé privée qui fuite, c’est une identification qui a fuité.

Comment obtenir un certificat de production avec Let’s Encrypt et certbot

En production, il vous faut un certificat signé par une CA à laquelle le navigateur de chaque visiteur fait déjà confiance, et Let’s Encrypt est l’option gratuite et automatisée que la plupart des sites utilisent. certbot parle le protocole ACME avec les serveurs de Let’s Encrypt et, avec un plugin pour le serveur web, configure Nginx ou Apache pour vous :

1
sudo certbot --nginx -d example.com -d www.example.com

Cela prouve que vous contrôlez le domaine (en servant un token que certbot dépose, ou via un enregistrement DNS, selon le type de challenge), obtient le certificat et écrit les directives ssl_certificate et ssl_certificate_key dans votre configuration Nginx.

Les certificats Let’s Encrypt durent 90 jours. Cette fenêtre courte est délibérée : elle fait du renouvellement automatisé la seule voie viable. Certbot installe un timer systemd, ou une tâche cron sur les systèmes plus anciens, qui tourne deux fois par jour et renouvelle tout ce qui est à moins de 30 jours de l’expiration :

1
sudo certbot renew --deploy-hook "systemctl reload nginx"

--deploy-hook ne se déclenche que sur un renouvellement effectif, pas à chaque exécution du timer, donc Nginx n’est pas rechargé deux fois par jour pour rien. Testez le pipeline avant de lui faire confiance sans surveillance :

1
sudo certbot renew --dry-run

Comment fonctionne la terminaison TLS sur un reverse proxy ou un CDN

La plupart des services ne terminent jamais TLS dans le processus de l’application. Un reverse proxy devant votre app est l’endroit habituel pour cela : Nginx ou Caddy détient le certificat, déchiffre la connexion HTTPS entrante et transmet la requête au backend en HTTP clair sur le réseau interne. Le code de votre application ne voit jamais de certificat.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8080;
    }
}

fullchain.pem, c’est le certificat feuille avec l’intermédiaire déjà ajouté, le fichier avec la chaîne de confiance prêt à servir tel quel. Utilisez-le plutôt que cert.pem, qui est la feuille seule et réintroduit la panne d’intermédiaire manquant vue plus haut.

Un CDN fait le même travail un saut plus loin. Il termine TLS au PoP edge le plus proche du visiteur, puis ouvre une seconde connexion TLS vers votre origine. Deux connexions, deux handshakes, et le certificat que valide le navigateur du visiteur est le certificat edge du CDN. Votre certificat d’origine n’a qu’à satisfaire le CDN.

Cette séparation décide où déboguer. Si curl directement sur l’origine montre un certificat valide mais pas le domaine public, le problème se situe au niveau du proxy ou du CDN, et aucune lecture de logs applicatifs ne le trouvera.

Ce que HSTS ajoute par-dessus TLS

TLS sécurise une connexion une fois qu’elle est en HTTPS. Il ne fait rien pour la toute première requête, qui peut encore partir en http:// clair quand un utilisateur tape un domaine nu ou suit un vieux lien, et c’est exactement cette première requête qu’un attaquant sur le chemin peut intercepter et forcer en downgrade. Strict-Transport-Security comble cet écart : après une première visite HTTPS réussie, le navigateur réécrit chaque requête ultérieure vers ce domaine en HTTPS avant qu’elle ne quitte la machine.

1
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

max-age est en secondes, et 63072000, c’est deux ans, confortablement au-dessus du minimum d’un an exigé pour la soumission à la preload list. includeSubDomains étend la politique à chaque sous-domaine. preload marque le domaine comme éligible à la liste embarquée dans Chrome, Firefox et Safari, dont les entrées obtiennent la réécriture HTTPS uniquement dès la toute première requête, sans visite préalable nécessaire.

N’ajoutez preload que lorsque chaque sous-domaine sert vraiment du HTTPS. Sortir de la preload list prend des mois à se propager dans les versions des navigateurs, et jusque-là, tout sous-domaine incapable de faire du HTTPS devient inaccessible.

Quand TLS ne suffit pas, et quoi vérifier en premier

TLS vérifie le serveur auprès du client. Il ne dit rien de ce que ce serveur fait ensuite de votre requête, et il n’empêche pas un utilisateur victime de phishing de taper un mot de passe sur un faux domaine convaincant qui détient un certificat parfaitement valide. Let’s Encrypt délivre pour examp1e.com aussi facilement que pour example.com. Un cadenas signifie que la connexion est privée, pas que l’autre bout est honnête.

Trois pannes expliquent la plupart des signalements « le site est en panne » qui se révèlent être une mauvaise configuration TLS : un certificat expiré, un intermédiaire manquant, et une liste SAN qui ne couvre pas le hostname demandé. Les trois sont visibles dans la sortie de openssl s_client ci-dessus, avant même d’ouvrir un seul log applicatif.

Si vous terminez TLS vous-même, mettez cette commande -checkend dans une tâche cron ou une alerte de monitoring dès aujourd’hui. Le certificat qui fait tomber un site, c’est toujours celui dont plus personne ne se souvenait qu’il existait.

Articles associés