SSL证书验证失败修复说明(通用bash版)
Python(requests)访问 HTTPS 报证书验证失败 —— 通用修复说明(纯 bash 版)
适用:Linux / WSL / macOS(bash 环境)
特征:公司网络存在 HTTPS 流量审计(TLS 中间人)代理
依赖:openssl、curl、awk、grep、Python 的certifi(多数环境默认已具备)
一、问题现象
执行需要访问 HTTPS 的命令(例如下载 qlib 数据)时报错,关键信息:
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED]
certificate verify failed: unable to get local issuer certificate
requests.exceptions.SSLError: HTTPSConnectionPool(host='github.com', port=443):
Max retries exceeded ...
(Caused by SSLError(SSLCertVerificationError(1,
'[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed ...')))
含义:Python 在 TLS 握手时无法把服务器证书链回溯到受信任的根证书,连接被中止。
二、问题原因(根因)
1. 网络出口存在 HTTPS 流量审计代理(TLS 中间人)
用下面的命令检查服务器证书链:
openssl s_client -connect github.com:443 -servername github.com </dev/null 2>/dev/null \
| grep -E '^ *[0-9]+ s:|^ *i:|Verify return code'
在本类环境中会看到签发者是公司内部 CA(出现公司名/内部域名/DC= AD 域结构),而不是
DigiCert、Let's Encrypt、ISRG、GlobalSign 等公共 CA,例如:
0 s:..., CN=github.com
i:..., CN=<公司>-CA ← 叶子证书由公司中间 CA 签发
1 s:..., CN=<公司>-CA
i:..., CN=<公司根 CA> ← 再向上是公司根
Verify return code: 20 (unable to get local issuer certificate)
公司代理会解开并重新签名所有 HTTPS 流量;客户端只有信任了该公司根 CA 才能验证通过。
2. 系统 / Python 的信任库不含公司根 CA
- Windows 通常已由 IT 安装并信任公司根证书,所以 Windows 浏览器访问正常;
- 但 WSL/Linux 是独立系统,其系统 CA 库以及 Python 的
certifi证书包
(requests默认使用的信任库)都不包含公司根 CA; - 因此 Python 报
unable to get local issuer certificate。
关键细节(实测):代理在握手时通常只下发"叶子证书 + 中间证书",不下发自签名根证书。
OpenSSL 3 / Python 默认要求信任到自签名根,所以仅把抓到的中间证书加入信任库仍然会失败
(错误变为unable to get issuer certificate)。必须额外获得根证书。
结论:这不是应用本身(如 qlib)的问题,也不是网络不通,而是 Linux/Python 缺少公司代理的根证书。
补充:很多下载链接会 302 重定向到资源站(如 release-assets.githubusercontent.com),
这些站点可能使用标准公共 CA(ISRG/Let's Encrypt),certifi 已默认信任;把公司根加入后两类站点都能访问。
三、修改策略(全程 bash,不改源码、不关闭证书校验)
- 用
openssl s_client -showcerts抓取代理下发的证书链; - 找到链顶证书;若其不是自签名根,则从证书的 AIA(Authority Information Access)
的 CA-Issuers(http)地址用curl下载根证书(http 地址无需证书校验); - 下载内容若为 DER 二进制,则用
openssl x509 -inform DER转成 PEM; - 构造合并证书包 = certifi 公共 CA + 公司根证书;
- 通过环境变量
REQUESTS_CA_BUNDLE/SSL_CERT_FILE指向合并包,并写入~/.bashrc永久生效; - (可选)把公司根安装到系统 CA 库,使
curl、git、apt等系统工具同样信任。
该方案保留证书校验(不使用 verify=False),安全、通用、可重复执行。
四、具体操作步骤
步骤 1(推荐,一键完成):运行通用脚本
将随附脚本保存为 setup_corporate_ca.sh,然后执行:
bash setup_corporate_ca.sh github.com ~/.corporate_ca_bundle.pem
参数:第 1 个为目标域名(默认 github.com),第 2 个为合并包输出路径(默认 ~/.corporate_ca_bundle.pem)。
脚本会自动:抓链 → 找链顶 → 必要时从 AIA 下载根并 DER→PEM → 合并 certifi → 写 ~/.bashrc → 连通性验证。
步骤 2(理解原理时可手工执行的等价命令)
# (1) 抓取链并拆分成单张 PEM(只保留 BEGIN/END 之间的内容)
openssl s_client -connect github.com:443 -servername github.com -showcerts </dev/null 2>/dev/null \
| awk '/-----BEGIN CERTIFICATE-----/{n++;f=sprintf("chain%02d.pem",n);c=1} c{print>f}
/-----END CERTIFICATE-----/{close(f);c=0}'
# (2) 查看链顶证书的 AIA,找到 http 形式的 CA Issuers 地址
openssl x509 -in chain02.pem -noout -text | grep -A4 'Authority Information Access'
# (3) 下载根证书;若是 DER 二进制则转 PEM
curl -sS -L -o root_raw 'http://<AIA中的CA-Issuers地址>'
openssl x509 -inform DER -in root_raw -outform PEM -out root.pem # 若已是PEM则跳过本转换
# (4) 合并 certifi + 根证书
python -c 'import certifi;print(certifi.where())' # 查看 certifi 路径
cat "$(python -c 'import certifi;print(certifi.where())')" root.pem > ~/.corporate_ca_bundle.pem
# (5) 当前终端立即生效,并写入 ~/.bashrc 永久生效
export REQUESTS_CA_BUNDLE=~/.corporate_ca_bundle.pem
export SSL_CERT_FILE=~/.corporate_ca_bundle.pem
grep -q REQUESTS_CA_BUNDLE ~/.bashrc || \
printf '\nexport REQUESTS_CA_BUNDLE=%s\nexport SSL_CERT_FILE=%s\n' \
"$HOME/.corporate_ca_bundle.pem" "$HOME/.corporate_ca_bundle.pem" >> ~/.bashrc
步骤 3(可选):安装到系统 CA 库
让 curl、git、apt 等系统级工具也信任公司根(Debian/Ubuntu/WSL):
sudo cp root.pem /usr/local/share/ca-certificates/corporate-root.crt
sudo update-ca-certificates
步骤 4:使配置生效并重新执行原命令
source ~/.bashrc # 或重新打开一个终端
python scripts/get_data.py qlib_data --target_dir ~/.qlib/qlib_data/cn_data --region cn
五、验证方法(如何确认修复成功)
# 1) openssl:返回码应为 0 (ok)
openssl s_client -connect github.com:443 -servername github.com \
-CAfile ~/.corporate_ca_bundle.pem </dev/null 2>/dev/null | grep 'Verify return code'
# 2) Python:应打印 200
REQUESTS_CA_BUNDLE=~/.corporate_ca_bundle.pem python -c \
"import requests;print(requests.get('https://github.com',timeout=30).status_code)"
实测输出示例:
Verify return code: 0 (ok)
verify status = 200
实际数据文件(跟随 302)也应可下载:
download = 200 | host = release-assets.githubusercontent.com | zip magic ok = True
返回码速查
| Verify return code | 含义 | 处置 |
|---|---|---|
0 (ok) |
验证成功 | — |
20 unable to get local issuer certificate |
信任库里没有根证书 | 需加入自签名根(本文方案) |
2 unable to get issuer certificate |
只信任到中间证书、缺上级根 | 说明只加了中间证书,仍需补根 |
21 unable to verify the first certificate |
服务器未下发中间证书 | 需补全中间证书 |
关于版本文件 404(如出现,属正常)
部分程序(如 qlib)会先请求带版本号的文件,不存在(404)后自动回退到 latest,
这是应用层逻辑,与 SSL 问题无关。
六、注意事项
- 本文适用于“证书链由公司代理 CA 签发”的场景;若是普通缺 CA,
可先sudo apt-get install --reinstall ca-certificates或升级 certifi。 - 脚本幂等,可重复执行;
~/.bashrc已含配置时不会重复写入。 - 使用其他 shell(zsh/fish)时,把同样的
export写入对应配置文件(如~/.zshrc)。 - certifi 升级后如需同步最新公共 CA,重新运行一次步骤 1 的脚本即可。
- 不要用
verify=False、--no-check-certificate等方式关闭校验,存在安全风险。 - 若证书未提供可用的 http CA-Issuers 地址,则需向 IT 安全部门索取公司根 CA(PEM/DER),
再从“步骤 2 的第(4)小步”开始手工合并。
七、文件清单
| 文件 | 说明 |
|---|---|
setup_corporate_ca.sh |
一键发现/下载根 CA、生成合并包、写环境变量的通用脚本 |
~/.corporate_ca_bundle.pem |
certifi 公共 CA + 公司根证书的合并包(Python 实际使用,勿删) |
root.pem |
从 AIA 下载并转换得到的公司自签名根证书(安装系统 CA 时使用) |
浙公网安备 33010602011771号