JDK가 유출된 인증서를 믿을 때: CVE-2026-91403 eMudhra 대응기
JDK 기본 신뢰 목록(cacerts)에 남은 eMudhra 루트 때문에 생긴 TLS 중간자 위험을, 온프레미스 고객사별로 Amazon Corretto 수정판을 배포하거나 현장에서 cacerts의 루트 인증서 4개를 지워 막은 과정을 정리합니다.
10월 21일, Oracle이 정기 패치와 별개로 Java 긴급 보안 공지를 냈습니다. CVE-2026-91403, CVSS 9.1이었습니다. 공지를 읽어 보니 고칠 코드가 없는 취약점이었습니다. 문제는 JDK에 기본으로 들어 있는 신뢰 인증서 목록이었습니다.
제가 담당하는 제품은 온프레미스로 고객사에 납품되고, 고객사에 따라 Java 17과 Java 8이 섞여 있습니다. 패치 파일을 바로 반입해 적용할 일정이 잡히는 고객사가 있었고, 일정상 파일 반입을 기다리기보다 현장에서 직접 조치해야 하는 고객사도 있었습니다. 이 글에서는 취약점이 어떻게 생겼는지, 어떤 연결이 위험한지, 그리고 두 부류의 고객사를 각각 어떻게 조치했는지 정리합니다.
TL;DR
- 원인은 JDK의 기본 신뢰 목록(cacerts)이고, 고칠 애플리케이션 코드는 없습니다. 개인키가 유출된 eMudhra 중간 인증서의 경로를 JDK가 신뢰하고 있었습니다.
- 위험한 것은 Java가 밖으로 거는 TLS 연결(LDAPS, 메일 발송, DB SSL, 외부 API)뿐입니다. 서버가 받는 HTTPS와 SSH는 해당하지 않습니다.
- 패치 파일을 바로 반입할 수 있는 고객사는 공지 직후 수정판이 나온 Amazon Corretto로 JDK를 바꿔 패치했습니다.
- 현장 대응이 필요한 고객사는 cacerts에서 루트 인증서 4개를 지우고 Java 프로세스를 재시작했습니다.
1. 유출된 중간 인증서 하나로 가짜 서버가 진짜가 된다
TLS 클라이언트는 서버가 내민 인증서를 그대로 믿지 않습니다. 서버 인증서에 서명한 중간 인증서, 그 중간 인증서에 서명한 루트 인증서로 서명을 따라 올라가고, 마지막 루트가 자기 신뢰 목록에 있을 때만 통과시킵니다. Java에서 이 신뢰 목록이 JDK와 함께 배포되는 cacerts 파일입니다.
이번에는 인도 인증기관 eMudhra의 중간 인증서 8개에서 개인키가 유출됐습니다. 이 중간 인증서들은 cacerts에 들어 있는 emSign 루트들 아래에 있습니다. 유출된 키로 아무 도메인의 서버 인증서나 만들 수 있고, 그 인증서는 Java가 보기에 정상 체인을 갖춘 인증서가 됩니다.


