TT Lab
はじめる
学ぶ 学習パス コース

閉域網のミラーとプライベート CA

mirror の一行と JVM の信頼ストア

TT Labで続きを見る

一言でいうと

Mavenのエアギャップ環境への対応は、settings.xmlのmirror 1行で、すべてのリモートリクエストを、社内リポジトリに回すことです。社内リポジトリは、ファイルを決まったパスに並べたWebサーバーで十分ですが、持ち込む一覧には、ライブラリだけでなく、ビルドに使うプラグインまで入っている必要があり、HTTPSなら、JVMがそのプライベートCAを信頼する必要があります。

なぜ必要なのか

エアギャップ環境のJavaビルドは、たいてい2回失敗します。最初はCould not transfer artifact ... from/to centralで、リポジトリのアドレスを社内のものに変えたあとは、PKIX path building failedで。2回目を越えるために、-Dmaven.wagon.http.ssl.insecure=trueのような、検証をオフにするオプションを、CIに埋め込んでいる場所が多いです。そして、もう一度失敗します。開発者がいつも実行していたmvn clean packageで。持ち込み一覧を、packageだけで作ったからです。

どう動くのか

settings.xmlの2か所: 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が、OSの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 1つを使うプロジェクトを、新しいローカルリポジトリでpackageすると、jarが51個、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=が記録されました。OSのストアに入れるほうを試すと、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