keytool Java keystore 与 truststore 工具手册
浏览器正常而 Java 报 PKIX,先找真实运行时
同一个 HTTPS 地址在浏览器里能打开,Java 却报 PKIX path building failed,通常不是服务突然换了证书,而是两类客户端读取了不同信任库。更麻烦的是,一台开发机上可能同时存在系统 JDK、IDE Runtime、Gradle daemon JDK、Maven Toolchain、容器 JDK 和 CI JDK。向错误的 cacerts 导入再多次,目标进程也不会改变。
keytool 随 JDK 提供,不需要单独下载。第一步不是编辑 store,而是打印目标进程和当前命令的身份:
java -XshowSettings:properties -version 2>&1 | grep 'java.home'
command -v java
command -v keytool
"$JAVA_HOME/bin/java" -version
"$JAVA_HOME/bin/keytool" -helpWindows 可以用 where.exe java 与 where.exe keytool,再从应用启动日志、IDE 设置或容器进程确认 java.home。只看 JAVA_HOME 仍不够,因为已经运行的 daemon 可能在环境变量修改前启动。
store、alias 和条目类型是三件事
keystore 是保存密钥和证书条目的容器,truststore 是按用途对 store 的称呼:运行时把其中的可信证书当作信任锚。cacerts 是 JDK 自带的系统级 CA store,典型路径为 $JAVA_HOME/lib/security/cacerts。keytool -cacerts 等价于选择当前 JDK 的 cacerts,不能再同时指定另一条 -keystore 路径。
alias 是条目的稳定名称,不是证书 subject,也不是文件名。删除、替换、审计和回滚都应该围绕 alias 与指纹进行。先查看当前 JDK 的条目,再查看一个项目专用 store:
"$JAVA_HOME/bin/keytool" -list -cacerts
"$JAVA_HOME/bin/keytool" -list -v \
-keystore .local/dev-truststore.p12 \
-storetype PKCS12当前 JDK 的默认 keystore type 是 PKCS12;历史项目仍可能显式使用 JKS。文件扩展名不可靠,.jks 可能实际是 PKCS12,.p12 也可能因为错误转换而缺少私钥。让 keytool -list -v 输出 store type 和条目类型,再决定迁移方式。
trustedCertEntry 只有证书,常用于 truststore。PrivateKeyEntry 同时持有私钥和证书链,用于服务端身份或 mTLS 客户端身份。把叶子证书导成 trusted entry 不会让 Java 服务获得对应私钥;把 CA 私钥塞进服务 keystore 更是完全错误的权限扩大。
导入 CA 前先从独立渠道核对指纹
为了让实验可回滚,优先创建项目专用 truststore,不要直接修改全局 cacerts。先打印待导入证书的 SHA-256 指纹,与 CA 管理入口或另一条受控渠道核对:
mkdir -p .local
"$JAVA_HOME/bin/keytool" -printcert -file certs/dev-root.pem
"$JAVA_HOME/bin/keytool" -importcert \
-keystore .local/dev-truststore.p12 \
-storetype PKCS12 \
-alias blog-stack-dev-root \
-file certs/dev-root.pem
"$JAVA_HOME/bin/keytool" -list -v \
-keystore .local/dev-truststore.p12 \
-storetype PKCS12 \
-alias blog-stack-dev-root不要为未知根证书加 -noprompt。Oracle keytool 手册要求在导入 CA 前检查指纹;导入不受信的根意味着 Java 将信任它签发的所有服务身份。自动化场景可以取消交互,但前提应是流水线已经从受控清单验证证书摘要,而不是为了让命令“跑过去”。
若必须修改当前 JDK 的全局库,使用同一个 JAVA_HOME 下的 keytool,并在变更前导出 alias、指纹和 store 摘要。全局 cacerts 影响这个 JDK 启动的所有应用,升级或替换 JDK 还可能丢失手工变更;它更适合由镜像、配置管理或发行商基线重建,而不是靠每台机器的命令历史维护。
把 truststore 接给真实 JVM
项目专用 store 只有被目标 JVM 读取才生效。常见入口是启动参数:
java \
-Djavax.net.ssl.trustStore=.local/dev-truststore.p12 \
-Djavax.net.ssl.trustStoreType=PKCS12 \
-Djavax.net.ssl.trustStorePassword="$TRUSTSTORE_PASSWORD" \
-jar app.jar口令不应直接写进仓库脚本、进程编排文件或 CI 日志。即使 truststore 只含公开 CA,口令仍保护 store 完整性;服务 keystore 的口令还保护私钥。生产环境应从权限受控的 secret source 生成启动配置,并限制 store 文件的读取权限。
正向实验是让应用访问只被项目根信任的测试服务。反向实验不是删除服务器证书,而是让同一应用切回默认 truststore;预期应恢复 PKIX 错误。如果仍成功,说明 CA 已进入全局 cacerts、系统 CA 被当前发行版采用,或者应用代码显式提供了另一套 TrustManager。
需要定位加载路径时,可以短时启用 JSSE 调试:
java \
-Djavax.net.debug=ssl,handshake,trustmanager \
-Djavax.net.ssl.trustStore=.local/dev-truststore.p12 \
-jar app.jar日志会暴露加载的 store、候选 issuer、握手主机和拒绝位置,也可能包含内部域名、证书主体和会话信息。只对受控请求短时开启,完成后删除或脱敏,不要长期把完整握手日志送入集中日志平台。
私钥身份通常先以 PKCS12 进入 Java
keytool 能生成密钥对、CSR 和自签证书,也能导入 CA 返回的证书链。项目已有 PEM 私钥和证书时,通常先由 OpenSSL 打包成 PKCS12,再用 keytool 检查或迁移:
openssl pkcs12 -export \
-inkey certs/server-key.pem \
-in certs/server.pem \
-certfile certs/intermediate-ca.pem \
-name server-tls \
-out .local/server.p12
"$JAVA_HOME/bin/keytool" -list -v \
-storetype PKCS12 \
-keystore .local/server.p12 \
-alias server-tls目标条目必须显示为 PrivateKeyEntry,证书链长度应符合服务端身份预期。trustedCertEntry 只证明证书存在,不证明私钥被带入。应用启动后还要从外部执行 TLS 握手,确认服务实际发送的叶子、完整中间链和 SAN;store 文件可读并不等于监听器采用了它。
旧应用如果只接受 JKS,可以显式转换,并保持源文件只读:
"$JAVA_HOME/bin/keytool" -importkeystore \
-srckeystore .local/server.p12 \
-srcstoretype PKCS12 \
-srcalias server-tls \
-destkeystore .local/server.jks \
-deststoretype JKS \
-destalias server-tls
"$JAVA_HOME/bin/keytool" -list -v \
-keystore .local/server.jks \
-storetype JKS \
-alias server-tls转换前后要核对 alias、条目类型、链长度和指纹。失败回滚应恢复旧文件路径和启动参数,不要把转换结果反向覆盖唯一的原始 store。旧 JKS 只是兼容约束,不是新项目继续选择它的理由。
多 JDK 环境要把证书状态当成可部署配置
IDE 能启动应用而 CI 失败,往往说明证书被手工导进了 IDE Runtime;本机 Maven 成功而 Gradle 失败,可能是 daemon 尚未重启或 toolchain 选择了另一 JDK;宿主机成功而容器失败,则是镜像里的 $JAVA_HOME/lib/security/cacerts 完全独立。
团队应让构建和运行入口同时输出 Java 供应商、版本、java.home、truststore 路径与批准的 CA 指纹。敏感口令无需输出,证书私钥也不能进入诊断包。容器镜像中的信任基线应由 Dockerfile 或镜像构建流程可重复生成;运行时临时导入只适合短时实验,因为下一次重建会丢失状态。
mkcert 可以在设置了 JAVA_HOME 时尝试安装本地根,但它无法替团队证明每个进程选择了哪一个 JDK。keytool 的价值正是在真实运行时边界内对 store、alias 和条目做可复核操作。
删除 alias 后必须看到失败恢复
项目专用 truststore 的退出动作很直接:先让所有服务切回批准的 store,再删除对应 alias,最后在确认文件没有其他共享条目后删除整个文件。
"$JAVA_HOME/bin/keytool" -delete \
-keystore .local/dev-truststore.p12 \
-storetype PKCS12 \
-alias blog-stack-dev-root
"$JAVA_HOME/bin/keytool" -list \
-keystore .local/dev-truststore.p12 \
-storetype PKCS12 \
-alias blog-stack-dev-root第二条应以 alias 不存在失败。全局 cacerts 则使用同一个 JDK 执行 keytool -delete -cacerts -alias ...,并重启 IDE、daemon、容器和长驻服务。只删除 PEM 文件或修改 JAVA_HOME 不会自动撤销已经运行进程中的 TrustManager。
最终反证是新 JVM 对原测试服务恢复明确的信任失败,同时其他批准的公网和内部 CA 仍可用。若删除一个 alias 后大量无关服务一起失败,说明此前把共享根、叶子证书和应用身份混在了同一条目策略里;恢复原 store 后,应重新按 CA 归属和运行时半径设计,而不是继续向 cacerts 追加临时证书。