공격자가 Java 애플리케이션과 실제 서버 사이에 자리를 잡으면 이 가짜 인증서로 중간자 공격(MITM)을 할 수 있습니다. Java는 상대가 진짜 서버라고 믿고 LDAP 계정 정보나 DB 쿼리를 보내고, 공격자는 그것을 읽거나 바꿔서 실제 서버로 넘깁니다. 공격자에게 필요한 것은 네트워크 경로상의 위치뿐이고, 인증이나 사용자 조작은 필요 없습니다. CVSS 9.1에 기밀성·무결성이 모두 High로 매겨진 이유입니다.1
OpenJDK는 cacerts에서 지울 루트로 eMudhra의 emSign 루트 3개와 함께 Comodo AAA 루트를 지정했습니다.2 Comodo AAA를 함께 지우는 이유는 안내에 나와 있지 않습니다. 저는 Comodo AAA가 eMudhra 인증서에 교차 서명을 해 준 경로가 있다고 보고 있지만, 확인한 사실은 아니고 추정입니다.
그렇다면 고객사에 설치된 제품에서 어떤 연결이 이 경로를 타고 있을까요.
2. 위험한 건 Java가 밖으로 거는 연결뿐이다
cacerts는 Java가 상대 서버의 인증서를 검증할 때 쓰입니다. 그래서 영향 범위는 Java가 클라이언트가 되는 연결로 좁혀집니다.
| 연결 | 영향 | 이유 |
|---|---|---|
| Java가 밖으로 거는 TLS (LDAPS 로그인, 메일 발송, DB SSL, 외부 API 호출) | 있음 | 상대 서버 인증서를 cacerts로 검증한다 |
| 서버가 받는 HTTPS (사용자가 웹 화면에 접속) | 없음 | 상호 TLS(클라이언트 인증서)를 쓰지 않는 한 상대를 cacerts로 검증하지 않는다 |
| SSH / SFTP | 없음 | TLS가 아니다 |
사용자가 웹 화면에 접속하는 HTTPS는 서버가 자기 인증서를 내미는 쪽이라 이번 취약점과 관계가 없습니다. 점검할 대상은 LDAP 서버에 붙어 로그인하는 기능, 메일 서버에 TLS로 붙어 메일을 보내는 기능, 외부 DB에 SSL로 붙는 기능처럼 Java가 클라이언트가 되는 기능입니다.
제품이 아닌 다른 Java 프로그램이 별도 trustStore를 지정해 쓰고 있다면, cacerts를 고쳐도 그 프로그램에는 반영되지 않으니 자기 trustStore를 따로 점검해야 합니다.
3. 버전 번호만 봐서는 판단이 안 된다
OpenJDK 공지에 적힌 영향 버전은 다음과 같습니다.1
| 계열 | 영향 버전 |
|---|---|
| Java 8 | 8u504 이하 |
| Java 11 | 11.0.32.1 이하 |
| Java 17 | 17.0.21 이하 |
| Java 21 | 21.0.13 이하 |
| Java 25 / 27 | 25.0.5 / 27.0.1 이하 |
표만 보면 "우리 버전이 표 안에 있나"로 끝날 것 같지만, 두 가지가 판단을 헷갈리게 했습니다.
배포사마다 번호가 다릅니다.
Oracle JDK와 OpenJDK 빌드는 같은 시기의 업데이트에도 번호가 다르게 붙습니다. 이번 공지에서도 Java 8의 영향 버전을 Oracle은 8u511로, OpenJDK는 8u504 이하로 적었습니다.
Oracle 공지는 지원 기간(Premier·Extended Support) 안의 버전만 적고, 그보다 오래된 버전도 영향을 받을 가능성이 높다고 덧붙입니다.3 OpenJDK 공지는 "and earlier"로 그 이하 전체를 포함합니다.
서버의 java -version 두 번째 줄에서 배포사를 먼저 확인하고, 그 배포사의 공지와 맞춰 봐야 합니다.
Java 8은 버전에 따라 지울 인증서 수가 다릅니다.
emSign 루트 3개는 2023년 12월에 OpenJDK 8u 소스에 추가됐습니다(JDK-8319187).4
그래서 jdk8u392-ga 이하 빌드에는 emSign 루트가 없고 Comodo AAA만 있습니다.
오래된 Java 8 서버에서 삭제 대상을 조회했을 때 1개만 나오는 건 정상입니다.
버전으로 해당 여부를 확인했으니, 이제 어떻게 막을지 고를 차례입니다.
4. 패치 파일을 반입할 수 있는 고객사와 없는 고객사는 다르게 막았다
검토한 대응 방법은 네 가지였습니다.
| 방안 | 내용 | 맞는 상황 |
|---|---|---|
| ① 수정판 JDK로 교체 | 수정이 들어간 JDK 빌드로 바꾸고 재시작 | 근본 조치. 수정판이 나와 있고 JDK를 교체할 수 있을 때 |
| ② cacerts에서 인증서 삭제 | 루트 4개를 지우고 재시작 | JDK를 당장 교체할 수 없을 때 |
| ③ 인증서 폐기 확인 켜기 | -Dcom.sun.net.ssl.checkRevocation=true -Dcom.sun.security.enableCRLDP=true | ②도 할 수 없을 때 |
| ④ OS 인증서 패키지 갱신 | OS 패키지로 설치한 JDK는 ca-certificates 업데이트 | RHEL·Fedora 계열 OS 패키지 JDK |
OpenJDK 안내도 ①을 먼저 권하고, 수정판이 없을 때 ②~④를 쓰라고 합니다.2 ③은 인증서 폐기 목록(CRL)을 받으러 외부로 나가야 해서, 외부 인터넷이 막힌 망에서는 동작을 기대하기 어렵습니다. ④는 OS 패키지로 설치한 JDK에만 해당합니다.
저희는 ①과 ②를 썼고, 어느 쪽을 쓸지는 고객사마다 갈렸습니다. 온프레미스 제품이라 수정판 JDK를 쓰려면 패치 파일을 고객사 망 안으로 반입해야 하고, 반입 일정은 고객사마다 다릅니다.


