중간 CA 침해: 신뢰 체인이 끊어질 때
eMudhra 중간 CA 침해가 약한 키로 웹 PKI 신뢰를 무너뜨린 과정과 클라이언트 점검·패치 방법을 정리했습니다.

여기 거꾸로 들리는 주장이 하나 있습니다. 가장 최악의 인증기관(CA) 사고는 공격자가 침입한 사고가 아니라는 겁니다. 최근 발생한 eMudhra 중간 CA 침해가 그 증거입니다. 인증서 자체에서 중간 CA 키 8개를 복구할 수 있었고, 침입도 필요 없었습니다. CA의 키 생성 절차 자체가 너무 약한 개인키를 만들어냈고, 그중 하나는 말 그대로 숫자 4였습니다. 누군가 키를 훔쳐 간 게 아닙니다. 키가 태어날 때부터 손상되어 있었던 겁니다. 이건 어떤 침해보다 무서운 실패 양상입니다. 자격 증명을 교체하고 구멍을 패치하는 것으로는 고칠 수 없으니까요. 신뢰 자체가 첫날부터 결함이 있었던 겁니다.
브라우저 밖에서 TLS 연결을 만드는 무언가를 운영하고 있다면 — 그리고 거의 다 그렇습니다 — 이 이야기는 꼭 봐야 합니다. 브라우저는 며칠 안에 보호됐습니다. 나머지 생태계, 즉 cron 작업의 curl 호출, certifi를 고정(pinning)한 Python 서비스, 루트 저장소가 바이너리에 박혀 있는 Go 바이너리는 아직 따라잡는 중입니다. 실제로 무슨 일이 있었는지, 중간 CA 침해가 리프 인증서 침해보다 본질적으로 더 나쁜 이유는 무엇인지, 그리고 고르지 않은 대응 과정이 현대 인터넷의 폐기(revocation) 상태에 대해 무엇을 말해 주는지 차근차근 살펴보겠습니다.
eMudhra에서 실제로 무슨 일이 있었나
CA가 Mozilla 이슈 트래커에 올린 예비 평가에 따르면, 이야기는 놀랄 만큼 평범합니다. CA가 가장 민감한 키를 만들 때 쓰는 통제되고 감사되는 절차인 키 생성 세리머니 스크립트가, 키 래핑/언래핑 절차를 내보내기용으로 테스트하려고 승인된 편차를 적용해 수정되었습니다. 그 수정된 코드의 결함 때문에 HSM에서 두 번째 ECC 키 세트가 생성됐고, 그 키들이 약했습니다. '엔트로피가 조금 줄어든' 정도가 아닙니다. 공개 인증서만 보고도 간단히 복구할 수 있을 정도로 약했습니다.
그 의미를 생각해 보세요. ECC 개인키는 무작위 256비트 수여야 합니다. 전체 방식의 보안은 이산 로그 문제에 기대고 있습니다. 공개키가 주어졌을 때 개인 스칼라를 찾는 것이 계산상 불가능해야 합니다. 하지만 '무작위' 스칼라가 1바이트라면, 공개키는 생성점에 그 바이트를 곱한 값일 뿐이고, 노트북으로도 256가지 가능성을 마이크로초 단위로 전부 무차별 대입할 수 있습니다. 보고된 8개 키 중 하나는 정수 4였습니다. 이건 전통적 의미의 '침해'가 아닙니다. 애초에 한 번도 안전하지 않았던 인증서이고, 그것이 Mozilla, Microsoft, Apple, Chrome이 신뢰하는 루트 아래에서 발급된 겁니다.
이 문제를 보고한 연구자 Wayne은 8개 개인키 각각으로 메시지에 서명해 그 서명을 루트 프로그램에 제출하는 방식으로 키 소유를 증명했습니다. 누구든 OpenSSL 명령 몇 개로 검증할 수 있었습니다. 각 중간 인증서에서 공개키를 추출한 다음, 메시지 서명이 그 공개키로 검증되는지 확인하면 됩니다. 암호학적 키 소유 증명의 우아하고도 섬뜩한 점이 바로 이것입니다. 모호함이 없고, '보안을 진지하게 생각한다'는 식의 홍보성 안개도 없습니다. 수학이 맞거나 틀리거나 둘 중 하나입니다. 이번엔 맞았습니다.
중간 CA 침해가 리프 침해보다 더 나쁜 이유
리프 인증서의 개인키가 유출되면 — 예를 들어 api.example.com 키가 공개 GitHub 저장소에 올라가면 — 영향 범위는 호스트명 하나이고, 와일드카드라면 몇 개 정도입니다. 인증서를 폐기하고 새로 발급받고 넘어가면 됩니다. 성가시긴 하지만 존재를 위협할 정도는 아닙니다.
중간 CA 키는 완전히 다른 종류입니다. 웹 PKI는 서명의 체인으로 동작합니다. 루트 CA(오프라인에 보관되고 핵물질처럼 경계되는)가 중간 CA에 서명하고, 중간 CA가 서버가 실제로 제시하는 리프 인증서에 서명합니다. 중간 CA의 개인키를 가진 자는 암호학적으로 모든 목적에서 그 CA 자체입니다. 그들은 google.com, 여러분의 은행, 내부 SSO 도메인에 대한 유효한 인증서를 발급할 수 있고, 그 체인을 신뢰하는 모든 클라이언트는 아무런 불평 없이 받아들입니다. 호출할 중앙 기관도, 요청 제한도 없습니다. 위조이지만 유효한 서명과 정당한 서명을 구별할 방법도 없습니다.
eMudhra 사고는 이것을 중간 CA 8개로 곱해 놓았습니다. 일부는 Sectigo 루트를 통한 교차 서명(cross-sign)이었고, 수십억 대 기기에 배포된 신뢰 저장소로 이어져 있었습니다. 문제가 있는 인증서가 만들어진 시점부터 공개될 때까지 대략 1년 반 동안, 그 중간 인증서를 신뢰하면서 Certificate Transparency를 강제하지 않은 클라이언트는 키를 복구할 마음만 먹은 누군가에게 사칭당할 수 있었습니다. 이것이 그 기간입니다. 누군가 악용했는지는 알 수 없습니다. CT 로그에서 운 좋게 잡히거나 관측된 공격이 없다면, 영영 모를 수도 있습니다.
루트 프로그램이 중간 CA 사고를 리프 사고에는 결코 붙지 않는 심각도로 다루는 이유가 이것입니다. 중간 CA를 신뢰 목록에서 제외(distrust)하면 그 CA가 발급한 모든 인증서가 무효가 됩니다. 수천 개 웹사이트가 한꺼번에 깨지는 겁니다. 수년에 걸쳐 진행된 Symantec 불신 사태는 CA의 운영이 신뢰할 수 없다고 판단될 때 전면 대응이 얼마나 고통스러워지는지를 보여 주는 대표 사례입니다. 시스템은 작동하지만, 수술이라기보다 절단처럼 작동합니다.
우연히 영웅이 된 Certificate Transparency
이야기에서 더 주목받아야 할 부분이 여기입니다. Chrome, Safari, Firefox, Edge는 문제가 있다는 사실이 알려지기 전에 이미 보호되고 있었다고 볼 수 있습니다. 폐기 덕분이 아니라 Certificate Transparency 덕분입니다.
CT를 강제하는 브라우저는 자신들이 받아들이는 모든 공개 신뢰 인증서가 공개적으로 추가만 가능한(append-only) CT 로그에 기록되어 있기를 요구합니다. 문제가 된 eMudhra 중간 인증서는 해당 기간의 CT 로그 기록이 없습니다. 즉, 공격자가 그 키로 만든 리프 인증서는 유효한 서명된 인증서 타임스탬프 없이 도착하게 되어 CT를 강제하는 클라이언트에서 어차피 거부됐을 겁니다. 위조는 믿어지려면 공개되어야 했고, 공개하는 순간 바로 드러났을 겁니다.
CT는 사후에 잘못된 발급을 잡아내고 불법 인증서를 탐지할 수 있게 하려고 설계되었습니다. 이번 사고에서는 예방 통제로 작동했습니다. 대량 가로채기를 위한 가장 가치 높은 대상인 브라우저 사용자층을 상대로 손상된 키를 거의 쓸모없게 만든 겁니다. 대부분의 엔지니어가 거의 생각하지 않는 인프라에 대해 정말 좋은 소식입니다.
하지만 — 그리고 여기서 제 낙관론의 한계를 인정해야 합니다 — CT 강제는 본질적으로 브라우저만의 사치입니다. 인터넷의 대다수 TLS 클라이언트는 SCT를 검사하지 않습니다. 여러분의 curl 호출도 그렇지 않고, Python requests 호출도 그렇지 않습니다. 벤더 클라우드로 홈 신호를 보내는 IoT 기기들도 마찬가지입니다. 이런 모든 경우에 유일한 보호는 신뢰 저장소에 폐기 데이터가 제때 도달하는 것이었고, 이 지점에서 이야기가 지저분해집니다.
폐기 격차: 브라우저와 그 밖의 모든 것
브라우저 벤더들은 이 문제를 잘 처리했고, 10년 전에 구축한 인프라에 대해 공을 인정받을 만합니다. Mozilla는 10년 넘게 중간 및 루트 인증서를 불신 처리하기 위한 푸시 메커니즘인 OneCRL을 제공해 왔고, 이번 사고도 브라우저 업데이트 없이 몇 시간 안에 Firefox 사용자에 대해 조치했습니다. Chrome의 CRLSet과 Apple의 유사한 메커니즘도 며칠 안에 각 플랫폼에서 같은 일을 했습니다. CA가 중간 인증서를 폐기했습니다. 브라우저 사용자 입장에서 이 사고는 시작되기도 전에 거의 끝나 있었습니다.
이제 브라우저가 아닌 세계를 봅시다. 대응 목록은 소프트웨어 공급망 재고 목록처럼 읽힙니다. Ubuntu, Debian, Fedora, Alpine, Arch, Gentoo용 업데이트된 ca-certificates 패키지, 새 NSS 릴리스, 새 curl CA 번들, Python용 certifi, Rust용 rustls-webpki와 webpki-roots, Go 포인트 릴리스, 세 개 릴리스 라인에 걸친 Electron 패치 버전, Composer의 ca-bundle, Elixir의 castore가 있습니다. 이것들은 각각 별도의 아티팩트이고, 별도의 릴리스 주기를 따르며, 별도의 업데이트 메커니즘으로 소비됩니다. 일부는 공개 후 며칠이 지나도 여전히 대기 중이었습니다.
그리고 제가 밤잠을 설치게 만드는 범주가 있습니다. 바이너리 안에 자체 CA 저장소를 번들로 넣는 소프트웨어입니다. 작년에 빌드된 정적 링크 Go 서비스는 컴파일된 결과물 안에 루트를 담고 있습니다. 재빌드하고 재배포하는 것만이 유일한 해결책입니다. 패치할 시스템 패키지가 없으니까요. 여기에 레지스트리의 모든 컨테이너 이미지(내장된 /etc/ssl/certs 레이어가 있는), 데스크톱의 모든 Electron 앱, 연결된 모든 기기의 펌웨어 이미지를 곱해 보면 문제의 윤곽이 보입니다. 시스템 신뢰 저장소는 쉬운 부분입니다. 임베디드와 OT 기기가 보안 사고를 어떻게 수년짜리 사태로 바꾸는지는 이전에 다룬 적이 있습니다. 침해된 CA는 바로 그 기기들이 결코 고쳐지지 않는 종류의 사건입니다.

