Un mardi matin, sur un logiciel métier de santé en production, tous les appels au téléservice INSi de la CNAM se sont mis à renvoyer une 500. Plus aucune vérification d’identité patient ne passait.
L’interface, très professionnelle dans son approche du désastre, affichait une erreur brute au soignant. Le patient était là, l’acte devait continuer, et le logiciel répondait en substance : “OpenSSL a des états d’âme”.
L’erreur sous-jacente, remontée par Sentry, tenait en une ligne :
OpenSSL::SSL::SSLError: SSL_connect returned=1 errno=0
certificate verify failed (self-signed certificate in certificate chain)
Le diagnostic a pris vingt minutes. La correction propre, deux heures. Mais le vrai sujet n’était pas seulement de faire taire OpenSSL. Il fallait remettre le téléservice en marche, sans baisser la sécurité, et surtout éviter qu’une panne externe bloque le travail clinique.
Il existe une mauvaise réponse à ce problème, recopiée partout sur les forums, qu’il faut absolument éviter quand on transporte de l’identité patient. Elle “corrige” TLS à peu près comme couper le voyant moteur corrige une panne.
Le besoin réel
Le besoin métier n’était pas “réussir un handshake TLS”. C’était beaucoup plus simple :
- Vérifier l’identité INS d’un patient quand le téléservice répond.
- Permettre au soignant de continuer son acte quand le téléservice ne répond pas.
- Marquer clairement la donnée comme provisoire tant qu’elle n’a pas été vérifiée.
- Savoir avant la panne suivante qu’un certificat va expirer.
La partie TLS n’était qu’un morceau du problème. Important, mais pas suffisant. Une intégration critique qui remarche sans mode dégradé, c’est juste une prochaine 500 avec un meilleur certificat.
Ce que fait INSi
INSi est le téléservice de la CNAM qui permet à un logiciel métier d’aller chercher ou vérifier l’Identifiant National de Santé d’un patient à partir de ses traits civils : nom, prénom, date et lieu de naissance. C’est une brique centrale pour produire, échanger ou archiver des données de santé avec la bonne identité patient.
L’endpoint vit sur services-ps-tlsm.ameli.fr. C’est du SOAP, authentifié par certificat client délivré via le Portail de Confiance ANS. Côté serveur, la chaîne TLS remonte à la racine IGC-Santé, l’autorité de certification de l’État pour l’e-santé française.
Cette racine n’est dans aucun magasin de confiance système par défaut. Ni Debian, ni Ubuntu, ni macOS, ni le bundle Mozilla embarqué dans la plupart des libs HTTP. C’est une PKI gouvernementale spécialisée, séparée du Web PKI grand public. Autrement dit : si vous ne l’ajoutez pas explicitement, OpenSSL ne la devinera pas par communion administrative.
La fausse bonne idée
Si vous tapez “OpenSSL self-signed certificate in certificate chain Ruby” dans un moteur de recherche, vous tomberez sur dix réponses StackOverflow qui disent la même chose :
# NE FAITES PAS ÇA SUR UN FLUX D'IDENTITÉ PATIENT
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
Sur un script perso qui scrape un site avec un certificat auto-signé, chacun négocie avec sa conscience. Sur un flux qui transporte le numéro de sécu, le nom et la date de naissance d’un patient vers un service authentifié par certificat client ? Surtout pas.
Désactiver verify_mode, c’est :
- Accepter n’importe quel certificat en face, y compris un attaquant en MitM sur le réseau.
- Trouer la seule garantie cryptographique que la machine en face est bien celle de la CNAM.
- Inscrire dans le code, durablement, une régression de sécurité que personne ne reverra jamais.
Et pour rien : la vraie réponse demande peu de temps, elle est documentable, et elle laisse la sécurité en place. Détail appréciable quand on parle d’identité patient.
La correction propre
L’ANS publie publiquement la chaîne IGC-Santé sur son site (autorités racine + intermédiaires, format PEM). Le bon réflexe :
- Télécharger la chaîne officielle depuis le portail ANS.
- Vérifier les empreintes : SHA-256 du certificat récupéré comparé à celui présenté par l’endpoint live, et à celui publié par l’ANS. Trois sources, un seul hash. Si l’un des trois diffère, on s’arrête.
- L’embarquer dans le projet, sous version, distinct des certificats clients (qui eux restent en credentials chiffrés).
- Pointer OpenSSL dessus au moment d’instancier le client SOAP.
Côté Rails, ça donne, en simplifiant, ceci dans le service Ameli::Insi :
class Ameli::Insi
def http_client
Savon.client(
wsdl: wsdl_path,
ssl_cert_file: client_cert_path,
ssl_cert_key_file: client_key_path,
ssl_ca_cert_file: igc_sante_bundle_path, # <- la chaîne publique embarquée
ssl_verify_mode: :peer # <- on garde la vérif stricte
)
end
private
def igc_sante_bundle_path
Rails.root.join("lib", "certs", "igc_sante.pem").to_s
end
end
Pour valider de bout en bout avant même de redéployer, openssl fait le boulot :
openssl s_client \
-connect services-ps-tlsm.ameli.fr:443 \
-CAfile lib/certs/igc_sante.pem \
-servername services-ps-tlsm.ameli.fr \
-showcerts < /dev/null 2>&1 | grep "Verify return code"
Sortie attendue : Verify return code: 0 (ok). À partir de là, le client Ruby remarche, en VERIFY_PEER, sans aucune dégradation de sécurité.
La correction utile
Une fois la cause racine corrigée, il restait une question plus intéressante : pourquoi est-ce qu’une erreur transport sur un téléservice externe avait produit une 500 brute à la figure du soignant ?
Réponse : le service Ameli::Insi ne rescuait pas les erreurs HTTPI. Toute panne réseau, tout time-out, toute erreur TLS remontait jusqu’au contrôleur Rails, qui n’a rien d’autre à faire qu’un 500. Ce n’est pas un comportement métier. C’est une absence de décision produit avec une stacktrace au bout.
La correction tient en quelques lignes par méthode SOAP :
def fetch_by_traits(patient)
response = http_client.call(:fetch_by_traits, message: payload_for(patient))
Ameli::Insi::Identity.from_soap(response)
rescue HTTPI::Error, Savon::Error => e
Sentry.capture_exception(e, extra: { patient_id: patient.id })
raise Ameli::Insi::TeleserviceUnavailable
end
Le contrôleur appelant rescue TeleserviceUnavailable et bascule sur une identité provisoire (les traits civils saisis par le soignant, taggés “non-vérifiés INS”), avec un bandeau qui explique qu’INSi est indisponible et qu’il faudra re-vérifier plus tard. Le soignant continue son acte, le patient n’est pas bloqué, et la donnée sera consolidée au prochain appel réussi.
C’est le comportement utile. Pas spectaculaire, pas très vendeur en démo, mais c’est lui qui compte en production : une dépendance externe qui tombe ne doit pas arrêter la chaîne de soins. Elle doit basculer sur un mode dégradé explicite.
Le travail invisible
Cet incident a aussi révélé un angle mort. Les certificats clients INSi, MSSanté ou Pro Santé Connect sont renouvelés à la main sur le Portail de Confiance ANS, collés dans des credentials chiffrés, et ensuite plus personne ne les regarde. La méthode est artisanale, mais pas dans le bon sens du terme.
Le jour où l’un d’eux expire, on retrouve exactement la même panne, avec un sentiment de déjà-vu et une envie raisonnable d’insulter un calendrier.
Donc dans la foulée, un job quotidien parcourt l’inventaire des certificats embarqués : chaînes publiques et certificats clients. Il remonte à Sentry tout certificat à moins de 30 jours d’expiration, puis passe en erreur à 7 jours ou si le certificat est déjà expiré.
J’ai aussi ajouté un runbook de renouvellement versionné dans le repo (lib/certs/README.md), avec la procédure PFC pas à pas. Personne ne le verra tant que ça marche. Le jour où ça menace de casser, ce sera un avertissement exploitable, pas une panne surprise.
Mini-cartographie de la PKI e-santé française
Pour qui découvre l’écosystème, les briques se ressemblent et c’est facile de s’y perdre :
- INS : l’identifiant national de santé du patient (NIR ou NIA + traits). C’est la donnée.
- INSi : le téléservice CNAM qui permet de récupérer / vérifier un INS. C’est l’API.
- MSSanté : la messagerie sécurisée entre professionnels et avec le patient. PKI séparée, certificats émis par les opérateurs MSSanté.
- Pro Santé Connect : le SSO d’État pour les professionnels de santé (carte CPS dématérialisée, OIDC).
- IGC-Santé : la racine PKI commune qui signe l’essentiel de tout ça côté serveur ANS.
Chaque brique est documentée, chaque chaîne est publique et vérifiable. Le piège, c’est de traiter ça comme un détail technique gênant alors que c’est une partie du produit : identité, sécurité, continuité de service, procédures d’exploitation.
Ce que je retiens
Quand une intégration TLS casse en production, la bonne réponse n’est presque jamais “désactiver la vérification”. C’est : trouver le maillon de confiance manquant, le rendre explicite, le versionner, et l’auditer dans le temps. Le réflexe VERIFY_NONE économise dix minutes sur le moment et laisse une dette de sécurité qui vieillira tranquillement dans le code.
L’autre point, plus produit, c’est qu’une intégration critique doit avoir un mode dégradé. Un téléservice externe en panne, c’est normal sur dix ans d’exploitation. Un 500 sec qui bloque le métier, c’est un choix de design. Souvent implicite. Rarement bon.
Le vrai travail a donc été de couvrir la boucle complète : comprendre le besoin clinique, corriger la chaîne de confiance, préserver la sécurité, offrir un fallback métier, puis surveiller les certificats pour éviter de rejouer la même scène trois mois plus tard avec un autre certificat.
Si vous avez ça dans votre boîte
Les intégrations e-santé françaises (INSi, MSSanté, Pro Santé Connect, DMP), c’est un mélange de PKI, de certificats, de WSDL et de procédures ANS qui demandent plus de patience que de génie. Ça se fait. Ça marche. Mais il faut cadrer le besoin métier autant que le branchement technique.
C’est le genre de sujet que je traite chez SXN Labs : comprendre ce qui doit vraiment continuer à fonctionner, simplifier le périmètre, brancher proprement, puis laisser derrière un système exploitable. Si vous avez un téléservice santé coincé, écrivez-moi.