패치 파일을 바로 반입할 수 있는 고객사: Amazon Corretto 수정판으로 교체
일정상 패치 파일을 바로 반입할 수 있는 고객사는 JDK 배포사를 Amazon Corretto로 바꿔 수정판으로 패치했습니다. 공지 직후 수정판을 바로 받을 수 있었던 곳이 Corretto였습니다.
Corretto 릴리스 노트에는 10월 20일 정기 업데이트에 CVE-2026-91403이, 10월 24일 업데이트에 후속 이슈 CVE-2026-114820이 올라 있습니다.56 OpenJDK 안내는 10월 24일 갱신에서 "최근 며칠 사이 업데이트했더라도 다시 조치하라"고 덧붙였기 때문에, 저희는 두 CVE가 모두 들어간 10월 24일 빌드를 배포했습니다.
| 계열 | 배포한 Corretto 빌드 | 릴리스 날짜 |
|---|---|---|
| Java 17 | 17.0.21.0.11 | 2026-10-24 |
| Java 8 | 8.512.09.1 | 2026-10-24 |
배포사를 바꿀 때 걸리는 건 JDK 자체보다 주변 설정이었습니다.
Tomcat의 setenv.sh 같은 곳에 JAVA_HOME이 이전 경로로 박혀 있으면 새 JDK를 깔아도 이전 JDK로 실행됩니다.
교체한 뒤에는 ps -ef | grep java로 실제 프로세스가 새 경로의 java를 쓰는지 확인해야 합니다.
현장 대응이 필요한 고객사: cacerts에서 루트 4개 삭제
일정상 파일 반입보다 현장 대응이 맞는 고객사는 설치된 JDK를 그대로 두고 cacerts에서 루트 인증서를 지웠습니다. 새 파일을 들여올 필요 없이 서버에 이미 있는 keytool만으로 할 수 있어서, 현장에서 바로 처리할 수 있습니다. 다음 섹션이 그 절차입니다.
5. cacerts에서 루트 4개 지우기
지울 대상은 OpenJDK 안내에 적힌 4개입니다.2
emsignrootcag1emsignrootcag2emsigneccrootcag3comodoaaaca
별칭 표기는 배포판마다 조금씩 달라서, 이름을 그대로 치지 않고 인증서 소유자(Owner)로 찾은 뒤 나온 별칭을 지웠습니다.
아래는 Java 17 기준이고, 명령은 keytool로 합니다. $JAVA_HOME은 조치할 JDK 경로로 바꿔 읽어 주세요.
1) 백업
cp -pn $JAVA_HOME/lib/security/cacerts $JAVA_HOME/lib/security/cacerts.bak-n은 백업 파일이 이미 있으면 덮어쓰지 않게 합니다. 두 번 실행해도 원본 백업이 지워지지 않습니다.
2) 지울 대상 찾기
K=$JAVA_HOME/bin/keytool
$K -J-Duser.language=en -list -v -cacerts -storepass changeit \
| grep -E "^(Alias name|Owner):" \
| grep -i -B1 -E "^Owner:.*(emsign|emudhra|AAA Certificate Services)"출력은 이런 모양입니다.
Alias name: comodoaaaca [jdk]
Owner: CN=AAA Certificate Services, O=Comodo CA Limited, L=Salford, ST=Greater Manchester, C=GB
--
Alias name: emsigneccrootcag3 [jdk]
Owner: CN=emSign ECC Root CA - G3, O=eMudhra Technologies Limited, OU=emSign PKI, C=IN
Alias name: emsignrootcag1 [jdk]
Owner: CN=emSign Root CA - G1, O=eMudhra Technologies Limited, OU=emSign PKI, C=IN
Alias name: emsignrootcag2 [jdk]
Owner: CN=emSign Root CA - G2, O=eMudhra Technologies Limited, OU=emSign PKI, C=INAlias name: 뒤의 문자열 전체([jdk] 포함)가 지울 별칭입니다.
한국어 로케일에서는 grep이 아무것도 못 찾습니다
서버 로케일이 한국어면 keytool 출력이 Alias name: 대신 별칭 이름:으로 나옵니다.
그러면 위 grep이 아무것도 걸러 내지 못해서, 삭제 대상이 없는 것처럼 보입니다.
-J-Duser.language=en은 keytool 출력만 영어로 고정해 이 문제를 막습니다.
3) 삭제
2)에서 나온 별칭마다 반복합니다.
$K -delete -cacerts -storepass changeit -alias "comodoaaaca [jdk]""별칭이 존재하지 않습니다"가 나오면 별칭을 잘못 옮겨 적은 것입니다.
cacerts 비밀번호를 기본값 changeit에서 바꾼 환경이면 -storepass 값도 바꿔야 합니다.
4) 확인
2)의 명령을 다시 실행해서 아무것도 나오지 않으면 삭제가 끝난 것입니다.
OpenJDK가 배포한 점검 도구 CheckCerts.java(JDK 8 이상)로 이 JDK가 아직 위험한지 한 번 더 확인할 수도 있습니다.2
5) 재시작
cacerts는 JVM이 시작할 때 읽기 때문에, 파일만 바꾸고 재시작하지 않으면 실행 중인 프로세스에는 반영되지 않습니다. Tomcat 등 이 JDK를 쓰는 Java 프로세스를 모두 재시작한 뒤, LDAP 로그인과 메일 발송처럼 밖으로 나가는 연동이 정상인지 확인합니다.
되돌리기
cp -p $JAVA_HOME/lib/security/cacerts.bak $JAVA_HOME/lib/security/cacerts복원하고 재시작하면 지운 인증서가 다시 들어옵니다. 취약한 상태로 돌아가는 것이니, 외부 연동이 깨졌을 때 원인이 이 조치인지 가릴 때만 씁니다.
Java 8에서 다른 점
| 항목 | Java 17 | Java 8 |
|---|---|---|
| cacerts 위치 | lib/security/cacerts | JDK는 jre/lib/security/cacerts, JRE는 lib/security/cacerts |
| keytool 옵션 | -cacerts | -keystore "경로" (-cacerts는 Java 9부터) |
Java 8은 JDK인지 JRE인지에 따라 cacerts 위치가 달라서, 경로를 먼저 찾아 변수에 담고 시작했습니다.
K=$JAVA_HOME/bin/keytool
C=$(ls $JAVA_HOME/jre/lib/security/cacerts $JAVA_HOME/lib/security/cacerts 2>/dev/null | head -1)
echo "$C" # 경로가 한 줄 나와야 한다
cp -pn "$C" "$C.bak"
$K -J-Duser.language=en -list -v -keystore "$C" -storepass changeit \
| grep -E "^(Alias name|Owner):" \
| grep -i -B1 -E "^Owner:.*(emsign|emudhra|AAA Certificate Services)"
$K -delete -keystore "$C" -storepass changeit -alias "별칭"6. 지운 뒤에 깨질 수 있는 것들
cacerts에서 루트를 지우는 건 신뢰를 줄이는 조치라서, 그 루트에 기대던 정상 연결도 같이 끊길 수 있습니다. 삭제하기 전에 알고 있어야 할 부작용은 다음과 같습니다.
삭제 조치의 부작용
- Sectigo(구 Comodo) 인증서를 쓰는 일부 외부 서버와 연결이 실패할 수 있습니다. 같은 계열의
usertrustrsaca,usertrustecc루트는 남아 있어 대부분은 유지될 것으로 보지만, 상대 서버가 체인을 어떻게 구성했는지에 따라 다릅니다. - eMudhra 인증서를 쓰는 서버(주로 인도 기관·기업)와는 연결할 수 없게 됩니다.
- JDK를 새로 깔면 cacerts도 새 파일로 바뀝니다. 수정판이 아닌 JDK로 교체하면 다시 조치해야 합니다.
- 조치한 JDK의 cacerts만 바뀝니다. 같은 서버의 다른 JDK, 별도 trustStore를 쓰는 프로그램, Java가 아닌 프로그램에는 적용되지 않습니다.
- 같은 달 Java 정기 보안 패치에 들어 있는 다른 취약점은 이 조치로 해결되지 않습니다. 근본 해결은 수정판 JDK입니다.
OS 패키지로 설치한 JDK를 쓰는 다른 시스템이 있다면 하나 더 주의할 점이 있습니다.
Debian·Ubuntu의 OpenJDK 패키지는 ca-certificates-java 문제로 패키지 업데이트만으로는 반영되지 않아서, /etc/ssl/certs/java/cacerts를 지우고 update-ca-certificates --fresh로 다시 만들어야 합니다.2
범위와 한계
이 글의 조치는 고객사에 설치된 Java 17과 Java 8, 두 계열에서 한 것입니다. Java 11·21·25에는 적용해 보지 않았습니다. Comodo AAA 루트를 지우는 이유는 OpenJDK가 설명하지 않았고, 그림 1의 교차 서명 경로는 제 추정입니다. 후속 이슈 CVE-2026-114820은 식별자와 "다시 조치하라"는 안내 외에 자세한 내용을 확인하지 못했습니다. Corretto 수정판이 문제를 어떤 방식(cacerts에서 루트 삭제, 또는 유출된 중간 인증서 차단 목록 추가)으로 막았는지도 릴리스 노트에 나와 있지 않아 확인하지 못했습니다.
마치며
- 이번 취약점은 JDK가 기본으로 믿는 인증서 목록에서 생겼습니다. 의존성 버전을 올리는 방식으로는 막을 수 없고, JDK나 cacerts를 직접 다뤄야 합니다.
- 영향 범위를 "Java가 밖으로 거는 TLS 연결"로 좁히고 나니 점검할 기능이 LDAP 로그인, 메일 발송, 외부 DB 연결 몇 개로 정리됐습니다.
- 패치 파일을 바로 반입할 수 있는 고객사는 수정판이 빨리 나온 Amazon Corretto로 배포사를 바꿔 근본 조치를 했고, 현장 대응이 필요한 고객사는 cacerts에서 루트 4개를 지워 경로를 끊었습니다.
- 삭제 조치는 임시 조치입니다. 정상 연결이 함께 끊길 수 있고 JDK를 새로 깔면 원래대로 돌아가므로, 이 고객사들도 패치 파일을 반입할 수 있게 되면 수정판 JDK로 바꿔야 합니다.
참고
- OpenJDK Vulnerability Advisory: 2026/10/21
- OpenJDK: Compromised Certificates and OpenJDK Java Runtimes
- Oracle Security Alert for CVE-2026-91403
- NVD: CVE-2026-91403
- Amazon Corretto 17 CHANGELOG
- Amazon Corretto 8 CHANGELOG
- openjdk/jdk8u: 8319187 Add three eMudhra emSign roots
각주
-
OpenJDK Vulnerability Advisory 2026/10/21, CVSS 3.1 점수 9.1과 계열별 영향 버전, 2026-10-30 확인: https://openjdk.org/groups/vulnerability/advisories/2026-10-21 ↩ ↩2
-
OpenJDK, Compromised Certificates and OpenJDK Java Runtimes(2026-10-24 갱신), 삭제 대상 별칭·대응 방법·CheckCerts.java, 2026-10-30 확인: https://openjdk.org/emudhra ↩ ↩2 ↩3 ↩4 ↩5
-
Oracle Security Alert Advisory - CVE-2026-91403(Rev 1, 2026-10-21), 영향 버전과 지원 정책 문구, 2026-10-30 확인: https://www.oracle.com/security-alerts/alert-cve-2026-91403.html ↩
-
openjdk/jdk8u 커밋 이력, 8319187 "Add three eMudhra emSign roots"(2023-12-13), 2026-10-26 확인: https://github.com/openjdk/jdk8u/commits/master/jdk/make/data/cacerts/emsignrootcag1 ↩
-
Amazon Corretto 17 CHANGELOG, 17.0.21.0.10(2026-10-20, CVE-2026-91403)과 17.0.21.0.11(2026-10-24, CVE-2026-114820) 항목, 2026-10-30 확인: https://github.com/corretto/corretto-17/blob/develop/CHANGELOG.md ↩
-
Amazon Corretto 8 CHANGELOG, 8.512.08.1(2026-10-20, CVE-2026-91403)과 8.512.09.1(2026-10-24, CVE-2026-114820) 항목, 2026-10-30 확인: https://github.com/corretto/corretto-8/blob/develop/CHANGELOG.md ↩
- cacerts↑
- JDK에 함께 배포되는 키스토어 파일로, TLS 연결에서 상대 서버 인증서를 검증할 때 신뢰의 출발점이 되는 루트 인증서들이 들어 있다. JDK 11 이상은 $JAVA_HOME/lib/security/cacerts, JDK 8은 $JAVA_HOME/jre/lib/security/cacerts에 있다. 별도 trustStore를 지정하지 않으면 이 파일이 쓰인다.
- 중간 인증서↑
- 인증기관은 루트 인증서의 개인키를 오프라인에 보관하고, 루트가 서명한 중간 인증서로 실제 서버 인증서를 발급한다. 클라이언트는 서버 인증서 → 중간 인증서 → 루트 인증서로 서명을 따라 올라가 루트가 신뢰 목록에 있으면 검증을 통과시킨다. 중간 인증서의 개인키가 유출되면 누구나 그 이름으로 서버 인증서를 만들 수 있다.
- 교차 서명↑
- 새 인증기관의 루트가 아직 여러 클라이언트의 신뢰 목록에 들어가지 못했을 때, 이미 널리 신뢰받는 다른 루트가 서명해 주면 그 경로로도 검증이 통과한다. 그래서 문제가 된 인증서를 끊으려면 원래 루트뿐 아니라 교차 서명한 루트까지 함께 봐야 할 수 있다.
- 중간자 공격(MITM)↑
- Man-in-the-Middle 공격. TLS에서는 클라이언트가 서버 인증서를 검증하기 때문에 보통 막히지만, 공격자가 클라이언트가 신뢰하는 체인으로 가짜 인증서를 만들 수 있으면 검증을 통과해 암호화된 통신을 그대로 중계하면서 열람·변조할 수 있다.
- keytool↑
- $JAVA_HOME/bin/keytool. 키스토어의 인증서 목록 조회(-list), 가져오기(-importcert), 삭제(-delete) 등을 한다. Java 9부터 -cacerts 옵션으로 기본 cacerts를 바로 지정할 수 있고, Java 8은 -keystore 옵션에 경로를 직접 적어야 한다.
- trustStore↑
- javax.net.ssl.trustStore 시스템 속성이나 애플리케이션 설정으로 지정한다. 지정하지 않으면 JDK의 cacerts가 쓰인다. 별도 trustStore를 쓰는 프로그램은 cacerts를 고쳐도 영향을 받지 않는다.