内网 Jenkins HTTPS 部署:自签证书、节点连接与中文乱码

内网部署 Jenkins 时一旦启用 HTTPS 并用自签证书,Windows 节点基本连不上。原因是 JVM 不读 Windows 系统证书库,自签证书必须手动导入 Java 自己的 trustStore。加上中文 Windows 下 PowerShell 和 JVM 默认都是 GBK 编码,构建日志里的中文很容易变成乱码。下面是我在生产环境用过的一套流程,包含背后的机制和踩过的坑。

为什么会出问题

三个问题都源于同一件事:Jenkins 节点是一个跑在 JVM 里的 Java 程序,而 JVM 的证书和编码体系跟操作系统是两套。

证书体系是独立的。 Windows 把自签证书放进「受信任的根证书颁发机构」后,浏览器和 .NET 程序就认了。但 Java 不读 Windows 证书库,它有自己的 trustStore,默认在 $JAVA_HOME/lib/security/cacerts(JDK 8 是 jre/lib/security/cacerts)。节点连 HTTPS 的 Jenkins 时,JVM 用这个 trustStore 验证证书链,自签证书不在里面就直接报 unable to find valid certification path

trustStore 和 keystore 不是一回事。 keystore 存自己的私钥和证书,trustStore 存你信任的对方证书。导入自签证书用的是 trustStore 的语义,命令里写的 -keystore 只是指定文件路径,别被名字误导。

证书库格式有版本差异。 JDK 9 之前默认 JKS(Java 专有格式),JDK 9 起(JEP 226)默认改为 PKCS12(行业标准)。导入和启动时 -storetype / trustStoreType 必须一致,否则会读到空库,表现为证书导入了但节点还是连不上。

编码冲突。 中文 Windows 控制台默认 codepage 936(GBK),JVM 在中文 locale 下默认 file.encoding 也是 GBK。Jenkins 流水线日志按 UTF-8 传输,节点按 GBK 解码就乱码。

准备工作

  • Jenkins 服务已部署,反向代理已配置 HTTPS
  • 自签证书文件(.crt.cer 格式)已生成
  • Windows 节点已装 JDK(建议 11 或 17)
  • 已从 Jenkins 管理页面拿到节点密钥和 agent.jar

第一步:导入自签证书到 JVM trustStore

用 keytool 把证书导入一个 trustStore 文件。用户级证书库不需要管理员权限,推荐先用这个:

