这个报错我处理得实在太多了见得多到有点腻SSL certificate problem: unable to get local issuer certificate。第一次遇到的人基本都会懵——这句话到底在说什么“local issuer”指的是谁证书又是谁签的我先把这句话翻译成人话SSL 库在验证服务端证书时需要在你本地信任库中找到“给服务器证书签字的那张上级证书”结果没找到。这篇内容就是围绕这个错误做一次完整复盘从它常出现的现场到逐步排查的思路再到 Git、curl、Python、Node、Java 这些日常工具的修复方案最后聊聊为什么我强烈建议你不要一上来就关闭证书验证。适合被这个报错卡住的开发同学、做接口联调的测试朋友以及维护内部系统的运维同事。1. 这行报错到底在说什么先搞懂证书链和“本地信任库”1.1 HTTPS 不是在验证一张证书而是在验证一条担保链HTTPS 握手时服务器会把自己的证书发给客户端比如一张服务于example.com的叶子证书。客户端要做的事不是单纯确认这张叶子证书真实存在而是要顺着它的签名往上找这张证书是谁签发的那张签发证书又是谁签发的最终要一路追到一张客户端本来就信任的根证书CA Root。完整的证书链通常包含三层叶子证书服务于具体域名、中间 CA 证书给叶子证书签发、根 CA 证书给中间 CA 签发。服务器正常情况下会把叶子证书和中间 CA 证书一起发给客户端但根 CA 证书通常不随链下发因为根证书早就预置在你的电脑或软件里了。这就是unable to get local issuer certificate的真相客户端需要在本地信任库里找到“叶子证书的签发者”也就是中间证书或根证书却在本地没找到。可能是服务器只发了一张叶子证书中间环节断掉了也可能是中间证书发了但最上面的根不在本地。无论哪种都表现为无法构建一条完整的信任链。生活化类比你拿着一张某公司分公司开的介绍信去办事对方验证的不只是介绍信本身还要看开介绍信的分公司章是否在总公司备案过。如果对方没有总公司的印章样本他只能说“我无法核实这份介绍信的签发资格”。TLS 的本地信任库就是客户端保存的“总公司印章样本”。1.2 “本地”并不是单指操作系统这是很多人理解偏掉的地方。“本地”指当前程序运行机器上的一个信任库但它不是单一的存在。有些程序直接读操作系统的证书库Windows 证书存储、macOS 钥匙串、Linux 的/etc/ssl/certs有些程序自带一份 CA 列表比如 Python 的 requests 库内置cacert.pem、Node.js 内置根证书列表、JDK 自带cacerts、Git for Windows 自带ca-bundle.crt。所以同一个证书浏览器访问没问题不代表 curl 和程序也没问题反过来也是一样。这个错误最常出现在“浏览器能打开但命令行工具报错”的场景因为浏览器用的是系统或浏览器自己的信任库而命令行工具可能用它自己携带的那份。想明白这一点就不会再把它和网络不通混为一谈。2. 报错出现之前的现场还原五种高频场景对号入座2.1 Git 拉取或推送仓库时最常见的就是git clone https://git.xxx.internal/team/project.git或git push时终端直接弹出一段红字fatal: unable to access ...: SSL certificate problem: unable to get local issuer certificate这类情况多数是企业内部 Git 服务器使用了私有 CA 签发的证书Git 自己携带的 CA 列表里没有对应根证书。有的同事图省事直接执行git config --global http.sslVerify false然后整个 Git 就进入“裸奔”状态了。这个坑后面我会展开讲。2.2 curl 调试接口返回 60比如我随手敲curl -v https://api.internal.example.com/health很快就看到curl: (60) SSL certificate problem: unable to get local issuer certificatecurl 的机制也一样除非用--cacert显式指定否则它默认依赖系统 CA 文件而系统 CA 文件里没有企业私有根证书时就会报 60 号错。2.3 Python 脚本请求 HTTPS 接口requests.get(https://...)直接抛异常时报错文本会长一些requests.exceptions.SSLError: HTTPSConnectionPool(host..., port443): Max retries exceeded ... Caused by SSLError(SSLCertVerificationError(...) certificate verify failed ...)注意Python 这里不同环境、不同版本措辞有差异有段时间 macOS 上 Python 还容易报 “Python is not configured to use TLS/SSL” 之类但底层很多时候就是同一个问题在 requests 内置的证书库中找不到那张签发者证书。2.4 Java 联调接口提示 PKIX path building failedJava 的报错经常长这样sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target很多初学者觉得这跟“SSL certificate problem”是两码事其实底子一致JDK 的cacerts信任库里缺少对应的根证书或中间证书。只是 JDK 用了另一套证书校验框架所以错误文案完全不同。2.5 公司代理与抓包工具介入时公司为了安全审计在网络出口做 TLS 解密或者你在开发时挂了 Charles、Fiddler 之类的代理抓包。这些工具会为每个 HTTPS 连接动态签发证书相当于一个“中间人”。如果你要调试的脚本或命令行工具不信任这个代理的根证书一样会报本文标题这个错。这些场景我都实操遇过不止一次也踩过“直接关闭验证”的坑。下面分享我认为最值得遵循的排查顺序。3. 定位断点三步走服务端没发全还是本地不认账3.1 从 openssl 输出的证书链找答案遇到报错第一件事我不会去翻配置文件而是先问服务端你到底把证书链完整发过来了吗用这条命令看openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts /dev/null 2/dev/null | grep -E s:|i:输出大概长这样s:/CUS/CNapi.example.com i:/CUS/OExample Intermediate CA s:/CUS/OExample Intermediate CA i:/CUS/OExample Root CA-servername是触发 SNI 用的很多多域名服务器不写它会返回默认证书。grep里s:是当前证书的 Subjecti:是它的 Issuer也就是签发者。如果输出里只有一行比如s:是api.example.comi:是某个 CA那就说明服务端只发了叶子证书没有把中间证书一起发过来。这是很典型的服务端证书链配置不完整Nginx 需要把网站证书和中间证书按顺序串成一个文件后填到ssl_certificateApache 也有类似配置。这种问题你在客户端改任何配置都没用只能让运维去修服务端。如果输出里出现了多行也就是叶子证书和中间证书都在那服务端这边基本没问题可以进入下一步。3.2 对照本地信任库挨个排查服务端链完整报错还出现问题就在本地信任库。这时候要找出“链顶层那张根证书”在本地是否存在。Linux 上直接看ls -l /etc/ssl/certs/ca-certificates.crt或者检索grep -A 20 Example Root CA /etc/ssl/certs/ca-certificates.crtWindows 上按Win R输入certlm.msc打开本地计算机证书在“受信任的根证书颁发机构”里找。macOS 则用“钥匙串访问”查看“系统”分类下的证书。如果工具用的是自带信任库就要单独查。Git 的 CA 文件位置可以通过git config --global --get http.sslCAInfo看Python 的 requests 用 certifi 包直接检查site-packages/certifi/cacert.pem里面有没有对应根证书JDK 可以执行keytool -list -keystore $JAVA_HOME/lib/security/cacerts -storepass changeit | grep -i example找到缺失项之后再把根证书补进去。这一步的核心结论是别只盯着一种信任库要看你报错的那个工具到底读的是哪一份。3.3 系统时间必须看一眼这个点最容易忽略。TLS 证书的有效期判断依赖本地时间本机时间如果比真实时间慢或快超过容忍范围很多证书会被判定为“尚未生效”或“已过期”同样会触发验证失败。我见过一台内网服务器因为主板电池失效时间停在两年前项目组排查了整个下午最后发现只是时间同步没做。在 Linux 上先date看一眼再timedatectl status确认Windows 上如果时间明显不对先手动同步。时间对了之后证书错误可能直接就消失了。3.4 为什么不能跳过排查直接关验证有些人看到报错就开始改配置Git 关sslVerify、curl 加-k、Python 加verifyFalse。这样做确实能绕过去但你永远不知道链到底断在哪。这次侥幸连上了下次换一台机器、换一个仓库同样的报错又会冒出来。正确路线是先确认服务端链完整吗、本地缺哪一张、时间对吗然后再进入下一节的按工具修复。4. 分工具落地修复Git、curl、Python、Node、Java 一套配齐4.1 Git指定 CA Bundle而不是关掉验证Git for Windows 可能使用 OpenSSL 后端也可能用 Windows 的 schannel 后端取决于安装时的选项。最稳妥的做法是显式指定一个 CA 文件git config --global http.sslCAInfo /path/to/your-root-ca.pem如果你的私有 CA 有多张根证书可以拼成一个 PEM 文件给 Git。如果确定 Git 走的是 schannel 后端且根证书已经导入 Windows 系统信任库也可以试试git config --global http.schannelUseSSLCAInfo true无论哪种后端都建议确认一下验证没有被关掉git config --global --get http.sslVerify输出应该是true。如果有人之前把它设成了false立刻改回来git config --global http.sslVerify true4.2 curl命令行与系统环境变量两条路临时调试可以用curl --cacert /path/to/ca.pem https://api.example.com/health希望全局生效可以设置环境变量export CURL_CA_BUNDLE/path/to/ca.pem如果是在 Windows 的 PowerShell 里就用$env:CURL_CA_BUNDLEC:\path\ca.pem。生产环境的脚本我更推荐在命令行级指定--cacert避免环境变量影响其他无关命令。需要提醒一句curl -k是应急手段不是修复方案。它会把证书验证完全关闭局域网里任何人伪造服务器证书curl 都会照单全收。4.3 Python/requestsverify 参数与 CA 环境变量requests 的信任库来源于 certifi 包。企业内部接私有 CA 最直接的方式是每次请求指定import requests resp requests.get( https://api.internal.example.com/health, verify/path/to/ca.pem, ) print(resp.status_code)如果希望整个项目都生效可以用环境变量export REQUESTS_CA_BUNDLE/path/to/ca.pem export SSL_CERT_FILE/path/to/ca.pemrequests 优先读取REQUESTS_CA_BUNDLE而更底层的 urllib 原生调用会看SSL_CERT_FILE所以两个变量都设到同一份 bundle覆盖面更广。这里有个细节verifyFalse确实能立刻“解决问题”但同时会产生InsecureRequestWarning警告。这个警告本身就是告诉你当前处在不可信连接中。调试时可以临时用一次长期脚本里出现它我会建议直接拉黑。4.4 Node.jsNODE_EXTRA_CA_CERTS 与 npm cafileNode 程序访问 HTTPS 时可以追加证书export NODE_EXTRA_CA_CERTS/path/to/ca.pem这个环境变量会把证书追加到默认信任库之后不会覆盖 Node 内置根证书。注意它只对 Node 的 TLS 模块生效对 npm 安装依赖时的请求不生效。npm 访问私有 registry 时正确配置是npm config set cafile /path/to/ca.pem不是strict-ssl false。网上很多旧教程让人设strict-sslfalse这是一颗定时炸弹尤其是 registry 地址被劫持时npm 将没有防护能力。cafile才是正确的姿势它告诉 npm 使用哪张根证书去校验证书链。4.5 Java导入 cacerts 与自定义 TrustStoreJDK 的信任库默认位置在$JAVA_HOME/lib/security/cacertsJDK 8JDK 9 之后是$JAVA_HOME/conf/security/cacerts。导入命令keytool -importcert \ -alias my-internal-ca \ -file /path/to/root-ca.pem \ -keystore $JAVA_HOME/lib/security/cacerts \ -storepass changeit \ -noprompt导入后对使用该 JVM 发起的新 HTTPS 连接立即生效不需要重启操作系统但有些服务会缓存 SSLContext需要重启进程。如果不想动全局 JDK也可以在应用启动时指定java -Djavax.net.ssl.trustStore/path/to/truststore.jks \ -Djavax.net.ssl.trustStorePasswordchangeit \ -jar yourapp.jar还可以把私有 CA 打包进自己的truststore.jks然后交给代码里的SSLContext。对绝大多数联调场景直接改cacerts是最省事的。下面这张表基本覆盖了常见的信任库位置和配置入口可以直接收藏工具/环境默认信任库推荐的配置入口Git系统库或自带 ca-bundlehttp.sslCAInfo/schannelUseSSLCAInfocurl系统 CA 文件--cacert/CURL_CA_BUNDLEPython requestscertifi 内置 cacert.pemverify/REQUESTS_CA_BUNDLE/SSL_CERT_FILENode.js内置根证书NODE_EXTRA_CA_CERTSnpm内置 CA 列表npm config set cafileJava/JVM$JAVA_HOME/lib/security/cacertskeytool -importcert/trustStore启动参数5. 自签证书、企业私有 CA 与抓包工具的特殊处置5.1 先核实根证书指纹再放入信任库如果是企业内部统一 CA建议找网络管理员从企业 CA 门户下载根证书不要自己在浏览器里“导出”。如果是某台服务器自签证书也要从管理员或服务器本机导出。拿到证书文件后先核对指纹openssl x509 -in root-ca.pem -noout -subject -fingerprint -sha256输出里会有一段形如SHA256 Fingerprint...的字符串。把这个指纹和管理员给你的值比对一致了再信任。这个步骤虽然多花十秒钟但能防止证书在下载链路上被替换属于值得坚持的安全习惯。5.2 把证书导入操作系统信任库而不是每个软件手工配Linux 上把root-ca.pem复制到/usr/local/share/ca-certificates/目录扩展名必须是.crt然后执行sudo cp root-ca.pem /usr/local/share/ca-certificates/root-ca.crt sudo update-ca-certificatesWindows 上用certlm.msc打开本地计算机证书进入“受信任的根证书颁发机构”导入macOS 需要双击证书文件在钥匙串访问中把它加入“系统”并设置为“始终信任”。操作系统层面导入后浏览器和很多原生工具都能识别。但请记住第 1 章说的Git、Python 虚拟环境、JDK、npm 等仍可能有自己的证书库所以还是需要按第 4 章的方案配置。两者不是替代关系而是互补关系。5.3 抓包代理为什么也会触发这个错Charles、Fiddler 这类工具的本质是 TLS 代理它们本地生成一个自己的根证书并为每次 HTTPS 连接临时签发一张域名证书。你的程序连上代理后拿到的其实是代理签发的证书不是服务器原本的证书。程序只要不信任代理根证书就会报无法获取本地签发者证书。解决办法不是关验证而是把代理工具的根证书安装到目标信任库。Charles 的菜单里有 “Install Charles Root Certificate”可以导出.pem文件Fiddler 也有类似入口。导出后按第 4 章的方案配置到对应工具或 JDK 里。有一个经常踩的坑代理工具重装或升级后根证书可能重新生成之前导入的信任关系全部失效需要重新导入。所以别在出问题时才想起“我上个月明明配好了”要习惯性检查证书生成时间。5.4 不要让临时调试习惯变成生产隐患办公网络如果强制走审计代理全局跳过验证相当于把公司的安全审计也跳过了。更合理的做法是第一时间找网络部门确认代理根证书的分发方式把它纳入系统信任库。这既解决了报错也维持了安全边界。我在实际项目里见过有人把-k写进定时任务脚本半年后安全扫描才发现代价远大于当初配置证书的十分钟。6. 禁用证书验证是下策应急开关与根治思路的区别6.1 关闭验证关闭的到底是什么不管你是执行git config --global http.sslVerify false还是给 curl 加-k亦或是给 requests 传verifyFalse关闭的都是“证书链信任验证”。HTTPS 加密本身还在但客户端不再核实对面服务器是不是它声称的那台。于是中间人攻击变得异常简单攻击者只要在局域网内部署一台伪造证书的服务器客户端就会毫无戒备地把账号、Token 发过去。有些事情只有测试环境能忍生产环境绝对不能。即使只在联调时“临时用一次”也很容易被人复制进生产脚本变成长期隐患。这是所有证书类报错里最需要警惕的处理方式。6.2 真正需要应急时我怎么做我的习惯是在隔离的调试虚拟机里且仅针对某个一次性的请求可以允许自己用一次curl -k但同一时间记下目标服务证书的 fingerprint 用于核对。Git 则用单次开关而不是改全局配置git -c http.sslVerifyfalse clone https://git.xxx.internal/team/project.gitPython 里临时调试可以用verifyFalse但代码里要留 TODO并在调试结束后立刻删掉。核心原则是让“跳过验证”这个状态是显式的、一次性的、可见的而不是悄悄埋进全局配置里。6.3 长期来看让私有 CA 成为可信根真正该做的事是让私有 CA 成为组织内公认的信任源。具体几条经验公司内部系统统一由一套内部 CA 签发避免每台服务器都是自签证书、每台开发机各自导入不同的东西通过自动化配置管理工具或组策略把内部根证书分发到所有开发机和服务器服务端必须把证书链配置完整叶子证书、中间证书按顺序合并在一个文件里定期更新工具和系统证书库比如 Linux 上的ca-certificates包、Java 的cacerts避免因为旧 CA 列表缺少新根证书而报错。我自己现在的习惯是遇到SSL certificate problem: unable to get local issuer certificate第一反应永远是先跑一次openssl看链再检查信任库最后才考虑临时开关。排查五分钟后把证书配好比无脑跳过验证要可靠得多。毕竟这一行报错真正要告诉你的是“信任断了”而不是“验证慢了”。