Air-Gapped Mirrors and a Private CA
An HTTPS internal Maven mirror the JVM trusts
Goal
Collect dependencies and plugins on the connected side, stand up an internal Maven repository over HTTPS, and redirect every request with the mirror in settings.xml. Make the JVM trust the private CA, build with a new local repository on the air-gapped side, and finish the procedure for additionally importing a plugin that was missing from the import list.
Why it matters
Air-gapped failures of Java builds come in three layers — the repository address, the certificate, and a hole in the import list. The address is handled by a single mirror entry, the certificate by the JVM trust store, and the hole by the habit of "making the list with the goals you will actually run". This Pod can reach the internet (80/443), so it plays the download side, and on the air-gapped side mirrorOf * redirects every request to the internal repository so that the outside cannot be used. The grader checks where files were downloaded from by the _remote.repositories of the local repository and the access log of the internal server.
Steps
- In
/root/mvn/app, createpom.xml(gson 2.11.0, plugin versions pinned) andsrc/main/java/demo/App.java, and runpackagewith a new local repository/root/mvn/outside-repo. - Move
/root/mvn/outside-repoto/srv/maven, but strip_remote.repositories,*.lastUpdated, andresolver-status.properties. - In
/root/pki, create a root with CNAirgap Internal Root CA(ca.crtandca.key) and amaven.airgap.internalserver certificate (maven.crtandmaven.key, with a SAN), and add the name to/etc/hosts. - Start nginx with
/root/mvn/nginx.confso thathttps://maven.airgap.internal:8443/serves/srv/maven. The access log is/root/mvn/access.log. - In
~/.m2/settings.xml, write a mirror with idairgap-internaland mirrorOf*. Also create an empty configuration/root/mvn/outside-settings.xmlto use when playing the connected side. - Run
packagewith the air-gapped side's new local repository/root/mvn/inside-repoand save the failing output in/root/mvn/pkix.log. - Make the JVM trust the private root (do not use options that disable verification).
- With the same command, get the air-gapped side's build to pass.
- Write the coordinates of the artifact that makes
mvn clean packagefail in/root/mvn/missing.txt(groupId:artifactId:version), download it on the connected side and add it to the internal repository, and then getclean packageto pass on the air-gapped side.
Notes
- Specifying the local repository:
-Dmaven.repo.local=<경로>(the placeholder is the path) · a different settings file:-s <파일>(the placeholder is the file) - Where it was downloaded from:
<로컬 저장소>/com/google/code/gson/gson/2.11.0/_remote.repositories(the placeholder is the local repository) - Checking and starting the nginx configuration:
nginx -t -c /root/mvn/nginx.conf→nginx -c /root/mvn/nginx.conf - Viewing the JVM store:
keytool -list -cacerts -storepass changeit | head - Common mistake 1: just typing the connected-side commands after step 5. The mirror in the user settings.xml sends even outside requests to the internal repository — for the outside role, use
-s /root/mvn/outside-settings.xml. - Common mistake 2: rebuilding right after the additional import. The 404 is recorded in the local repository, so
-Uis needed. - Common mistake 3: doing the first build with this image's default local repository (
/root/.m2/repository). It is already filled, so the import list gets distorted.
Download side: build with a new local repository to collect
Create pom.xml and App.java in /root/mvn/app and run package with a new local repository /root/mvn/outside-repo.
You can change the location of the local repository with a system property. If you give a new directory, only what this build actually downloaded piles up. If you do not pin the plugin versions in the POM, this Maven's default compiler does not know the JDK 21 settings.
Strip the tracking files and make the internal repository
Move /root/mvn/outside-repo to /srv/maven, but strip the tracking files (_remote.repositories, *.lastUpdated, and resolver-status.properties).
The internal repository only needs to be the file layout as it is. The tracking files are the record in which the downloading side's local repository writes "where it came from", so they must not remain in the repository. You can pick names with find and delete them.
Issue the mirror certificate with a private CA
In /root/pki, create a root (ca.crt and ca.key, CN Airgap Internal Root CA) and a maven.airgap.internal certificate (maven.crt and maven.key, with a SAN), and add the name to /etc/hosts.
The root is self-signed with CA:TRUE, and the server certificate is signed with the root while giving the SAN through an extension file. It is the same procedure as in the private CA module.
Serve the HTTPS internal repository with nginx
Start nginx with /root/mvn/nginx.conf so that https://maven.airgap.internal:8443/ serves /srv/maven, and keep the access log in /root/mvn/access.log.
A Maven repository is static files, so a single root is enough. In this Pod you cannot open ports below 1024, and the pid and log paths must be moved to places you can write to. Check first with -t.
Redirect every request with mirrorOf *
In ~/.m2/settings.xml, write a mirror with id airgap-internal, mirrorOf *, and url https://maven.airgap.internal:8443/, and create an empty configuration /root/mvn/outside-settings.xml.
The user settings file is under .m2 in the home directory. If you give mirrorOf an asterisk, it intercepts even the repositories declared by the POM. The configuration for the outside role only needs a minimal settings element with no mirror.
A CA the JVM does not know — record the PKIX error
Run package with the new local repository /root/mvn/inside-repo and save the failing output in /root/mvn/pkix.log.
Telling curl about the root and the JVM trusting the root are separate things. Save the whole failing output, and look in the error lines for the wording about the certificate path.
Put the root in the JVM trust store
Make the JVM trust the private root (do not use options that disable verification).
The JDK has a tool for handling trust stores, with an option that points directly at the default store (cacerts). There is an initial password that the documentation gives. On Ubuntu, there is also a route where updating the operating system store updates the JVM store as well.
The air-gapped side's build passes
Get the build to pass with the same command as in step 6 (/root/mvn/inside-repo, package).
Once the JVM trusts the root, the same command should pass. After it passes, look at which repository id the tracking files in the air-gapped side's local repository recorded.
The additional import that the word clean caused
Write the coordinates that caused the mvn clean package failure in /root/mvn/missing.txt as groupId:artifactId:version, download it on the connected side and add it to the internal repository, and then get clean package to pass on the air-gapped side.
The error line tells you which plugin could not be found. For the outside role, run the same goal with the empty settings file and the outside's local repository, and the plugin is collected. If it still fails after you move it again, read the error sentence to the end — the earlier failure is recorded.