pip だけ HTTPS で弾かれる ── 犯人はセキュリティソフトの SSL スキャンだった
ブラウザでは普通にサイトが見れるのに、pip install や uv、huggingface-cli だけが CERTIFICATE_VERIFY_FAILED で死ぬ。しばらく CA バンドルを無理やり指定して回避してたけど、そもそもの原因を追ったら犯人はセキュリティソフトだった、という話。
症状
こういうエラーが、Python 系のツールでだけ出る。
SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED]
certificate verify failed: unable to get local issuer certificate'))厄介なのが、
- ブラウザでは同じサイトが普通に開ける
pip/uv/git/huggingface-cliなどコマンドラインのツールだけが蹴られる
というちぐはぐな挙動。「ネットは繋がってるのに、なんで pip だけ?」となって、しばらく悩んだ。
なぜ開発ツールだけ蹴られるのか
鍵は 「誰の証明書ストアを見に行くか」 の違いだった。
- ブラウザや OS のツールは Windows の証明書ストア を見る
- Python (
certifi)、uv、gitなどは 自前の証明書バンドル を見る
もし通信の途中に「TLS を一度復号して中身を見て、自分の証明書で再暗号化して送り直す」中間者がいると、ツールに返ってくるのは本物のサイトの証明書ではなく 中間者が発行した証明書 になる。
Windows のストアにはその中間者のルート証明書が入っている(だからブラウザは静かに通る)けど、Python の certifi は知らない。「知らない発行者だ、偽物かもしれない」と検証を蹴る ── これが unable to get local issuer certificate の正体。
つまりエラーメッセージは嘘をついていない。「ローカル(=このツールが見ている証明書ストア)に、この発行者が居ない」と正確に言っている。
犯人の特定 ── 証明書の発行者を見る
「中間者がいるのか?いるなら誰か?」は、返ってくる証明書の 発行者 (Issuer) を見れば一発で分かる。検証なしで生の証明書を覗いて、発行者を出すだけ。
import socket, ssl
ctx = ssl.create_default_context() # 標準の検証。回避用 CA は入れない
for host in ["github.com", "pypi.org", "huggingface.co"]:
try:
with socket.create_connection((host, 443), timeout=15) as raw:
with ctx.wrap_socket(raw, server_hostname=host) as s:
issuer = {k: v for t in s.getpeercert()["issuer"] for (k, v) in t}
print(f"[{host}] OK / Issuer: {issuer.get('organizationName')} / {issuer.get('commonName')}")
except ssl.SSLCertVerificationError as e:
print(f"[{host}] 検証NG(傍受されてる): {e}")判定はシンプル。
- 本物の CA の名前が出て検証も通る → 傍受されていない
- 見慣れない名前(
○○ Web Protection、○○ Proxy CAみたいなの)が出る → そいつが中間者
自分の環境で実行したらこうなった。
[github.com] OK / Issuer: Sectigo Limited / Sectigo Public Server Authentication CA DV E36
[pypi.org] OK / Issuer: GlobalSign nv-sa / GlobalSign Atlas R3 DV TLS CA 2025 Q4
[huggingface.co] OK / Issuer: Amazon / Amazon RSA 2048 M04全部 本物の公的 CA(Sectigo / GlobalSign / Amazon)で、しかも標準の検証が通った。つまり 今は中間者がいない。
……で、「今は」というのがミソで、少し前まではここに傍受元の名前が出ていた。
犯人はセキュリティソフトの「SSL スキャン」だった
犯人はインストールしていた セキュリティソフト。多くの製品が「Web 保護」「SSL/TLS スキャン」「暗号化接続のスキャン」みたいな名前の機能で、まさにこの中間者をやっている。悪意があるわけではなく、通信の中身をウイルスチェックするために一度復号している。その副作用で開発ツールだけが巻き込まれていた。
セキュリティソフトをアンインストールしたら、上のとおり傍受が消えて、証明書は本物の CA に戻った。
アンインストールまでしなくても、多くの製品は設定で 「HTTPS スキャンを無効化」 や 「特定アプリを暗号化スキャンから除外」 ができる。まずはそこを探すのが穏当。
応急処置(傍受を残したまま通したい場合)
会社支給 PC など、どうしてもそのソフトを外せない事情がある場合。中間者の CA 証明書を書き出して、各ツールに「これも信頼していい」と教えれば通る。
Windows のストアから、その中間者のルート CA を PEM で書き出して(certmgr.msc から書き出すか、ソフトの設定画面からエクスポート)、環境変数で指定する。
$env:REQUESTS_CA_BUNDLE = "C:\path\to\corp_ca.pem" # requests / huggingface_hub 系
$env:SSL_CERT_FILE = "C:\path\to\corp_ca.pem" # python 標準 ssl
$env:CURL_CA_BUNDLE = "C:\path\to\corp_ca.pem" # curl
# uv は --native-tls(OS のストアを使う)で回避できることも多いこれは「中間者を信頼する」設定なので、正体が分かっている(自分の環境のセキュリティソフト等)ものにだけやること。素性の分からないネットワークで安易にやらない。
まとめ
- ブラウザは通るのに pip だけ落ちる → 証明書ストアの違い。中間者を疑う
sslで Issuer を見れば犯人が分かる。本物の CA なら傍受なし、変な名前なら中間者- Windows で開発ツールだけ TLS で蹴られるとき、犯人は セキュリティソフトの SSL スキャン のことが多い
- 根治はスキャン無効化 or 除外設定。応急処置は CA を書き出して環境変数で信頼
CA バンドルを指定して回避、は対症療法で、しばらくそれで凌いでいたけど、Issuer を一回見ておけば「何と戦っているのか」がはっきりして気持ちが楽になった。同じ症状で時間を溶かしている人の一助になれば。