LabHub
Get started
배우기 러닝패스 코스

Air-Gapped Mirrors and a Private CA

One mirror line and the JVM trust store

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

Maven 의 폐쇄망 대응은 settings.xmlmirror 한 줄로 모든 원격 요청을 사내 저장소로 돌리는 것이다. 사내 저장소는 파일을 정해진 경로에 늘어놓은 웹 서버면 되지만, 반입할 목록에는 라이브러리뿐 아니라 빌드에 쓰는 플러그인까지 들어가야 하고, HTTPS 라면 JVM 이 그 사설 CA 를 믿어야 한다.

왜 이게 필요했나

폐쇄망의 자바 빌드는 대개 두 번 실패한다. 처음에는 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=...)로 한 번 빌드하고 그 디렉터리를 웹 서버에 올리면 사내 저장소가 된다. 새 로컬 저장소를 쓰는 이유는 예전에 받아 둔 것이 섞여 목록을 부풀리거나, 반대로 이미 있어서 받지 않은 것이 목록에서 빠지는 일을 막기 위해서다.

_remote.repositories. 로컬 저장소에는 파일마다 어느 저장소 id 에서 왔는지 적은 _remote.repositories 가 생긴다(Maven Resolver 의 추적 파일). Resolver 문서는 R1 에서 받은 파일이라도 지금 빌드가 R1 을 정의하지 않으면 없는 것으로 치고 다시 받는다고 적는다. 사내 저장소로 옮길 때 이 파일을 걷어 내는 이유이고, 반대로 폐쇄망 쪽 로컬 저장소의 이 파일을 보면 실제로 사내 미러에서 받았는지 확인할 수 있다.

JVM 의 신뢰 저장소. JSSE 는 javax.net.ssl.trustStore 속성, jssecacerts, cacerts 순으로 신뢰 저장소를 찾는다. keytool 은 JDK 9 부터 -cacerts 옵션으로 그 저장소를 바로 가리키고, 문서가 적은 cacerts 의 초기 비밀번호는 changeit 이다. 우분투는 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 하면 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= 이 찍혔다. 운영체제 저장소에 넣는 쪽을 시험하자 update-ca-certificatesdebian: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