pip报错HTTPSConnectionPool?一文搞懂SSL证书验证失败与certifi修复

📅 发布时间:2026/9/9 18:02:56
pip报错HTTPSConnectionPool?一文搞懂SSL证书验证失败与certifi修复
1. 先把报错看明白HTTPSConnectionPool 到底在对我喊什么第一次看到这串报错的人十有八九会对着屏幕愣住三秒钟。明明昨天pip install还是好的今天一运行就吐出一大段 WARNING最后以一句HTTPSConnectionPool收场。更让人烦躁的是报错前面那几行“Retrying”看起来像是网络问题可 Web 页面能打开、浏览器下载文件也正常偏偏 pip 死活连不上 PyPI。我这里先给你看一个最典型的报错长相然后逐行拆开讲清楚它到底在说什么。WARNING: Retrying (Retry(total4, connectNone, readNone, redirectNone, statusNone)) after connection broken by SSLError(SSLCertVerificationError(1, [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:997))): /simple/pytest/ WARNING: Retrying (Retry(total3, connectNone, readNone, redirectNone, statusNone)) after connection broken by SSLError(...): /simple/pytest/ WARNING: Retrying (Retry(total2, connectNone, readNone, redirectNone, statusNone)) after connection broken by SSLError(...): /simple/pytest/ ... Could not fetch URL https://pypi.org/simple/pytest/: There was a problem confirming the ssl certificate: HTTPSConnectionPool(hostpypi.org, port443): Max retries exceeded with url: /simple/pytest/ (Caused by SSLError(SSLCertVerificationError(1, [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:997))))注意看最后一行HTTPSConnectionPool(hostpypi.org, port443)这一句信息量很大。它表示 TCP 层面的连接实际上已经建立了pip 已经成功连上了 pypi.org 的 443 端口问题发生在后续的 TLS 握手阶段也就是证书验证环节。很多人一看到“HTTPS”三个字就条件反射地去查防火墙、查代理、查 DNS其实方向从一开始就偏了。Max retries exceeded只是在告诉你 pip 按默认重试策略连续失败多次后放弃了它不是根因。真正值得盯住的是这个关键字[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:997)这句英文翻译成大白话就是Python 在尝试用本地信任的 CA 证书库去验证 pypi.org 的证书链时发现本地缺了某个证书导致整条链拼不起来于是直接拒绝连接连 HTTP 请求都没发出去。你可能会问为什么浏览器好好的、curl 也好好的偏偏 Python 不行这就要引出第二个问题了先把它放在这里后面我会专门展开说。不同的证书报错其实有不同的“长相”排查方向差别很大我列个表方便你对照报错关键字实际含义常见场景unable to get local issuer certificate本地证书库找不到能签发目标证书的根证书或中间证书企业网络设备替换了证书链、证书库不完整self-signed certificate in certificate chain证书链里出现了一个自签证书不受信任抓包工具、上网行为管理设备插入了自己的根证书certificate has expired证书已过有效期系统时间错误、证书确实过期hostname mismatch证书上的域名和实际访问的域名不匹配DNS 被劫持、走了错误的代理、hosts 被改如果你遇到的报错属于self-signed certificate in certificate chain那多半是某台设备在中间做了 TLS 解密再重新加密。这在国内办公网络里特别常见后面几章我讲怎么应对。如果是hostname mismatch那就要警惕是不是请求被导到了别的服务器上这种属于安全事件解决思路完全不同。还有个非常迷惑人的现象为什么“昨天还好好的今天突然就不行了”。我遇到过的情况主要有这么几类公司安全软件半夜自动升级顺手往设备里部署了新的根证书某次 Windows 系统更新把旧的根证书标记为不受信任系统时间被偷偷改乱导致证书验证时“当前时间”落到了有效期之外。所以排查时别急着只盯证书库先看一眼时间对不对这个检查成本最低也最容易被人忽略。2. 证书链验证的底层逻辑Python 的信任库和浏览器不一样想要彻底解决这类问题不能只靠“照着命令敲一遍”你得先理解 HTTPS 证书验证到底在验证什么。我打个比方你去银行办事柜员要核实你的身份证身份证是公安局签发的银行通过公安局的权威来确认你的身份。证书链也是同一个逻辑只是链条更长根证书相当于公安局中间证书相当于省公安厅服务器证书相当于你手里那张身份证。正常情况下pypi.org 的服务器会返回自己的服务器证书同时附上签发它的中间证书。客户端也就是你本机拿到这份证书后会从服务器证书向上追溯签发者直到找到一个本地已经信任的根证书再从根证书出发逐级校验签名全部通过才算验证成功。问题就出在这个“本地已经信任的根证书”上。不同的软件信任的根证书列表来源可能完全不一样。这里要澄清一个很多人搞错的关键点。pip 在底层使用的是 urllib3 这个 HTTP 库而 urllib3 在绝大多数 Python 环境里会优先读取certifi这个包提供的cacert.pem文件而不是直接使用操作系统自带的证书库。也就是说即使你在 Windows 的“受信任的根证书颁发机构”里装好了某个企业根证书浏览器和 curl 都认了pip 也照样不认因为它是从自己的cacert.pem里找根证书的。这是我在实际排查中最大的心得也是最容易让新手绕弯路的地方。很多人折腾半天往系统证书库里导入了证书浏览器访问正常了以为问题解决了结果一回终端pip install还是原封不动的报错然后就开始怀疑人生。Python 的证书查找机制大致可以按平台归纳一下环境证书来源排查优先级绝大多数 pip 安装场景urllib3 优先读certifi的cacert.pem最高优先级Python ssl 模块本身编译 OpenSSL 时的默认 CA 路径ssl.get_default_verify_paths()第二优先级Windows 系统ssl 模块部分场景会加载系统证书库辅助参考macOSPython 安装器附带Install Certificates.command脚本需要手动触发所以排查证书问题的时候第一步不是去看浏览器而是先搞清楚你当前的 Python 环境里 pip 到底在读哪个证书文件。这一步没搞清楚后面所有操作都可能是在对着空气打拳。回到“为什么浏览器正常、pip 异常”的问题。浏览器用的是操作系统证书库Windows 也好、macOS 也好都会自动从系统更新里同步证书而 Python 的 certifi 包相当于一个独立的“私房证书库”它不会跟着系统自动同步。如果企业网络里某台安全网关在中间插入了自己的证书Python 的证书库里可没有这个企业根证书验证自然就失败了。还有一类非常典型的环境是conda环境的 Python。conda 为了方便跨平台会自带一份 OpenSSL 库它的默认 CA 路径指向miniconda3\Library\ssl\cacert.pem这类位置和系统 Python 的证书完全不是同一份。于是你会看到同一个机器上系统 Python 装的包都能正常下载conda 环境里的 pip 却一直报 SSL 错误两个环境各管各的互相看不见。我在内网帮同事排查时经常遇到这种局面处理方式也很简单分别定位每个环境自己的证书库分别处理。理解了这个底层机制你再看网上一堆排错帖子就不会觉得乱了。有的人说“更新 certifi 就好”那是因为他的环境确实缺的是公共根证书有的人说“把证书装进系统库就行”那也只有在 curl 类软件报错时才成立。对 pip 而言真正的关键路径始终是 certifi 的cacert.pem和你 Python 环境里 ssl 模块默认读取的 CA 目录。3. 逐级修复从临时绕过到把证书真正装进信任库这一章给完整实操按“救急 → 定位 → 根治”的顺序走。每一步我都会告诉你要么这样做、要么那样做的判断依据让你不仅会敲命令还能知道自己为什么要敲。3.1 救急方案--trusted-host 临时绕过证书验证如果你现在被报错卡住急需装一个包干活最快的办法是pip install pytest --trusted-host pypi.org --trusted-host files.pythonhosted.org--trusted-host的作用是告诉 pip这个域名我信得过不用验证证书直接下载。这里的files.pythonhosted.org是 PyPI 真正存储安装包文件的域名只加 pypi.org 往往不够因为下载阶段访问的是 files.pythonhosted.org 这个子域。如果你是用了国内镜像源可以把域名替换成镜像源地址pip install pytest -i https://pypi.tuna.tsinghua.edu.cn/simple --trusted-host pypi.tuna.tsinghua.edu.cn强调一句--trusted-host只适合救急绝对不要当成长期方案写在 pip.conf 里长期使用。它的本质是关闭 TLS 证书校验等于放弃了 HTTPS 的防篡改能力中间任何人理论上都能往你下载的包里塞东西。你临时用它装一个包应急没问题但这背后真正的证书问题并没有解决等下一个环境、下一台机器出现同样问题时还得重新折腾一遍。3.2 定位你的 pip 到底在读哪个证书库救急之后回到正题。先跑这三条命令把当前环境的证书路径摸清楚python -c import ssl; print(ssl.get_default_verify_paths()) python -c import certifi; print(certifi.where()) python -c import urllib3; print(urllib3.util.ssl_.DEFAULT_CERTS)第一条命令会输出 OpenSSL 编译时设定的默认 CA 路径包括openssl.cnf的位置第二条输出 certifi 包的cacert.pem具体路径第三条确认 urllib3 实际使用的默认证书文件。这三条输出合并起来看基本就能确定 pip 是会读 certifi 文件还是会退回到 OpenSSL 默认路径。在大多数情况下输出结果里会有一个类似C:\Python39\lib\site-packages\certifi\cacert.pem或/usr/local/lib/python3.9/site-packages/certifi/cacert.pem的路径。对 pip 来说这个文件才是“主战场”。你先把这个路径记下来后面所有追加操作都在这个文件上做。3.3 看链用 openssl 拉出服务器真正返回的证书链不要急着把证书乱加一通。先用一条命令看看 pypi.org 服务器在面对你当前的网络环境时到底返回了怎样的证书链openssl s_client -connect pypi.org:443 -showcerts -servername pypi.org这条命令会建立一条真实的 TLS 连接然后把服务端在握手时发送的整条证书链打印出来。输出里会有多个-----BEGIN CERTIFICATE-----段每一段是一张证书。如果你在企业网络里开启了某个安全设备这里的证书链大概率不是 PyPI 官方的 Lets Encrypt 证书而是那个设备自己签发的证书。看清楚“服务端发来的链”和“你本地 truststore 里的链”到底差在哪一级才能决定要补哪张证书。实际操作中我发现很多时候不是根证书缺失而是中间证书缺失。企业网关在替换证书时只会下发自己的叶子证书和根证书中间证书没给全客户端拼不出完整链。这种情况光往 certifi 里追加根证书也没用还要把中间证书一并补上顺序通常是“叶子证书在前中间证书在后根证书在后”但追加到 cacert.pem 里的顺序本身并不敏感只要三张都在就行。3.4 根治把企业 CA 证书追加进 certifi 并让 pip 识别拿到需要补的证书之后要确认它是 PEM 格式。什么是 PEM 格式就是文本文件开头写着-----BEGIN CERTIFICATE-----的那种。如果拿到的证书是.cer或.crt的二进制 DER 格式先用 OpenSSL 转换openssl x509 -inform DER -in company.cer -out company.pem转成 PEM 之后先备份原文件再追加。Windows 环境type company.pem C:\Python39\Lib\site-packages\certifi\cacert.pemLinux 环境cat company.pem /usr/local/lib/python3.9/site-packages/certifi/cacert.pem追加完之后最好再验证一下文件末尾有没有多余的空行导致 PEM 块不能正确切割这个坑我踩过。用tail -n 5 cacert.pem看一下最后几行确保结尾直接是-----END CERTIFICATE-----换行结束。如果你不想直接改 certifi 的源文件还有一个更灵活的方式用环境变量指定 CA 文件。Python 的 ssl 模块和 urllib3 都认SSL_CERT_FILE这个环境变量requests 库则额外认REQUESTS_CA_BUNDLEset SSL_CERT_FILED:\certs\company-ca.pem set REQUESTS_CA_BUNDLED:\certs\company-ca.pem pip install pytest这种方式的好处是你可以把企业 CA 单独放一个文件管理不想用的时候直接删环境变量就行不用去改动 certifi 文件。缺点是每个新终端窗口都要重新设置比较麻烦。如果你确定这个环境长期要在企业内部网络使用直接改 certifi 的 cacert.pem 反而更干净毕竟它是一个 base64 文本文件追加内容不会对文件原有结构造成破坏。3.5 系统级证书库强烈建议同时处理虽然 pip 不读系统证书库但我还是建议你把企业根证书同时装进系统库里。原因很简单你会用到的工具不止 pipcurl、git、各种 IDE 的插件、Node.js 的 npm它们读取的可能就是系统证书库。只解决 pip 一个问题等于治标不治本。Windows 上双击.cer文件选择“安装证书”存储位置选“本地计算机”然后把证书放到“受信任的根证书颁发机构”。Linux 上sudo cp company.pem /usr/local/share/ca-certificates/ sudo update-ca-certificates这里再给一个额外的经验如果你在公司内网搭了私有 PyPI 源服务器证书也是自签的同样按照上面的办法把私有源 CA 追加到 certifi 就行。不用为了私有源特意关掉全局证书验证那样风险太大。4. 从报错到恢复的完整排查链路复盘理论和操作都说完了这一章我拿一次真实的排查过程做个完整复盘。某天我在办公网络下想装一个新包一运行就回退到HTTPSConnectionPool报错跟第一章开头的报错一模一样。整个排查过程我按下面这个顺序走每一步都有明确的判断依据。第一步先看范围。我打开终端跑了一句curl https://pypi.org/simple/结果 curl 居然成功返回了页面。这一步直接帮我排除了系统层面的网络封锁问题把矛头锁定在 Python 自身的证书库上。如果 curl 也报错那说明问题出在系统证书链或者 DNS 解析上排查方向就要往系统层面延伸。第二步检查代理配置。运行pip config list和echo %HTTPS_PROXY%确认 pip 有没有走企业代理。在办公网络里经常有人之前在 IDE 里配置过代理后来系统代理变了但环境变量还是旧的导致 pip 请求被导到一个不该去的地方。这一步排除了代理干扰。第三步看 DNS 解析结果。执行nslookup pypi.org确认解析到的是公网 IP 而不是内网地址。这一步是为了防止公司 DNS 私自把公网域名解析到内网网关导致我实际连接的是一个“假的 pypi.org”。第四步执行python -c import certifi; print(certifi.where())确认当前 pip 读的是哪个 cacert.pem 文件。同时用openssl s_client -connect pypi.org:443 -showcerts拉出了服务端证书链发现链上的证书确实是由公司安全网关自签的跟官方 PyPI 证书完全不一样。第五步导出公司的根证书转换成 PEM 格式追加到 certifi 的 cacert.pem。这里我特意先在一台干净的测试机器上验证了流程确认无误后才在要用的环境里操作。第六步重新执行pip install这次直接成功没有重试、没有警告。把整个过程整理成一张排查表方便你以后遇到同类问题时对照执行检查项命令结果判断系统层证书curl https://pypi.org/simple/失败→系统证书库问题成功→Python证书库问题代理环境变量echo %HTTPS_PROXY%/pip config list有值→确认代理是否可信DNS 解析nslookup pypi.org解析到内网 IP→检查 DNS 和 hostsPython证书路径python -c import certifi;print(certifi.where())确认 pip 到底读哪个文件服务端证书链openssl s_client -connect pypi.org:443 -showcerts对比本地信任库缺哪个环节除了上面这个标准流程我再分享几个特殊情况。有一次我排查了半天发现不是证书库的问题而是系统时间快了六个小时证书还在“未来有效期”被判定为无效。另一次是 conda 环境和系统 Python 并存系统 Python 修好了conda 环境里 pip 依旧报错因为 conda 环境用的 OpenSSL 路径完全不同得单独再处理一遍。这类问题本质都一样只是证书库位置不一样很多人忽略了“同一个机器上可能存在多套 Python”这个事实。5. 别让证书问题反复找上门长期预防经验问题解决了但我建议你多做一步想想怎么让它别再发生。说句实在话证书问题属于“环境类坑”不像业务代码那样修一次就彻底消失。换台电脑、装个新环境、公司安全策略调整都会让它再冒出来。所以我把长期预防的一些经验写在最后给你作参考。我现在的做法是新装一个 Python 环境之后会把下面这几件事做成一套固定流程升级 pip 到最新版旧版 pip 的报错信息比较晦涩升级后信息更清晰排查起来省不少时间。先跑python -c import certifi; print(certifi.where())确认 certifi 文件路径存在。如果在企业网络环境主动把企业根证书追加到 cacert.pem 里不等报错再处理。顺手配置国内镜像源清华、阿里都可以省得每次都在 PyPI 官方源上浪费下载时间。配镜像源一样要关心证书问题镜像源自己的证书如果过期或不被信任同样会报 HTTPSConnectionPool。再提醒一句使用 venv 隔离环境时有个容易被忽略的细节同一个 Python 解释器创建的多个 venv 是共享同一份 certifi 包的所以你在基础环境里追加了证书所有从这个解释器创建的 venv 都能直接识别。但 conda 环境是各管各的每个 conda 环境都有自己独立的 certifi 和 OpenSSL 配置换了环境就要重新处理。这也是为什么我遇到 conda 用户时会多问一句“你到底在哪个环境里跑的 pip”。还有一个非常实用的建议不要把所有证书都塞到 cacert.pem 里。我见过有人把三四个不同公司的证书全加进去加到最后文件混乱不堪出了新问题反而不知道是哪张证书在生效。更稳妥的方式是单独维护一个company-ca.pem文件用SSL_CERT_FILE环境变量指向它这样环境和证书解耦排查问题的时候把环境变量一去掉就切回默认状态方便对比验证。另外pip install --trusted-host这种救急命令我建议只在确认“临时装一个无关紧要的包”时使用用完就把 pip.conf 里对应的配置删掉。你可能会觉得无所谓但证书验证机制是有安全意义的它可以防止下载的包被中间人替换。一旦长期关闭等于给攻击者留了一扇门。最后说一个我自己的习惯。每次遇到HTTPSConnectionPool报错我不再焦虑也不急着搜答案而是按“时间对不对 → 代理有没有 → certifi 路径是什么 → 服务端返回了什么证书链”这个顺序过一遍基本五分钟内就能定位。这套思路完全可以复制到你自己的工作流里毕竟证书问题不可怕可怕的是没有一套稳定的排查方法。