编程语言

PKIX path building failed(Java)原因与修复

作者: · 最后核实

报错信息javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

PKIX path building failed ... unable to find valid certification path to requested target 的意思是:JVM 没法把服务器证书连到它所加载的信任库里的任何一个根证书。 最常见的情况是服务器没发中间证书、公司代理重新签发了 HTTPS 流量,或者内网 CA 从来没导入过 JDK 的 cacerts。

快速修复

服务器缺中间证书就到服务器上补链,JVM 不信任根证书才用 keytool 导入。先判断是哪一种,不要一上来就导证书:

openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null
  • 输出里只有一张证书,结尾是 Verify return code: 21 (unable to verify the first certificate):服务器证书链不完整,到服务器上配置完整证书链,见缺少中间证书。
  • 证书链完整,但最上面的 issuer 是公司名、代理设备厂商或内网 CA:JVM 不信任这张根证书,导入到报错的那个 JVM 的信任库:
# JDK 9 及以后
keytool -importcert -cacerts -alias company-root -file company-root.pem -storepass changeit -noprompt
# JDK 8
keytool -importcert -keystore "$JAVA_HOME/jre/lib/security/cacerts" -alias company-root -file company-root.pem -storepass changeit -noprompt

导入后要重启应用,信任库只在启动时读一次。

原因

这个报错有 5 种原因,最常见的是服务器没发中间证书。

1. 服务器没发中间证书

Java 不会自己去下载缺失的中间证书,所以 Chrome 能打开的网站,Java 照样可能报错。

  • 确认: openssl s_client -showcerts 只显示站点证书,或者证书链补全工具提示缺少中间证书。
  • 修复: 让服务器发送站点证书加中间证书(Nginx 用 fullchain.pem,Tomcat、Apache、负载均衡上传完整链)。阿里云、腾讯云 SLB/CLB 上传证书时,“证书内容”要粘贴完整链。

2. 公司代理或安全网关解密了 HTTPS

不少企业的上网行为管理、安全网关会解密 HTTPS,再用自己的私有根证书重新签发。浏览器信任它,是因为 IT 已经把根证书推送到了系统证书库;但 Java 不读系统证书库。

  • 确认: 加 -Djavax.net.debug=ssl:handshake 运行,或在同一台机器上用 openssl s_client 看,issuer 是代理产品名或公司名。
  • 修复: 把代理根证书导入 cacerts;Windows 上也可以用 -Djavax.net.ssl.trustStoreType=Windows-ROOT。

3. 内网私有 CA 或自签名证书

内部接口、Nexus 私服、自建 GitLab 经常用公司自建 CA 签发的证书。

  • 修复: 每个 JVM 导入一次内网根证书(不是站点证书);更好的做法是做一个专用信任库,让应用显式加载:
keytool -importcert -keystore app-truststore.p12 -storetype PKCS12 -storepass changeit -alias company-root -file company-root.pem -noprompt
java -Djavax.net.ssl.trustStore=app-truststore.p12 -Djavax.net.ssl.trustStorePassword=changeit -jar app.jar

注意 javax.net.ssl.trustStore 是替换 cacerts,自定义信任库里也要包含应用需要访问的公网根证书,否则调用支付宝、微信支付等公网接口时又会报同样的错。

4. JDK 太旧,cacerts 没更新

很老的 JDK 版本里没有新的根证书,CA 切换到新根以后签发的证书就验证不过。

  • 修复: 升级到同一大版本的最新更新版,每次更新都会带上新的 cacerts。

5. 应用加载的不是你以为的那个信任库

Tomcat Connector、Spring 配置、Kafka 或 Elasticsearch 客户端都可能单独指定信任库。

  • 确认: 加 -Djavax.net.debug=ssl:trustmanager,日志会打印实际加载的信任库和其中的证书。

各环境修法

各环境的做法都是把根证书加进实际报错的那个 JVM 读取的信任库,位置因发行版和部署方式而异。

Debian / Ubuntu 的 OpenJDK: cacerts 由 ca-certificates-java 从系统证书库生成,往系统里加根证书就能同时更新 Java:

sudo cp company-root.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates

CentOS / RHEL / Rocky / 阿里云 Alibaba Cloud Linux: 把根证书放到 /etc/pki/ca-trust/source/anchors/,执行 sudo update-ca-trust;系统 OpenJDK 读的是 /etc/pki/ca-trust/extracted/java/cacerts。

Docker 镜像: 在构建时就把根证书打进镜像,每个容器启动即可用:

COPY company-root.pem /tmp/company-root.pem
RUN keytool -importcert -cacerts -alias company-root -file /tmp/company-root.pem -storepass changeit -noprompt

Windows: 用上面的 Windows-ROOT;或者以管理员身份运行 keytool,因为 Program Files 下的 cacerts 默认不可写。

构建工具: Maven、Gradle 下载依赖时也会报同样的错,见 Maven PKIX path building failed。

不要这样做

下面这些做法会关掉身份校验,或者让导入的证书很快失效。

  • 写一个什么都信任的 X509TrustManager(checkServerTrusted 为空),或者 HostnameVerifier 直接返回 true。这些从博客复制来的代码会关掉身份校验,而且往往一路带进生产环境。
  • 导入站点证书代替根证书。 只是把问题藏起来,下次续期又会报错。
  • 删掉 cacerts 或用别的机器、旧 JDK 的文件覆盖。 会丢失新的根证书,而且下次升级 JDK 时你的修改又会被覆盖。

常见问题

Java 的 cacerts 文件在哪里?

JDK 9 及以后是 $JAVA_HOME/lib/security/cacerts,JDK 8 是 $JAVA_HOME/jre/lib/security/cacerts。Debian/Ubuntu 的系统 OpenJDK 链接到 /etc/ssl/certs/java/cacerts,CentOS/RHEL 系链接到 /etc/pki/ca-trust/extracted/java/cacerts。默认密码是 changeit。

证书已经导入了,为什么还报 PKIX path building failed?

多半是报错的程序跑在另一个 JVM 上:IDEA 自带的 JDK、Docker 镜像里的 JDK、PATH 上的另一个 Java。在程序里打印 System.getProperty("java.home"),或看 mvn -v、gradle -v 的输出,再往那个 JVM 的 cacerts 里导入。另外检查启动参数里有没有 -Djavax.net.ssl.trustStore,它会整个替换掉 cacerts。

应该导入站点证书还是根证书?

导入根证书(或公司代理的根证书)。站点证书每次续期都会变,按现在的有效期几个月就要换一次,导入站点证书很快又会报错。如果只是服务器缺中间证书,应该修服务器,而不是在客户端导入任何东西。

Java 能直接用 Windows 证书库吗?

可以。在 Windows 上启动 JVM 时加 -Djavax.net.ssl.trustStoreType=Windows-ROOT,就会信任 Windows 证书库里的根证书。公司 IT 已经通过域策略下发了代理根证书时,这个办法最省事。

资料来源

  1. keytool (Java SE 21 tool reference)
  2. Java Secure Socket Extension (JSSE) Reference Guide
  3. openssl-s_client (OpenSSL 3.0)