快速修复
# 方案 A(临时救急,先装起来):跳过单个主机的证书校验
pip install --trusted-host <镜像域名> --index-url <镜像URL> <包名>
# 持久化:pip config set global.trusted-host <镜像域名>
# 方案 B(正规修复,推荐):把 CA 导入系统信任库,pip 指向系统 bundle
sudo cp 你的CA证书.crt /usr/local/share/ca-certificates/pipssl.crt
sudo update-ca-certificates
pip config set global.cert /etc/ssl/certs/ca-certificates.crt
# 然后正常 pip install,不需要任何 --trusted-host
你的 CA 证书从哪来:内网镜像/自建仓库找管理员要(一般叫ca.crt/rootCA.pem);公司代理找 IT 要拦截证书;浏览器能打开的话,可以在浏览器「证书查看器」里导出。
你会遇到什么
pip install 装包时突然报一长串:
ERROR: Exception:
HTTPSConnectionPool(host='pypi.org', port=443): Max retries exceeded with url: /simple/xxx/
SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1081)'))
更迷惑的是:浏览器打开 PyPI 正常,curl 下载也正常,只有 pip 报证书错;或者换了个国内源(清华/阿里)还是同一个错。常见场景:公司内网自建 PyPI 镜像(Nexus/devpi)、公司/校园网 SSL 拦截代理、私有仓库用了自签证书、旧证书过期。
遇到它先别急着 --trusted-host 一把梭——那是跳过校验,等于把 HTTPS 的锁拆了,装了个「看起来成功」但可能被中间人替换的包。
本文路径:快速修复(30 秒一段命令)→ 一步步排查(可跳读)→ 原理与变体(症状/原因/证据)。赶时间直接跳到「快速修复」。
为什么会这样
pip 走 HTTPS 下载包时,Python 的 ssl 模块会做证书链校验:服务器证书必须由某个「被信任的 CA」签发,且链上每一张证书都完整、未过期、域名匹配。校验失败时 pip 宁可报错也不下载——这是安全设计,不是 bug。
「信任链断了」通常有三种形态:
- 自签证书:服务器用的是自己签的证书(内网镜像、私有仓库常见),不在任何 CA 信任库里 →
self-signed certificate/unable to get local issuer certificate; - 公司代理 MITM:公司网络用 SSL 拦截代理检查流量,代理给每个站点签发「代理自己的证书」——浏览器装了代理的 CA 所以正常,pip 用的是独立的证书库没装 → 换什么源都报同样的证书错(证书是代理签的,不是源的);
- 证书过期/系统时间错:报
certificate has expired。
而 --trusted-host 的作用是告诉 pip 别校验这个主机的证书——不是修复,是跳过。实测(见验证证据):加 --trusted-host 后 SSL 阶段直接放行。临时救急可以,长期用等于裸奔:HTTPS 的加密在,但「对方是不是真的 pypi.org」没人验证了。
一步步来
1. 确认报错形态 — 必做
pip install --index-url https://<你的源>/simple/ 某个包 2>&1 | tail -5
# 看到 CERTIFICATE_VERIFY_FAILED / self-signed / unable to get local issuer 就是本页问题
顺手用 curl 看一眼服务器证书长啥样(区分「自签」还是「私有 CA」):
echo | openssl s_client -connect <源域名>:443 -showcerts 2>/dev/null | grep -E "subject=|issuer=" | head -4
# subject=CN=xxx 与 issuer=CN=xxx 相同 = 自签;issuer 是公司内部 CA = MITM/私有 CA
2. 判断属于哪种场景 — 必做
- 内网镜像/自建仓库 → 找管理员要 CA 证书(方案 B);
- 公司/校园网代理拦截 → 找 IT 要代理的 CA 证书;换源没用,因为证书是代理签的,换哪个域名都一样报错;
- 报
certificate has expired→ 先date看系统时间对不对,再考虑证书过期; - 浏览器正常、pip 报错 → 正是「pip 信任库没装 CA」的典型特征,方案 B 直接对症。
3. 正规修复:CA 导入系统信任库 — 必做
sudo cp ca.crt /usr/local/share/ca-certificates/pipssl.crt
sudo update-ca-certificates # 输出 "1 added" 即成功
4. 让 pip 用系统信任库 — 必做
pip config set global.cert /etc/ssl/certs/ca-certificates.crt
# 或环境变量:export PIP_CERT=/etc/ssl/certs/ca-certificates.crt
# 或单次:pip install --cert /etc/ssl/certs/ca-certificates.crt ...
为什么必须做这步:pip 自带独立的 CA 包(certifi),默认不读系统信任库。CA 只进系统库不够,还要让 pip 指向它(实测:只update-ca-certificates不带--cert依旧报错)。
5. 临时绕过只用于救急 — 可选
pip install --trusted-host <镜像域名> --index-url <镜像URL> <包名>
仅限「内网测试环境、马上要装」时用;装完尽快走方案 B,并在心里记一笔:跳过校验期间装的包,不保证来源可信。
怎么确认修好了
# 1. 不带任何 --trusted-host / --cert 也能装(用公网源或你的源都行)
pip install --index-url https://<你的源>/simple/ <包名>
# 2. 正向验证证书链真的被信任了
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt 你的证书.crt
# 输出 "OK" = 证书已被系统信任库接受
判定标准:不加跳过参数、不报 SSL 错误、装包成功,三条同时满足才算修好。
其它环境的表现
| 环境 | 表现与处理 |
|---|---|
| Python 3.14 + IP 字面量源(https://127.0.0.1:port) | 报 ValueError: check_hostname requires server_hostname(2026-09-01 实测):3.14 对 IP 字面量不发 SNI 导致 ssl 硬报错——源地址用域名(localhost 或内网域名),别用 IP |
| Windows | CA 导入:certutil -addstore Root ca.crt(受信任的根证书颁发机构);pip 指向:pip config set global.cert C:\path\ca-bundle.crt |
| macOS | 证书导入「钥匙串-系统」并信任;pip 同用 --cert 指向导出的 bundle |
| Docker 容器 | 容器默认不带宿主 CA,构建时 COPY ca.crt /usr/local/share/ca-certificates/ && update-ca-certificates;运行时同样要 pip config |
| 换源仍报证书错 | 大概率是代理 MITM:证书是代理签的,换域名没用,必须装代理的 CA(见「一步步来」第 2 步) |
| 系统时间错乱 | 报 certificate has expired:先 date 校时(sudo ntpdate / timedatectl),再谈证书 |
症状
pip install 报 SSLError(SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate) 或 unable to get local issuer certificate / certificate has expired;浏览器能打开包源、curl 正常,唯独 pip 失败;换源后错误不变。
原因
HTTPS 的证书校验链路是「客户端信任库 → 服务器证书 → 逐级验签直到根证书」:pip 用自己捆绑的 CA 包(certifi),与浏览器/系统信任库是三套独立列表——这就是「浏览器能开、curl 能下、pip 报错」的机制解释。校验有三个独立条件:①链上每张证书都在信任库可追溯(自签证书在链顶端直接断裂 → self-signed certificate);②证书未过期且系统时间正确(→ certificate has expired);③主机名与证书 SAN 匹配。公司代理 MITM 时,代理对每个站点签发的都是「代理自己的证书」,所以换源必然无效——证书不是源的,是代理的。确定性判断路径:openssl s_client -connect <host>:443 -showcerts 看 subject=/issuer= 是否相同(自签)或 issuer 是否为内部 CA(MITM);--trusted-host 是把「校验」整段短路,不是修复,装包前想清楚这一点。
pip 的 HTTPS 证书链校验失败:服务器证书不是由 pip 信任库(certifi)里任何 CA 签发。三种来源:自签证书(内网镜像/私有仓库)、公司 SSL 拦截代理的私有 CA(换源无效)、证书过期/系统时间错。--trusted-host 是跳过校验不是修复。
解法
- 正规修复:CA 证书导入系统信任库(
/usr/local/share/ca-certificates/+update-ca-certificates),再让 pip 指向系统 bundle(pip config set global.cert /etc/ssl/certs/ca-certificates.crt)——实测 SSL 阶段通过。 - 临时绕过:
pip install --trusted-host <域名> ...,仅救急,长期等于关掉 HTTPS 身份校验。 - 场景分流:内网镜像找管理员要 CA;公司代理找 IT 要拦截证书(换源无用);
expired先校时。 - Python 3.14 注意:源地址用域名不用 IP 字面量(实测会触发
check_hostname requires server_hostname)。
验证证据
- 2026-09-01 本机实测(WSL Ubuntu 26.04,Python 3.14.4,pip 25.1.1,自建 127.0.0.1:18443 HTTPS 源 + 自签 CA,SAN=DNS:localhost,IP:127.0.0.1):
实测复现日志(2026-09-01) - 基线:
pip install --index-url https://localhost:18443/simple/ six→[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1081)(实锤); - 快速绕过:
--trusted-host localhost→ SSL 阶段放行,后续报No matching distribution(实锤:跳过校验生效); - 正规修复:CA 复制进
/usr/local/share/ca-certificates/+update-ca-certificates+pip --cert /etc/ssl/certs/ca-certificates.crt→ SSL 阶段放行(实锤); - 负对照:撤销 CA + 不带
--cert→ 回到CERTIFICATE_VERIFY_FAILED: self-signed certificate(实锤:修复确实依赖信任库,不是缓存); - 变体:
https://127.0.0.1:18443(IP 字面量)→ValueError: check_hostname requires server_hostname(Python 3.14 新行为,实锤)。
相关 canonical
- pip 安装超时/慢(国内换源)——先换源再谈证书,换完源还报证书错就是本页问题
- 设置了代理环境变量却不生效(大小写坑)——代理通了但报证书错,多半是 MITM 证书没装
- JDK 老项目 TLS 协议升级——TLS 层同族问题(协议交集/证书链),Java 侧版本
适用工具 (1)
本问题方案已挂到下列工具的方案页——从工具页也能回到这里,双向可达。
pip · 外部工具来源与证据
← 编码代理与终端 问题域 | 问题货架 | 遇到其它问题?去 问题空间 提交求助卡