1
2
3
4
5
6
keytool.exe -importcert -v -noprompt `
-alias "prod_jenkins" `
-file "c:\temp\_.test.cn.crt" `
-storepass changeit `
-storetype PKCS12 `
-keystore "c:\users\test\.keystore"

参数含义:

  • -importcert:导入受信任证书到 trustStore
  • -noprompt:跳过「是否信任」的交互确认,脚本里必加
  • -alias:证书在库里的别名,后续删除或查看靠它
  • -file:自签证书文件路径
  • -storepass:证书库密码,changeit 是 cacerts 的默认密码,自定义库可以自己设
  • -storetype:库格式,JDK 9+ 用 PKCS12,JDK 8 用 JKS
  • -keystore:库文件路径

库文件放哪里有两种选择。

用户级(c:\users\<username>\.keystore):只影响当前用户跑的 Java 程序,不需要管理员权限,改动隔离。我倾向这个,单节点排查问题时不会污染其他服务。

全局(%JAVA_HOME%\lib\security\cacerts,JDK 8 是 jre\lib\security\cacerts):影响这台机器上所有 Java 程序,需要管理员权限。好处是启动参数里不用再指定 trustStore 路径,坏处是 JDK 升级或重装时这个库会被覆盖,证书会丢。生产环境如果要靠它,记得把证书导入写进部署脚本。

第二步:启动 agent 时指定 trustStore

导入之后,启动节点时用 JVM 参数告诉它去哪里找证书:

1
2
3
4
5
6
7
8
9
10
java `
"-Djavax.net.ssl.trustStore=c:\users\test\.keystore" `
"-Djavax.net.ssl.trustStorePassword=changeit" `
"-Djavax.net.ssl.trustStoreType=PKCS12" `
-jar agent.jar `
-url https://jenkins.test.cn `
-workDir "D:\jenkins_agent" `
-secret "xxxxxx" `
-name "Windows-构建节点01" `
-webSocket

按实际环境改这些:trustStore 路径、Jenkins 地址、工作目录、节点密钥、节点名称。

几个关键点:

  • 三个 trustStore* 参数必须和导入时的 -keystore / -storepass / -storetype 完全对应,有一个对不上就读不到证书。
  • -url 用 HTTPS 地址。
  • -secret 从 Jenkins 节点配置页拿。
  • -webSocket 这一项在内网反代场景很重要。JNLP 节点默认走固定 TCP 端口(通常 50000),反向代理一般只暴露 443,那个端口过不来。-webSocket 让节点连接复用 HTTPS 通道走 WebSocket,反代只要正常转发 443 就行,不用再单独开端口。

第三步:处理中文乱码

乱码的根因是编码不一致,治本的办法是让 JVM 全程用 UTF-8。

设环境变量。 在「系统变量」里加一条:

1
2
变量名:JAVA_TOOL_OPTIONS
变量值:-Dfile.encoding=UTF-8

JAVA_TOOL_OPTIONS 会被所有 JVM 进程自动读取,等于给每个 Java 程序都加了启动参数。设完之后重启 PowerShell 和节点代理,环境变量不会热生效。

如果还乱码,多半是 PowerShell 控制台本身的 codepage 还是 936。手动切到 UTF-8:

1
chcp 65001

注意 chcp 只对当前会话有效,新开的窗口还会回到 936。要永久生效,可以把这行写进 PowerShell 配置文件,或者干脆让节点跑在一个固定的服务会话里。

也可以在启动命令里直接带参数,双保险:

1
java -Dfile.encoding=UTF-8 -Djavax.net.ssl.trustStore=c:\users\test\.keystore -jar agent.jar ...

验证

  1. 启动节点,看控制台有没有 SSL 报错(SSLHandshakeExceptionunable to find valid certification path 等)。
  2. Jenkins 管理页面里节点状态变成「在线」。
  3. 跑一个会输出中文的测试任务,看日志里中文是否正常。
  4. 检查工作目录有没有正常创建和写文件。

替代方案与取舍

上面这套是「自签证书 + 手动导入 trustStore」的路子,维护成本主要在证书续期和节点迁移时要重新导入。还有几种选择。

反代终止 TLS,内部走 HTTP。 反向代理上配真实证书(比如内网 CA 签的),代理到 Jenkins 用 HTTP。节点直接连 HTTP 的 Jenkins,完全没有证书信任问题。代价是 Jenkins 到代理这一段是明文,只适合代理和 Jenkins 在同一可信网络(同一台机器或同一内网)的情况。

用内网 CA 签发证书。 如果公司有内部 PKI,让 CA 给 Jenkins 签一张证书,再把 CA 根证书导入 trustStore。比自签规范,证书管理也更可控,前提是内部 CA 已经搭好。

禁用 TLS 验证。 网上能搜到设置 -Dcom.sun.net.ssl.checkRevocation=false 之类绕过验证的做法。不建议在生产环境用,等于把 HTTPS 退化成明文,中间人风险全开。

踩过的坑

  • 导入和启动的 storeType 不一致。 用 JDK 8 的老机器导入时漏写 -storetype JKS,默认是 JKS 还能用;换到 JDK 17 的机器,默认变成 PKCS12,启动参数里还写着 JKS,结果读不到证书。两边的类型一定要对齐。
  • 环境变量改了没重启。 JAVA_TOOL_OPTIONS 是进程启动时读的,改完不重启 PowerShell 和节点,看起来「配置对了还是乱码」,其实是旧进程还在跑。
  • 全局 cacerts 被 JDK 升级覆盖。 早期把证书导进了全局 cacerts,后来 JDK 小版本升级,库文件被重置,节点集体掉线。后来全部改成用户级库加启动参数显式指定路径。
  • 反代没透传协议头。 反向代理要把 X-Forwarded-Proto: https 透给 Jenkins,否则 Jenkins 生成的回调 URL 可能是 HTTP,节点跟着 HTTP 去连就会失败。在 Jenkins 全局安全配置里把「Jenkins URL」也显式写成 HTTPS 地址,别让它自动猜。