아무도 소유하지 않은 신뢰 저장소의 긴 꼬리
이 사고에는 너무도 완벽하게 예시적인 특수 사례가 있어 별도의 문단을 쓸 가치가 있습니다. 바로 macOS입니다. Apple 플랫폼은 침해된 중간 인증서를 OS 수준에서 신속하게 차단했습니다. 하지만 기본 제공되는 /usr/bin/curl은 업데이트되지 않은 /etc/ssl/cert.pem 파일로 검증하고, 폐기 확인도 하지 않습니다. 그래서 완전히 패치된 Mac에서 Safari는 위조 인증서를 거부하는데, 같은 기기의 curl은 기꺼이 받아들일 수 있습니다. 한 대의 기기에서 두 개의 신뢰 결정이 정반대 답을 내는 겁니다.
이 파편화가 진짜 교훈입니다. 우리는 종종 '웹 PKI'를 하나의 시스템인 것처럼 이야기합니다. 실제로는 전혀 다른 속성을 가진 수십 개의 시스템입니다.
- 브라우저: 푸시 방식 폐기(OneCRL, CRLSet), CT 강제, 자동 업데이트. 대응은 몇 시간에서 며칠 단위입니다.
- OS 신뢰 저장소: 패키지 관리자 기반 업데이트(ca-certificates, ca_root_nss). 대응은 배포판의 릴리스 주기와 머신이 실제로 패치를 받는지에 달려 있습니다.
- 언어 생태계: certifi, webpki-roots, Go의 내장 루트, Node의 번들 Mozilla 저장소. 대응은 릴리스를 자르고 수천 개 프로젝트에서 의존성 업데이트를 기다리는 것을 뜻합니다.
- 컴파일된 아티팩트와 이미지: 바이너리, 컨테이너, 펌웨어. 대응은 재빌드와 재배포를 뜻하며, 실제로는 취약한 꼬리가 수년에 걸쳐 이어집니다.
- IoT와 임베디드: 흔히 업데이트 경로가 전혀 없습니다. 사실상 영구적인 노출입니다.
브라우저는 정확히 이런 시나리오에 10년을 투자해 왔습니다. 다른 곳은 거의 아무도 그러지 않았습니다. Mozilla 쪽 상류 댓글은 직설적으로 말했습니다. 브라우저 생태계 밖에서는 폐기에 대한 투자가 거의 없었고, 그런 클라이언트에게 이런 종류의 사고는 엄청난 문제라고요. 인프라를 운영하며 본 것과도 일치합니다. 클라우드 인증 이벤트가 로깅되는 방식의 사각지대를 겪어 보셨다면 이 패턴을 알아보실 겁니다. 중요한 경로는 잘 계측되어 있고, 긴 꼬리는 어두운 방입니다.
클라이언트가 아직 취약한지 확인하는 방법
이론은 제쳐 두고, 실제로 여러분의 환경을 테스트해야 합니다. TLS의 좋은 점은 경험적으로 테스트할 수 있다는 겁니다. 커뮤니티 구성원들이 침해된 중간 인증서로 서명된 인증서를 제공하는 호스트를 띄웠습니다. 클라이언트가 인증서 오류 없이 연결된다면, 여전히 그들을 신뢰하고 있는 겁니다. curl이나 어떤 명령줄 도구든 테스트하는 일반적인 패턴은 다음과 같습니다.
# A certificate error here means you're protected.
# A successful response means this client still trusts a compromised intermediate.
curl -v https://dv-g3b-emudhra.we-have-a-problem.com/
# You can also test whether a client will accept a forged cert for an
# arbitrary hostname by pointing it at a test server:
curl -v https://example.org --connect-to example.org::2.29.126.86:
중요한 모든 맥락에서 이것을 실행하세요. 개발자 노트북, CI 러너, 컨테이너 베이스 이미지, 서버 플릿이 모두 해당됩니다. 결과는 각각 다를 것이고, 그 차이가 바로 핵심입니다. 업그레이드가 필요한 것들을 빠르게 점검하려면 다음을 보세요.
- Go 서비스: 루트가 컴파일되어 들어가 있으므로 Go 1.27.3 또는 1.26.10 이상으로 재빌드하세요.
- Python: certifi 2026.10.19 이상 (
pip과uv버전도 확인하세요. 이들도 자체 번들을 가집니다). - Rust: rustls-webpki 0.103.16, webpki-roots / webpki-root-certs 1.0.10.
- Node.js와 Electron: 릴리스 라인별로 대기 중이거나 패치 릴리스를 확인하고, 배포되는 각 아티팩트를 점검하세요.
- 시스템 저장소: 업데이트된 ca-certificates 패키지가 Ubuntu, Debian, Fedora, Alpine, Arch, Gentoo용으로 나와 있습니다. FreeBSD의 ca_root_nss는 글 작성 시점에 대기 중이었습니다.
패키지를 기다릴 수 없다면, 대부분의 리눅스 배포판에서 영향받는 루트를 수동으로 불신 처리할 수 있습니다. Debian/Ubuntu에서는 /etc/ca-certificates.conf에서 해당 인증서 앞에 !를 붙이고 update-ca-certificates를 실행하면 됩니다. Fedora/RHEL 계열에서는 루트를 /etc/pki/ca-trust/source/blocklist/에 추출한 뒤 update-ca-trust를 실행합니다. NixOS에는 깔끔한 선언형 옵션이 있습니다.
security.pki.caCertificateBlacklist = [
"Comodo AAA Services root"
"emSign ECC Root CA - G3"
];
이런 우회책에서 실제로 무엇을 불신하는지 주목하세요. 중간 인증서들이 체인으로 연결되는 것은 루트입니다. 이것이 PKI 대응의 투박한 현실입니다. 대부분의 신뢰 저장소 도구에는 '이 중간 인증서만 불신'하는 기능이 없으니, 루트를 차단 목록에 올리고 관련 없는 인증서가 입는 부수적 피해를 감수해야 합니다. 비상시에는 올바른 선택이지만, 그것도 하나의 트레이드오프입니다.
제 역발상 주장이 무너지는 지점
글을 시작할 때 저는 자체적으로 만든 키 생성 실패가 침해보다 더 나쁘다고 주장했습니다. 그 주장이 어디서 깨지는지 솔직히 말씀드려야겠습니다. DigiNotar 같은 전통적 침해는 최소한 CA가 공격자에게 뒤처지기 전까지는 능력 있게 운영되고 있었다는 것을 암시합니다. 그리고 유능한 CA야말로 전체 시스템이 기대는 대상입니다. 개인키 '4'를 내보낸 키 세리머니는, 감사된 통제, HSM 절차, 컴플라이언스 체크리스트 중 어느 것도 CA가 수행하는 가장 민감한 작업의 치명적 결함을 잡아내지 못했다는 뜻입니다. 이것은 적대자의 문제가 아니라 보증(assurance)의 문제이고, 보증은 CA가 파는 제품 그 자체입니다.
주장이 실제로 실패하는 지점도 있습니다. 침해는 막기가 더 어렵고, 더 오래 조용할 수 있습니다. 조심스럽게 중간 CA 키를 훔친 공격자는 그것을 아껴서 표적 가로채기에만 쓰고, CT를 강제하는 클라이언트는 한 번도 건드리지 않을 수 있습니다. 반면 약한 키는 공개 인증서를 스캔하는 누구나 발견할 수 있습니다. 바로 이번 사고가 드러난 방식이 그랬습니다. 치명적이지만 명백한 실패가, 미묘하지만 표적화된 실패보다 낫다는 주장도 있습니다. 저는 완전히 동의하지는 않지만, 그렇다고 무시할 수도 없습니다. 진실은 두 실패 모드가 같은 곳에 도착한다는 겁니다. 불신해야 하는 중간 인증서, 그리고 몇 달 동안 소식을 듣지 못할 클라이언트의 긴 꼬리입니다.
실제로 무엇을 해야 하나
웹 PKI 자체를 고칠 수는 없습니다. 다음 실패에 대한 노출을 줄일 수는 있습니다.
- 가정하지 말고 테스트하세요. 침해된 체인을 제공하는 호스트를 상대로 클라이언트를 점검하세요. 반나절이면 되고, 놀라운 결과가 나올 겁니다.
- 컴파일된 아티팩트를 재빌드하세요. Go 바이너리와 컨테이너 이미지는 자신의 신뢰 저장소를 함께 들고 다닙니다. 재배포하기 전까지 '베이스 이미지를 패치했다'는 말은 의미가 없습니다.
- 신뢰 저장소를 목록화하세요. 열거하지 않은 것은 대응할 수 없습니다. certifi 사본, 벤더링된 번들, Electron 앱을 중요한 것처럼 세세히 세세요. 실제로 중요하니까요.
- 가능하면 시스템 신뢰 저장소를 쓰세요. 벤더링된 번들마다 여러분이 잊고 배포하지 않을 패치가 하나씩 생깁니다. OS 저장소로 중앙화한다고 문제가 사라지지는 않지만, 책임질 대상이 한 곳이 됩니다.
- CT와 폐기 생태계를 지켜보세요. 브라우저 수준의 폐기를 브라우저 밖 클라이언트로 가져오려는 프로젝트가 존재하며 지원받을 만합니다. 이번 사고는 그들이 받을 수 있는 가장 좋은 자금 조달 논거일 겁니다.
- CA 사고를 보안 사고로 취급하세요. 유출된 자격 증명과 똑같이, 담당자를 호출하고, 재고 조사를 실행하고, 문서로 기록을 남겨야 합니다. 실제로 그런 것이니까요.
불편한 요약입니다. 웹 PKI의 비상 브레이크는 가장 덜 필요로 했던 클라이언트에서 정말 잘 작동합니다. CT가 이미 그들을 보호하고 있었으니까요. 브레이크가 필요했던 클라이언트 — 헤드리스, 임베디드, 벤더링된, 잊힌 것들 — 은 대부분 브레이크가 없습니다. 다음 중간 CA 침해는 '일어날지 여부'가 아니라 '언제'의 문제입니다. 여러분이 통제할 수 있는 유일한 변수는 여러분의 환경이 그것을 알아차릴지 여부입니다.


