Aller au contenu
5 minutes

Python et proxy TLS : CERTIFICATE_VERIFY_FAILED sans désactiver la vérification

Derrière un proxy qui inspecte le TLS, requests, httpx et huggingface_hub échouent sur la vérification. truststore, les variables de bundle et le piège du CDN Xet, sans jamais couper la vérification.

Python et proxy TLS : CERTIFICATE_VERIFY_FAILED sans désactiver la vérification

Un script Python qui fonctionne partout échoue sur le poste du bureau avec CERTIFICATE_VERIFY_FAILED. La réponse n’est pas de couper la vérification, mais de faire confiance à la bonne autorité de certification.

Le symptôme

Observé sur un poste Windows d’entreprise dont le proxy inspecte le TLS : httpx, requests et huggingface_hub échouent tous sur la même ligne.

[SSL: CERTIFICATE_VERIFY_FAILED] unable to get local issuer certificate

Le proxy re-signe le trafic avec une autorité de certification (CA) d’entreprise. Elle figure dans le magasin de certificats Windows, pas dans le bundle certifi que requests et httpx utilisent par défaut, selon leurs documentations.

Piège observé : pip install passait, l’index de paquets n’étant pas intercepté. Un pip qui réussit ne prouve rien pour les autres appels HTTPS.

truststore : utiliser le magasin du système

truststore expose les magasins natifs (CryptoAPI sous Windows, Security framework sous macOS, OpenSSL sous Linux) via une API calquée sur ssl.SSLContext. Il demande Python 3.10 ou plus.

import truststore
truststore.inject_into_ssl()   # en tout premier

import httpx                    # imports réseau ensuite

r = httpx.get("https://huggingface.co/api/models?limit=1")
r.raise_for_status()

inject_into_ssl() patche le module ssl : requests, urllib3 et aiohttp en profitent sans autre changement, et nous l’avons observé aussi avec httpx. La documentation pose deux règles : l’appeler le plus tôt possible, car un module qui a déjà importé ssl.SSLContext n’est pas affecté ; et le réserver aux applications et aux scripts, jamais à une bibliothèque.

Pour éviter le patch global, on passe un contexte à un seul client, comme dans l’exemple de la documentation httpx :

import ssl
import truststore
import httpx

ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
client = httpx.Client(verify=ctx)

La bibliothèque standard est moins exposée : sous Windows, ssl.create_default_context() charge déjà les magasins CA et ROOT du système. Ce sont les bibliothèques qui imposent certifi qui cassent.

Sans truststore : les variables de bundle

Si truststore n’est pas envisageable, exportez la chaîne de la CA d’entreprise au format PEM dans un fichier, puis désignez-le :

$env:REQUESTS_CA_BUNDLE = "C:\certs\bundle.pem"   # requests
$env:SSL_CERT_FILE      = "C:\certs\bundle.pem"   # httpx
  • REQUESTS_CA_BUNDLE, avec repli sur CURL_CA_BUNDLE, remplace le bundle par défaut de requests. Le fichier doit donc contenir toutes les autorités utiles, pas seulement celle du proxy.
  • requests ignore l’environnement dans le flux des requêtes préparées : la variable n’est pas lue et l’erreur revient, sauf à appeler Session.merge_environment_settings().
  • httpx respecte SSL_CERT_FILE et SSL_CERT_DIR par défaut.

Le cas Hugging Face

Depuis la v1.0, huggingface_hub utilise httpx à la place de requests. C’est donc SSL_CERT_FILE qui compte. Autre conséquence documentée : l’erreur SSL n’existe plus en tant que telle, elle devient un httpx.ConnectError générique. Lisez le message, pas le type d’exception.

Sur le même poste, un second ConnectError attendait :

ConnectError: [Errno 11001] getaddrinfo failed

Avec hf-xet présent, les gros fichiers passaient par un hôte du CDN Xet, cas-bridge.xethub.hf.co, que le DNS interne ne résolvait pas. huggingface.co répondait en 200 : l’API semblait joignable, seul le téléchargement cassait. Contournement retenu, le retour au transport HTTP classique :

import os
os.environ.setdefault("HF_HUB_DISABLE_XET", "1")   # avant l'import

import truststore
truststore.inject_into_ssl()

from huggingface_hub import hf_hub_download

L’ordre compte : la documentation Hugging Face précise que ses variables sont lues à l’import de huggingface_hub, toute modification ultérieure étant ignorée. Elle invite aussi, quand on désactive Xet, à transmettre un diagnostic à l’équipe du projet.

Ce qu’il faut retenir

  • CERTIFICATE_VERIFY_FAILED derrière un proxy d’entreprise signale une CA absente de certifi, pas une raison de couper la vérification.
  • truststore.inject_into_ssl() en tête de script, ou truststore.SSLContext passé à un client précis.
  • Sans truststore : REQUESTS_CA_BUNDLE pour requests, SSL_CERT_FILE pour httpx, avec un bundle complet.
  • huggingface_hub v1 passe par httpx : une erreur SSL y prend la forme d’un ConnectError.
  • getaddrinfo failed sur un gros fichier alors que l’API répond : HF_HUB_DISABLE_XET=1, posé avant l’import.

In English

Behind a TLS-inspecting proxy, requests, httpx and huggingface_hub fail certificate verification. truststore, bundle variables and the Xet CDN pitfall, without ever turning verification off. truststore makes libraries use the system store and must be injected before network imports, and huggingface_hub v1 goes through httpx, so it needs SSL_CERT_FILE rather than REQUESTS_CA_BUNDLE.

Sources & vérification

Testé sur : Poste Windows derrière un proxy à inspection TLS, huggingface_hub avec hf-xet

Comment les billets sont vérifiés et corrigés