브라우저에서는 막힌 사이트가 터미널의 curl 로는 그대로 열리는 일이 있어요. 인증서를 "누가" 검증하느냐에 따라 보는 루트 목록이 달라서 생기는 일이에요. 결론부터 말하면 맥 한 대 안에는 신뢰 저장소가 여러 개 있고, Apple 이 내려보내는 차단 목록은 그중 시스템 검증(SecTrust)에만 걸려요. 인증기관(CA) 사고가 나면 내가 쓰는 도구가 어느 저장소를 보는지부터 확인해야 해요.
2026년 10월에 인도 인증기관 eMudhra 의 TLS 중간 CA 6곳 개인 키가 유출됐을 때 이 차이가 그대로 드러났어요. Apple 지원 문서는 10월 19일부터 해당 인증서 12장을, 23일부터 emSign ECC Root CA - G3 을 차단한다고 밝히면서, macOS 와 Xcode Command Line Tools 의 curl·git·python3·ruby·perl·openssl 은 /etc/ssl/cert.pem 으로 검증해서 이 차단을 보지 않는다고 적었어요. 이 글은 그 사건을 예로 들어, 사고와 상관없이 오래 쓸 수 있는 저장소 구분법과 점검 명령을 정리해요. 숫자는 macOS 26.2(Apple M1)에서 2026-10-27 에 직접 세고 돌려 본 값이에요.
신뢰 저장소란, 왜 중간 CA 사고에 약한가
TLS 서버는 접속할 때 자기 인증서와 중간 CA 인증서를 함께 보내요. 클라이언트는 이 사슬을 위로 따라가다 자기가 믿는 루트(트러스트 앵커) 하나에 닿으면 통과시켜요. 신뢰 저장소는 바로 그 "믿는 루트 목록"이에요. 대부분 PEM 파일 하나에 루트 인증서를 이어 붙인 형태고, PEM 본문은 Base64 텍스트예요. 한 장을 Base64 디코더에 넣으면 바이너리 DER 이 나와요(형식은 Base64 용도와 PEM 글 참고).
문제는 중간 CA 개인 키가 유출됐을 때예요. 키를 가진 사람은 아무 도메인에나 그 중간 CA 서명을 단 인증서를 만들 수 있고, 사슬은 여전히 믿는 루트에 닿아요. 막는 방법은 둘뿐이에요.
- 그 중간 CA 를 폐기·차단 목록으로 막기. 검증기가 CRL·OCSP·CRLite 같은 폐기 정보나 OS 의 차단 데이터를 봐야 해요. 폐기 확인 방식별 차이는 인증서 폐기 확인, CRL·OCSP·CRLite 글에서 다뤘어요.
- 위의 루트를 통째로 빼기. 루트 목록 파일만 보는 검증기에 쓸 수 있는 유일한 방법이에요. 대신 같은 루트 아래의 정상 인증서도 함께 실패해요.
eMudhra 사례는 여기에 한 겹이 더 있었어요. Apple 문서에 따르면 차단한 12장 중 6장은 emSign 루트 아래, 나머지 6장은 Sectigo 교차 서명을 거쳐 AAA Certificate Services 로 이어져요. 그래서 emSign 루트만 빼면 교차 서명 경로로 여전히 통과하고, AAA 까지 빼면 그 루트를 쓰는 다른 정상 사이트가 깨져요. 교차 서명이 있으면 루트 하나 빼기로 끝나지 않아요.
맥 신뢰 저장소 6곳, 루트 수를 직접 세 보니
같은 맥에 깔린 저장소의 인증서 수를 세고, 이번 사건의 두 루트가 들어 있는지 검색했어요. PEM 파일은 BEGIN CERTIFICATE 줄을, 키체인은 security find-certificate -a, JDK 는 keytool -list -cacerts, Node 는 tls.rootCertificates 를 셌어요.
| 저장소 | 이 맥에서 쓰는 프로그램 | 루트 수 | emSign ECC Root CA - G3 | AAA Certificate Services |
|---|---|---|---|---|
| 시스템 키체인 | Safari·Mail·URLSession 앱, Homebrew curl | 156 | 있음(차단 데이터로 막힘) | 있음 |
/etc/ssl/cert.pem | /usr/bin/curl 8.7.1, git, /usr/bin/python3 3.9.6 | 128 | 있음 | 있음 |
| Homebrew ca-certificates | Homebrew openssl@3, Homebrew python 3.13 의 ssl | 190 | 없음 | 없음 |
| certifi 2026.07.22 | requests 등 certifi 를 쓰는 파이썬 라이브러리 | 190 | 없음 | 없음 |
| Node 25.3.0 내장 | node 의 fetch·https | 146 | 있음 | 없음 |
| OpenJDK 24.0.2 cacerts | 자바 HttpClient 등 | 113 | 있음 | 있음 |
눈여겨볼 점이 셋이에요. 첫째, 루트 수가 113개부터 190개까지 다 달라요. 같은 사이트가 어떤 도구에서는 열리고 어떤 도구에서는 "unable to get local issuer certificate" 로 실패하는 흔한 이유예요. 둘째, /etc/ssl/cert.pem 맨 위 주석은 OpenBSD 의 cert.pem 1.22.2.1(2021-09-30)이었어요. OS 와 다른 주기로 관리되는 파일이라는 뜻이에요. 셋째, Mozilla 기반 묶음 두 개(Homebrew ca-certificates, certifi)에는 두 루트가 모두 없었고, 같은 Mozilla 계열인 Node 내장 목록에는 emSign ECC Root CA - G3 이 남아 있었어요. 묶음을 만든 시점에 따라 결과가 달라요.
curl 과 git 은 어느 저장소를 보나, 확인 명령
가장 확실한 방법은 상세 로그에서 CAfile 줄을 보는 거예요.
/usr/bin/curl -sv -o /dev/null https://example.com 2>&1 | grep -E 'CAfile|Native'
# CAfile: /etc/ssl/cert.pem
/opt/homebrew/opt/curl/bin/curl -sv -o /dev/null https://example.com 2>&1 | grep -E 'CAfile|Native'
# Native: Apple SecTrust
GIT_CURL_VERBOSE=1 git ls-remote https://github.com/git/git HEAD 2>&1 | grep CAfile
# CAfile: /etc/ssl/cert.pem
시스템 curl 은 /etc/ssl/cert.pem 을, Homebrew curl 8.19.0 은 Apple SecTrust 를 썼어요. Homebrew curl 의 curl-config --configure 출력에는 --with-apple-sectrust 가 들어 있었어요. curl 의 인증서 검증 문서에 따르면 이렇게 빌드된 curl 이라도 --cacert 로 파일을 직접 주면 SecTrust 를 끄고 그 파일만으로 검증해요. 둘 다 쓰려면 --ca-native --cacert 파일 처럼 함께 줘야 해요.
git 은 더 헷갈려요. Homebrew 로 설치한 git 2.52.0 도 HTTPS 전송 도우미(git-remote-https)가 시스템 /usr/lib/libcurl.4.dylib 를 링크하고 있어서, 결국 /etc/ssl/cert.pem 을 봤어요. otool -L $(brew --prefix git)/libexec/git-core/git-remote-https 로 확인할 수 있어요.
시스템 libcurl 은 curl -V 의 Features 에 MultiSSL 이 있고 백엔드로 SecureTransport 와 LibreSSL 3.3.6 을 함께 갖고 있어요. 그래서 백엔드를 바꾸면 검증 경로도 바뀌어요.
CURL_SSL_BACKEND=securetransport /usr/bin/curl -sv -o /dev/null https://example.com 2>&1 | grep -E 'CAfile|TLS'
git -c http.sslBackend=secure-transport ls-remote https://github.com/git/git HEAD
이렇게 돌리면 CAfile 줄이 사라지고 Apple 의 Secure Transport 가 검증해요. 다만 이 맥에서 두 경우 모두 TLS 1.2 로만 연결됐어요. 기본 LibreSSL 경로는 TLS 1.3 이었으니, 프로토콜 버전을 요구하는 서버가 있다면 이 방법은 맞지 않아요. 설정 이름과 의미는 git-config 문서의 http.sslBackend·http.sslCAInfo 항목에 있어요.
파이썬·Node·자바는 어디를 보나
파이썬: 실행 파일마다 다르고, 라이브러리마다 또 다르다
import ssl
print(ssl.OPENSSL_VERSION)
print(ssl.get_default_verify_paths().cafile)
print(len(ssl.create_default_context().get_ca_certs()))
/usr/bin/python3(Xcode CLT 3.9.6, LibreSSL 2.8.3)는 /private/etc/ssl/cert.pem 과 128개를, Homebrew 3.13.7(OpenSSL 3.6.5)은 /opt/homebrew/etc/openssl@3/cert.pem 과 190개를 출력했어요. 여기에 requests 는 표준 ssl 기본값이 아니라 certifi 의 cacert.pem 을 써요. 한 프로그램 안에서도 urllib 와 requests 가 서로 다른 목록을 볼 수 있다는 뜻이에요. 운영체제 검증을 쓰고 싶다면 truststore 패키지가 "native system trust stores" 로 검증하는 방법을 제공해요. 각 함수의 의미는 파이썬 ssl 모듈 문서에 있어요.
Node: 내장 목록이 기본, 시스템 옵션은 더하기만 한다
Node 는 루트 목록을 바이너리에 넣어 배포해요. Node CLI 문서의 --use-system-ca 는 23.8.0 에 추가됐고, 맥에서는 키체인에서 "항상 신뢰"로 지정한 인증서를 내장 목록과 함께 써요. 즉 내장 목록에 든 루트를 빼 주지는 않아요. 특정 루트를 빼야 한다면 Node 25.3.0 에서 아래처럼 기본 목록을 걸러 다시 넣을 수 있었어요. 146개가 145개가 됐고 fetch 는 정상이었어요.
const tls = require('node:tls');
const { X509Certificate } = require('node:crypto');
const keep = tls.getCACertificates('default').filter(
(pem) => !new X509Certificate(pem).subject.includes('CN=emSign ECC Root CA - G3')
);
tls.setDefaultCACertificates(keep); // 프로세스 시작 직후, 첫 연결 전에
자바: cacerts 파일, 수정보다 별도 트러스트스토어
OpenJDK 24.0.2 의 cacerts 에는 emsigneccrootcag3, comodoaaaca 라는 별칭으로 두 루트가 모두 있었어요. 원본은 JDK 를 올릴 때 덮이니, 복사본을 만들어 keytool 로 별칭을 지우고 -Djavax.net.ssl.trustStore=복사본경로 로 지정하는 쪽이 관리하기 쉬워요.
keytool -list -cacerts -storepass changeit | grep -iE 'emsign|aaa'
러스트: 컴파일할 때 굳는 목록
webpki-roots 크레이트는 루트 목록을 코드로 담아 컴파일해요. 그래서 실행 파일을 다시 빌드해 배포하기 전까지 목록이 바뀌지 않아요. 코드 편집기 Gram 은 3.4.0 릴리스 노트에서 이번 사건 때문에 webpki-root-certs·rustls-webpki 를 올렸다고 밝혔어요. OS 검증을 쓰려면 rustls-platform-verifier가 있고, README 표에 macOS 에서는 Security.framework 로 검증한다고 적혀 있어요.
CA 사고가 났을 때 점검 순서
- 도구별 저장소 찾기. 위의
CAfile줄,get_default_verify_paths(),getCACertificates('default'),keytool -list로 실제로 보는 파일을 적어요. - 문제 루트 검색. PEM 파일은 이름으로 찾아요.
openssl crl2pkcs7 -nocrl -certfile /etc/ssl/cert.pem \
| openssl pkcs7 -print_certs -noout \
| grep '^subject=' | grep -E 'emSign|AAA Certificate Services'
이 맥의 /etc/ssl/cert.pem 에서는 emSign 루트 4개(G1, ECC G3, C1, ECC C3)와 AAA Certificate Services 가 나왔어요. 이름은 겹칠 수 있으니 확실히 하려면 지문으로 비교해요. 지문이 무엇인지는 MD5·SHA-256 해시 차이 글을, 직접 계산해 맞대 보려면 해시 생성기를 쓰면 돼요.
- 경로를 바꿀지, 루트를 뺄지 고르기. 시스템 검증으로 돌릴 수 있으면 그쪽이 부작용이 작아요(Homebrew curl,
secure-transport백엔드, truststore, rustls-platform-verifier). 파일에서 루트를 빼야 한다면 Apple 문서의 백업 → 제거 → 확인 순서를 그대로 따르고, 교차 서명 경로까지 막을지는 정상 사이트 피해와 저울질해요. - 정상 사이트 확인. 같은 루트를 쓰는 사이트와 사내 API 에 연결해 보고, 실패하면 백업으로 되돌려요.
- 업데이트 뒤 재확인. Apple 문서는 macOS 업데이트가
/etc/ssl/cert.pem을 Apple 판으로 덮을 수 있다고 적었어요. Homebrew·pip·Node·JDK 업데이트도 각자 목록을 바꿔요.
흔한 실수와 판단 기준
| 상황 | 흔한 실수 | 판단 기준 |
|---|---|---|
| 브라우저는 막혔는데 curl 은 열림 | curl 이 고장 났다고 생각함 | 서로 다른 저장소를 보는 정상 동작이에요. CAfile 줄부터 봐요 |
| Homebrew 로 git 을 깔았음 | Homebrew 인증서를 쓴다고 가정함 | 이 맥에서는 시스템 libcurl 을 링크해 /etc/ssl/cert.pem 을 썼어요 |
| 사내 프록시 CA 를 키체인에 넣었음 | 파이썬·Node 도 믿을 거라 기대함 | 키체인을 보지 않는 도구는 SSL_CERT_FILE·NODE_EXTRA_CA_CERTS 따로 |
| "certificate verify failed" 해결 | verify=False, -k 로 넘김 | 검증을 끄면 위조 인증서도 통과해요. 빠진 루트·중간 CA 를 찾아 넣어요 |
| 루트 제거 후 정상 | 이제 끝났다고 봄 | OS·런타임 업데이트가 파일을 되돌릴 수 있어요. 재확인 일정을 잡아요 |
자주 묻는 질문
맥에서 curl 이 쓰는 CA 인증서 파일은 어디인가요?
/usr/bin/curl 은 /etc/ssl/cert.pem 을 써요. Homebrew 로 깐 curl 은 빌드에 따라 Apple SecTrust 를 쓰기도 해요. curl -sv https://example.com 2>&1 | grep -E 'CAfile|Native' 로 내 curl 이 무엇을 보는지 바로 확인할 수 있어요.
/etc/ssl/cert.pem 을 직접 고쳐도 되나요?
관리자 권한으로 고칠 수는 있지만 macOS 업데이트가 덮을 수 있고, 루트를 잘못 빼면 정상 사이트가 깨져요. Apple 지원 문서도 백업을 먼저 만들고 확인 명령을 돌린 뒤, 문제가 생기면 백업으로 되돌리라고 안내해요. 가능하면 도구를 시스템 검증 경로로 바꾸는 쪽을 먼저 검토하세요.
파이썬 requests 는 맥 키체인을 보나요?
기본으로는 보지 않아요. requests 는 certifi 패키지의 cacert.pem 을 써요. 키체인을 쓰게 하려면 truststore 패키지처럼 운영체제 검증을 쓰는 방법을 붙여야 하고, 사내 CA 만 더하려면 REQUESTS_CA_BUNDLE 로 파일을 지정해요.
Node 의 --use-system-ca 를 켜면 내장 루트 문제가 해결되나요?
아니에요. 맥에서 이 옵션은 키체인에서 신뢰로 지정한 인증서를 내장 목록에 더해 쓰는 것이라, 내장 목록에 든 루트는 그대로 남아요. 특정 루트를 빼려면 tls.getCACertificates('default') 를 걸러 tls.setDefaultCACertificates() 로 다시 넣거나 Node 를 목록이 갱신된 버전으로 올려요.
중간 CA 키 유출과 루트 CA 키 유출은 대응이 어떻게 다른가요?
루트가 새면 저장소에서 그 루트를 빼면 되지만, 중간 CA 가 새면 루트는 멀쩡해서 빼면 정상 인증서까지 깨져요. 그래서 중간 CA 사고는 폐기 확인이나 OS 차단 목록처럼 "이 중간 CA 만" 막을 수 있는 경로가 필요해요. 파일 하나만 보는 도구가 약한 이유가 여기에 있어요.
정리
- 맥 한 대 안에 시스템 키체인(156),
/etc/ssl/cert.pem(128), Homebrew·certifi 묶음(190), Node 내장(146), JDK cacerts(113)가 따로 있어요. Apple 차단 목록은 SecTrust 경로에만 자동으로 걸려요. - 도구 이름이 아니라 실제 검증 경로를 확인해요. 시스템 curl·git 은
/etc/ssl/cert.pem, Homebrew curl 은 SecTrust, requests 는 certifi 였어요. - CA 사고 때는 저장소 찾기 → 루트 검색 → 경로 변경 또는 루트 제거 → 정상 사이트 확인 → 업데이트 뒤 재확인 순서로 가요. 루트 이름 대신 지문으로 확인하려면 해시 생성기를, 폐기 확인 방식은 CRL·OCSP·CRLite 글을 함께 보세요.
더 깊이 읽기
이 글은 실무에서 판단할 때 필요한 것 위주로 정리했어요. 같은 주제의 내부 동작 원리는 devboi 기술 블로그의 ETH 반증·eMudhra CA 키 유출·blaze — IT 뉴스에서 도식과 코드로 단계별로 풀었어요.