Aller au contenu principal
SXN Labs
Retour aux articles
e-santé TLS PKI Rails INSi Retour d'expérience 04 juin 2026

Brancher INSi avec TLS strict et continuité métier

Un mardi matin, sur un logiciel métier de santé en production dont j’assure le développement et la maintenance, tous les appels au téléservice INSi de la CNAM se sont mis à échouer côté serveur. Plus aucune vérification d’identité patient ne passait.

Côté utilisateur, le logiciel faisait ce qu’il devait faire : il n’affichait pas une stacktrace, il indiquait que la vérification INS n’avait pas abouti. Le patient était là, l’acte pouvait continuer, mais l’identité restait non vérifiée.

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 sujet n’était pas de reprendre l’UX ou le comportement métier. L’intégration INSi tournait en production avec une vérification TLS stricte et un comportement correct en cas d’échec ; l’incident a surtout rendu visible un sujet d’exploitation qu’il fallait expliciter : la confiance IGC-Santé.

Il fallait remettre le téléservice en marche, sans baisser la sécurité, et conserver cette propriété importante : une panne externe ne devait pas bloquer 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 :

La partie TLS n’était qu’un morceau du problème. Important, mais pas suffisant. Une intégration critique a aussi besoin d’une réponse explicite pour les jours où la dépendance externe ne répond pas.

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 :

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 :

  1. Télécharger la chaîne officielle depuis le portail ANS.
  2. 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.
  3. L’embarquer dans le projet, sous version, distinct des certificats clients (qui eux restent en credentials chiffrés).
  4. 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é.

Le comportement utile

Une fois la cause racine corrigée, il restait un point intéressant à expliciter : pourquoi l’incident n’avait pas bloqué le soignant ?

Parce que le logiciel ne traitait pas l’échec INSi comme une exception à jeter à l’écran, mais comme un état métier. Quand INSi répond, l’identité patient est vérifiée. Quand le téléservice ne répond pas, l’erreur technique reste dans Sentry et l’interface indique au soignant que la vérification INS n’a pas pu être effectuée.

La frontière applicative ressemble à ça, en simplifiant :

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 conserve 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’était déjà 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 être traduite en mode dégradé explicite.

Le travail invisible

Cet incident a aussi rappelé un point d’exploitation facile à sous-estimer. Les certificats clients INSi, MSSanté ou Pro Santé Connect sont renouvelés à la main sur le Portail de Confiance ANS, puis stockés dans des credentials chiffrés. L’application peut fonctionner parfaitement pendant des mois, jusqu’au jour où une échéance arrive en silence.

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, j’ai ajouté un job quotidien qui 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 :

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. Ici, cette décision existait déjà côté utilisateur : la vérification INS échouait, le soignant était informé, et le soin pouvait continuer.

Le vrai travail a donc été de couvrir la boucle complète : comprendre le besoin clinique, rendre la chaîne de confiance explicite, préserver la sécurité, s’appuyer sur le fallback métier existant, puis surveiller les certificats pour éviter de rejouer la même scène trois mois plus tard avec un autre certificat.

Si ce genre de situation vous parle

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.