信任库不止一个
한국어 원문으로 표시합니다.
한 줄 요약
폐쇄망의 사내 미러는 거의 다 사설 CA 가 발급한 인증서를 쓴다. 그런데 신뢰 저장소는 하나가 아니다. 운영체제, JVM, 파이썬의 certifi, Node 의 내장 목록이 각자 따로 있다. 한 곳에 넣었다고 다 믿는 것이 아니라서, 어떤 도구가 어느 저장소를 보는지 표로 알고 있어야 한다.
왜 이게 필요했나
사내 미러를 HTTPS 로 세우고 서버에 루트 인증서를 넣었다. curl 은 된다. 그런데 파이썬 배치는 SSLError: CERTIFICATE_VERIFY_FAILED, 프런트엔드 빌드는 UNABLE_TO_VERIFY_LEAF_SIGNATURE, 자바 빌드는 PKIX path building failed 로 죽는다. 흔한 반응은 verify=False, strict-ssl=false, -k 로 검증을 끄는 것이다. 그러면 사내망 안에서 누가 미러 행세를 해도 아무도 모른다. 폐쇄망은 바깥 공격이 적을 뿐이지 안쪽 공격이 없는 곳이 아니다.
어떻게 동작하나
CA 와 서버 인증서. 루트 CA 인증서에는 basicConstraints = CA:TRUE 가 있어야 다른 인증서에 서명할 자격이 된다(OpenSSL x509v3_config 문서). 서버 인증서에는 접속할 이름이 subjectAltName 에 DNS: 로 들어가야 한다. RFC 9525 는 commonName 을 이름 확인에 쓰는 것이 더는 유효하지 않다고 적고, Go 는 1.15 부터 CN 을 이름으로 보지 않는다. CN 에만 이름을 적은 인증서는 요즘 도구에서 이름 불일치로 거절된다.
운영체제 저장소. 데비안·우분투는 /usr/local/share/ca-certificates/ 에 넣고 update-ca-certificates 를 돌린다. 매뉴얼은 확장자가 반드시 .crt 여야 한다고 적고, 결과는 한 파일로 이어 붙인 /etc/ssl/certs/ca-certificates.crt 다. 이 명령은 /etc/ca-certificates/update.d/ 의 훅도 돌리는데, 우분투의 ca-certificates-java 가 여기에 훅을 걸어 JVM 저장소(/etc/ssl/certs/java/cacerts)까지 갱신한다. RHEL 계열은 /etc/pki/ca-trust/source/anchors/ 에 넣고 update-ca-trust extract 를 돌린다.
도구마다 보는 곳.
curl 빌드할 때 정해진 파일 저장소(우분투는 운영체제 묶음). --cacert·CURL_CA_BUNDLE 로 바꾼다
파이썬 ssl OpenSSL 기본 경로 = 운영체제 묶음. SSL_CERT_FILE·SSL_CERT_DIR 로 바꾼다
requests certifi 묶음. REQUESTS_CA_BUNDLE(없으면 CURL_CA_BUNDLE) 로 바꾼다. SSL_CERT_FILE 은 읽지 않는다
httpx certifi 묶음. SSL_CERT_FILE·SSL_CERT_DIR 를 따른다
Node 빌드에 들어간 모질라 목록. NODE_EXTRA_CA_CERTS 로 더한다(프로세스 시작 때 한 번 읽는다)
Go 리눅스에서는 운영체제 묶음. SSL_CERT_FILE·SSL_CERT_DIR 로 바꾼다
JVM cacerts(우분투는 운영체제 훅이 채운다). keytool 이나 javax.net.ssl.trustStore
Node 는 문서가 여러 가지를 덧붙인다. NODE_EXTRA_CA_CERTS 는 setuid 로 뜬 프로세스에서는 무시되고, 파일이 없거나 형식이 틀리면 경고 한 번만 내고 넘어간다. 운영체제 저장소를 쓰게 하는 --use-system-ca 는 v23.8.0 에 생겼고(리눅스는 v23.9.0 부터), 같은 일을 하는 환경 변수 NODE_USE_SYSTEM_CA=1 은 v22.19.0·v24.6.0 에 들어왔다. 이 실습의 node 는 22.11 이라 둘 다 없다 — 버전을 먼저 확인하는 이유다.
체인. 현장의 사설 PKI 는 루트 아래 중간 CA 를 두고 서버 인증서는 중간 CA 가 발급하는 경우가 많다. 클라이언트 저장소에는 루트만 있으므로 서버가 자기 인증서와 중간 CA 인증서를 함께 보내야 경로가 이어진다. 서버에 잎 인증서만 걸면 unable to get local issuer certificate 가 난다. 이것은 클라이언트가 아니라 서버 설정의 잘못이다.
현장에서 만나는 모습
이 실습 파드에서 재 본 결과다. 루트를 운영체제 저장소에 넣기 전에는 curl·파이썬 urllib·requests·httpx·Node·pip 가 모두 인증서 검증에서 실패했다. update-ca-certificates 뒤에는 curl·urllib·Go·pip 가 통과했고, requests 와 httpx 는 CERTIFICATE_VERIFY_FAILED, Node 는 UNABLE_TO_VERIFY_LEAF_SIGNATURE 로 여전히 실패했다. REQUESTS_CA_BUNDLE·SSL_CERT_FILE·NODE_EXTRA_CA_CERTS 를 주자 모두 통과했다. pip 가 통과한 것은 이 이미지의 pip 가 우분투가 고친 판이라 운영체제 묶음을 읽기 때문이다.
그래서 현장에서는 환경 변수를 한 곳(/etc/profile.d/ 의 파일, 서비스라면 유닛의 Environment)에 모아 두고, 새 서버를 받으면 도구별로 한 번씩 접속해 보는 표를 남긴다. "OS 에 넣었으니 됐다" 는 절반만 맞다.
다음 실습에서 할 것
사설 루트 CA 와 SAN 이 든 서버 인증서를 만들어 repo.airgap.internal:8443 을 HTTPS 로 띄운다. 운영체제 저장소에 넣고, 파이썬과 Node 가 믿도록 /etc/profile.d/airgap-ca.sh 에 환경 변수를 모은다. 도구마다 "운영체제 저장소만으로 되는가" 를 표로 적으면 채점기가 같은 조건으로 다시 재서 대조한다. 마지막에 중간 CA 로 발급한 인증서를 체인과 함께 내보낸다.
참고 문서: OpenSSL x509v3_config · RFC 9525 · update-ca-certificates(8) · RHEL: Using shared system certificates · requests: CA Certificates · httpx SSL · Node.js CLI(NODE_EXTRA_CA_CERTS) · Go crypto/x509 · curl SSL CA certificates