TT Lab
开始
学习 学习路径 课程

隔离网络镜像与私有 CA

一行 mirror 与 JVM 信任库

在 TT Lab 中继续学习

一句话总结

Maven 应对隔离网络的办法,是用 settings.xml 中一行 mirror,把所有远程请求都转到内部仓库。内部仓库只需是把文件按规定路径摆放好的 Web 服务器,但要导入的清单中,不仅要有库,还要有构建所用的插件;如果是 HTTPS,JVM 还必须信任那个私有 CA。

为什么需要它

隔离网络中的 Java 构建,通常会失败两次。第一次是 Could not transfer artifact ... from/to central,把仓库地址换成内部地址之后,第二次是 PKIX path building failed。为了越过第二次,很多地方会把 -Dmaven.wagon.http.ssl.insecure=true 这类关闭验证的选项写死在 CI 中。然后还会再失败一次——在开发者一直在执行的 mvn clean package 上。因为导入清单只是按 package 制作的。

工作原理

settings.xml 的两个位置。 根据 Maven 文档,有全局的 ${maven.home}/conf/settings.xml 和用户的 ${user.home}/.m2/settings.xml,两者都有时会合并,但以用户一侧为准。可以用 -s 和 -gs 指定其他文件。

mirror 与 mirrorOf。 <mirror> 包含 id、mirrorOf 和 url。mirrorOf 中可以使用 *(所有仓库)、external:*(除 localhost 和文件仓库之外的全部)、external:http:*(从 3.8.0 起)、repo1,repo2 这样的列表,以及 *,!repo1 这样的排除。多个 mirror 都匹配时,id 完全相同的优先,否则先声明的那个胜出。在隔离网络中,把 mirrorOf 设为 *,不管 POM 中有人写了什么仓库,全部转到内部。从 3.8.1 起,全局配置中带有阻止外部 HTTP 仓库的 maven-default-http-blocker 镜像,所以如果把内部仓库搭成 http,还要与这个拦截缠斗——还是搭成 HTTPS 更好。

仓库就是文件布局。 路径是把 groupId 中的点换成斜杠,再加上 artifactId/version/artifactId-version.jar。所以在联网的一侧,用新的本地仓库(-Dmaven.repo.local=...)构建一次,再把该目录放到 Web 服务器上,就成了内部仓库。使用新的本地仓库,是为了避免以前下载好的内容混进来使清单膨胀,或者反过来,因为已经存在而没有下载的内容从清单中漏掉。

_remote.repositories。 本地仓库中,每个文件都会有一个写明它来自哪个仓库 id 的 _remote.repositories(Maven Resolver 的追踪文件)。Resolver 文档写道,即使是从 R1 下载的文件,如果当前构建没有定义 R1,也会视为不存在并重新下载。这就是迁到内部仓库时要去掉这个文件的原因;反过来,查看隔离网络一侧本地仓库中的这个文件,就能确认是否确实从内部镜像下载。

JVM 的信任库。 JSSE 按 javax.net.ssl.trustStore 属性、jssecacerts、cacerts 的顺序查找信任库。keytool 从 JDK 9 起,可以用 -cacerts 选项直接指向该信任库,文档写明 cacerts 的初始密码是 changeit。在 Ubuntu 上,ca-certificates-java 挂在操作系统的 update-ca-certificates 钩子上,会一并更新 /etc/ssl/certs/java/cacerts。像官方 JDK 压缩包那样使用自己的 lib/security/cacerts 的 JVM,则不受这个钩子的影响。

换成 Nexus 来看,把缓存中央仓库的 proxy 与上传内部制品的 hosted 合并成 group,再把该 group 的地址写进 mirrorOf * 的 url,结构是一样的。

在现场相遇的样子

我们在这个实验镜像(Maven 3.8.7,JDK 21)中实测过。用新的本地仓库对只用一个 gson 的项目执行 package,收集到 51 个 jar、20MB,其中大部分是插件及其依赖。如果不在 POM 中写插件版本,就会使用 3.8.7 的默认绑定(compiler 3.1 等),而那个版本不认识 maven.compiler.release,在 JDK 21 上构建就坏了。固定版本,同时也是固定导入清单。

把内部镜像指向 HTTPS 后,构建在 PKIX path building failed ... unable to find valid certification path to requested target 处停住了。用 keytool 把根证书放入 cacerts 后,同一条命令就通过了,而隔离网络一侧本地仓库的 _remote.repositories 中则记录了 gson-2.11.0.jar>airgap-internal=。试验放入操作系统信任库的做法时,update-ca-certificates 以 debian:airgap-os.pem 这个别名,也把它放进了 JVM 的 cacerts。

最后的陷阱是 mvn clean package。因为导入清单是按 package 制作的,所以缺少 maven-clean-plugin 2.5,以 Could not find artifact 失败。从外部下载该插件放进内部仓库再运行,这次又以 was not found in ... during a previous attempt. This failure was cached in the local repository 失败。因为 404 被记录在本地仓库中,在更新周期过去之前不会再次询问。用 -U 强制重新询问之后,就通过了。导入清单必须按实际要执行的全部 goal 来制作,追加导入之后需要 -U。

下一项实验要做什么

用新的本地仓库构建使用 gson 的项目并收集,去掉追踪文件后制作 /srv/maven。用私有 CA 签发 maven.airgap.internal 证书,用 nginx 启动 HTTPS,并通过 settings.xml 把所有请求转过去。记录 PKIX 错误之后,让 JVM 信任它,使隔离网络一侧的构建通过。最后追加导入 clean 插件,并用 -U 收尾。

参考文档:Settings Reference、Using Mirrors for Repositories、Maven 3.8.1 Release Notes、Repository Layout、Resolver Local Repository、keytool、JSSE Reference Guide、ca-certificates-java