"인증기관이 인증서를 폐기했다"는 공지를 받고 사이트를 열어 봤더니 크롬에서는 아무 경고 없이 열리는 경우가 있어요. 결론부터 말하면 인증서 폐기는 인증기관(CA)의 목록에만 기록되고, 그 목록을 확인할지·어떻게 확인할지는 접속하는 클라이언트가 정해요. 그래서 "브라우저에서 잘 열린다"는 폐기되지 않았다는 증거가 되지 못하고, 운영자는 CRL이나 OCSP로 직접 대조해 봐야 해요.
2026년 10월 eMudhra(emSign) 인증서 대량 폐기 때 이 차이가 그대로 드러났어요. Nemo의 조사 글에 따르면 폐기된 인증서를 계속 내건 인도 정부 사이트를 Firefox는 막았고, Chrome·Edge·Android는 열었어요. 이 글은 사건 요약이 아니라 그 바탕의 폐기 확인 구조와 openssl 점검법을 다뤄요. 명령과 숫자는 2026년 10월 25일 실제 사이트에 돌려 확인했어요.
인증서 폐기란 — 파일이 아니라 목록이 바뀌는 일
개인키가 유출됐거나, 인증서가 규정(CP/CPS)에 맞지 않게 발급됐다면 만료 전이라도 CA가 인증서를 폐기(revoke) 해요.
폐기는 CA가 그 인증서의 일련번호를 "더 이상 믿지 말 것" 목록에 올리는 일이에요. RFC 5280의 CRL 형식을 보면 목록 한 항목은 일련번호, 폐기 시각, 선택 항목인 사유 코드로 이뤄져요. 서버에 깔린 인증서 파일(PEM)은 한 글자도 바뀌지 않아요. PEM 본문은 DER 바이너리를 Base64로 감싼 것일 뿐이라 Base64 디코더로 풀어 봐도 "폐기됨" 표시는 어디에도 없어요. PEM이 어떤 구조인지는 Base64 용도와 PEM 형식 글에 정리해 뒀어요.
여기서 중요한 사실이 두 가지 나와요.
- 서버는 새 인증서를 배포하기 전까지 같은 인증서를 계속 보내요. eMudhra 사례에서 조사자가 10월 24일 다시 셌을 때 인도 정부 도메인 1,377개 중 811개가 여전히 폐기된 인증서를 쓰고 있었어요.
- 막을지는 클라이언트가 폐기 정보를 어디서 얻느냐에 달려 있어요. 같은 인증서라도 어떤 브라우저는 막고 어떤 브라우저는 열어요.
CRL과 OCSP 차이 — 목록을 받을까, 매번 물을까
인증서 안에는 폐기 정보를 어디서 얻으면 되는지 주소가 적혀 있어요. 두 가지 확장 필드예요.
CRL Distribution Points: CRL 파일 주소Authority Information Access의OCSP항목: OCSP 응답기 주소
| 항목 | CRL | OCSP |
|---|---|---|
| 방식 | CA가 서명한 폐기 일련번호 목록 파일을 받아 대조 | 접속할 때 응답기에 일련번호 하나의 상태를 질의 |
| 표준 | RFC 5280 | RFC 6960 |
| 응답 | 목록 전체(수백 바이트~수십 MB) | good / revoked / unknown 한 건 |
| 사생활 | CA가 누가 어느 사이트에 가는지 모름 | 응답기가 접속하려는 사이트를 알 수 있음 |
| 실패 시 | 캐시된 목록으로 판단 가능 | 대부분 응답이 없으면 그냥 통과(soft-fail) |
| 최근 추세 | 공개 CA에 의무 | 선택 사항이 됨, 끄는 CA가 늘어남 |
업계 규칙도 CRL 쪽으로 옮겨 갔어요. CA/브라우저 포럼은 2023년 7월 Ballot SC-063을 통과시켜 공개 TLS 인증서에서 OCSP를 선택 사항으로 바꾸고 CRL을 의무로 만들었어요. Let's Encrypt는 OCSP 종료 공지대로 2025년 5월 7일부터 인증서에서 OCSP 주소를 뺐고, 같은 해 8월 6일 응답기를 껐어요. 이유로는 OCSP 응답기가 방문자 IP와 방문하려는 사이트를 함께 볼 수 있다는 사생활 위험을 첫째로 들었어요.
실제 인증서에서 이 변화가 보여요. 2026년 10월 25일 기준으로 몇 곳의 리프 인증서를 열어 봤어요.
| 사이트 | 발급자 | CRL 주소 | OCSP 주소 |
|---|---|---|---|
| smithlab.co.kr | Google Trust Services WE1 | 있음 | 없음 |
| www.google.com | Google Trust Services WE2 | 있음 | 없음 |
| revoked.badssl.com | Let's Encrypt YE2 | 있음 | 없음 |
| github.com | Sectigo DV E36 | 없음 | 있음 |
OCSP 스테이플링(서버가 OCSP 응답을 대신 받아 붙여 주는 기능)도 OCSP 주소가 있는 인증서에서만 의미가 있어요. 점검 도구를 만들 때 OCSP만 보면 앞의 세 곳은 확인 자체가 안 되고, CRL만 보면 마지막 곳이 빠져요. 두 경로를 모두 지원해야 하는 이유예요.
실제 CRL 열어 보기 — 크기와 폐기 사유 실측
CRL이 얼마나 큰지는 CA마다, 파티션마다 크게 달라요. 요즘 CA는 CRL을 여러 조각(파티션)으로 나누고 인증서마다 자기 조각 주소만 적어요. 위 표의 두 인증서에 적힌 CRL을 받아 봤어요.
| CRL | 크기 | 항목 수 | 발행 시각(lastUpdate) | 다음 발행 예정(nextUpdate) |
|---|---|---|---|---|
| Google WE1 조각(smithlab.co.kr) | 310바이트 | 0개 | 2026-10-25 06:11 UTC | 2026-11-04 05:11 UTC |
| Let's Encrypt YE2 74번 조각(revoked.badssl.com) | 61,196바이트 | 1,565개 | 2026-10-25 08:33 UTC | 2026-11-03 08:33 UTC |
revoked.badssl.com은 일부러 폐기된 인증서를 걸어 둔 시험용 사이트예요. 그 인증서 일련번호 050A72B7…9549는 YE2 74번 CRL에 2026년 9월 15일 폐기, 사유 "Key Compromise"로 올라 있었어요. 같은 CRL의 1,565개 항목을 사유별로 세면 다음과 같아요.
대부분은 사유 코드 없이 폐기돼요. 사유가 없어도 폐기 효력은 같아요.
표로 보기
| 구분 | 건수 |
|---|---|
| 사유 코드 없음 | 1,354 |
| 운영 중단 | 106 |
| 키 유출 | 85 |
| 대체됨 | 20 |
사유 코드는 선택 항목이라 대부분 비어 있고, "Superseded(대체됨)"처럼 보안 사고가 아닌 사유도 흔해요. eMudhra G1 인증서 9,356개도 사유가 "Superseded"였어요. 사유가 가벼워 보여도 폐기된 인증서는 엄격한 클라이언트에서 접속이 끊겨요. 사유를 보고 교체를 미루면 안 돼요.
브라우저별 폐기 확인 — CRLite와 CRLSet이 갈린 이유
브라우저는 접속할 때마다 CRL을 받거나 OCSP에 묻지 않아요. 느리고, OCSP는 사생활 문제도 있기 때문이에요. 대신 브라우저 회사가 폐기 정보를 미리 모아 압축한 목록을 브라우저에 내려보내요.
Firefox CRLite — 모든 폐기를 담는 압축 목록
Mozilla Hacks 블로그의 CRLite 소개에 따르면 Firefox는 137판부터 데스크톱(Windows·macOS·Linux)에서 CRLite를 써요. 인증서 투명성(CT) 로그로 공개 인증서 전체를 파악하고, 약 3,000개 CRL에 흩어진 폐기 정보를 필터 구조(Ribbon 필터를 두 단계로 겹친 형태)로 압축해 12시간마다 갱신해요. 142판부터는 DV 인증서에 대한 OCSP 조회를 껐어요.
Chrome CRLSet — 긴급 차단용 선별 목록
Chromium의 CRLSet 문서는 CRLSet을 "긴급 상황에서 인증서를 빠르게 막는 주된 수단"이라고 설명해요. CCADB에 공개된 CRL을 수집해 그중 일부만 담고, 어떤 기준으로 고르는지는 공개하지 않아요. 또 Chrome은 기본적으로 온라인 OCSP·CRL 확인을 하지 않는다고 적어요. 기업 환경에서는 정책(EnableOnlineRevocationChecks)으로 온라인 확인을 켤 수 있어요. 현재 쓰는 CRLSet 버전은 chrome://components에서 볼 수 있어요.
eMudhra 사례의 결과가 이 구조 그대로예요. 조사 글에 따르면 10월 24일 CRLSet 10856판에는 문제의 인증서가 하나도 없었고, Firefox는 CRLite로 막았어요. iOS Safari는 EV 인증서만 OCSP로 확인해서 대부분 열렸고, OCSP가 "revoked"를 돌려준 EV 사이트 한 곳만 막혔어요. CRL을 직접 받아 대조하는 SSL Labs는 모두 F 등급으로 판정했어요.
| 확인 경로 | 쓰는 곳 | 폐기를 담는 범위 | eMudhra 사례 결과 |
|---|---|---|---|
| CRLite | Firefox 데스크톱 | 공개 인증서의 모든 폐기 | 차단 |
| CRLSet | Chrome·Edge·Android | 선별된 일부 | 열림 |
| OCSP(EV만) | iOS Safari | 질의한 인증서 한 건 | 대부분 열림, EV 1곳 차단 |
| CRL 직접 대조 | SSL Labs·직접 점검 | 인증서에 적힌 CRL 전부 | F 등급 |
속도도 미리 받은 목록이 유리해요. Mozilla 측정값은 아래와 같아요.
서버 간 통신은 더 허술해요 — 기본 TLS 클라이언트의 폐기 확인
백엔드 HTTP 클라이언트는 대부분 폐기를 아예 보지 않아요.
- curl: 서버가 스테이플한 OCSP 응답을 확인하는
--cert-status옵션이 있지만 기본값은 꺼져 있어요. CRL을 받으러 가지도 않아요. - 파이썬
ssl·requests: 기본SSLContext는 CRL을 확인하지 않아요.VERIFY_CRL_CHECK_LEAF플래그를 켜고 CRL 파일을 직접 넣어 줘야 해요. - Go
crypto/tls: 폐기는 확인하지 않아요. 필요하면VerifyPeerCertificate콜백에서 구현해요. - Java(JSSE):
com.sun.net.ssl.checkRevocation시스템 속성 기본값이 false예요.
그래서 상대 서버가 폐기된 인증서를 내걸어도 API 호출은 그대로 성공해요. 반면 Firefox 사용자는 같은 서버에서 접속이 끊겨요. 일부 사용자만 막히는 장애를 피하려면 내 서버 인증서는 내가 직접 점검해야 해요.
openssl로 인증서 폐기 확인하는 법
아래 명령은 macOS 기본 openssl(LibreSSL 3.3.6)과 OpenSSL 3 양쪽에서 동작하는 옵션만 썼어요.
1단계: 서버가 보내는 체인 받기
HOST=example.com
openssl s_client -connect "$HOST:443" -servername "$HOST" -showcerts </dev/null 2>/dev/null \
| awk '/BEGIN CERTIFICATE/{n++} n{print > ("cert" n ".pem")}'
# cert1.pem = 리프(내 도메인), cert2.pem = 발급자(중간 CA)
openssl x509 -in cert1.pem -noout -serial -issuer -enddate
2단계: CRL로 대조
CRL_URL=$(openssl x509 -in cert1.pem -noout -text | grep -A4 'CRL Distribution' | grep -o 'http[^ ]*' | head -1)
curl -s "$CRL_URL" -o leaf.crl
openssl crl -inform DER -in leaf.crl -noout -lastupdate -nextupdate
SERIAL=$(openssl x509 -in cert1.pem -noout -serial | cut -d= -f2)
openssl crl -inform DER -in leaf.crl -noout -text | grep -A4 "Serial Number: $SERIAL"
마지막 줄에 아무것도 없으면 폐기되지 않은 거고, 있으면 폐기 시각과 사유가 찍혀요. nextUpdate가 이미 지난 CRL은 낡은 목록이니 그 결과를 믿으면 안 돼요.
3단계: OCSP로 질의 (주소가 있을 때)
OCSP_URL=$(openssl x509 -in cert1.pem -noout -ocsp_uri)
openssl ocsp -issuer cert2.pem -cert cert1.pem -url "$OCSP_URL"
# cert1.pem: good / revoked / unknown
github.com에 돌렸을 때 good과 함께 This Update·Next Update 시각이 나왔고, 왕복은 약 0.18초였어요. -noverify를 붙이면 위조 응답을 걸러 내지 못하니 점검 스크립트에는 넣지 않아요.
인증서 폐기 모니터링 스크립트 — 만료 감시와 따로 두기
"만료 30일 전 경보"로는 하루아침에 일어나는 폐기를 잡지 못해요. 위 단계를 묶은 스크립트예요. 폐기면 종료 코드 2, 판단 불가면 1을 돌려줘서 어떤 모니터링 도구에도 붙일 수 있어요.
#!/bin/sh
# 사용법: ./check-revocation.sh example.com
HOST="$1"; DIR=$(mktemp -d)
openssl s_client -connect "$HOST:443" -servername "$HOST" -showcerts </dev/null 2>/dev/null \
| awk -v d="$DIR" '/BEGIN CERTIFICATE/{n++} n{print > (d "/c" n ".pem")}'
LEAF="$DIR/c1.pem"; ISSUER="$DIR/c2.pem"
SERIAL=$(openssl x509 -in "$LEAF" -noout -serial | cut -d= -f2)
CRL_URL=$(openssl x509 -in "$LEAF" -noout -text | grep -A4 'CRL Distribution' | grep -o 'http[^ ]*' | head -1)
OCSP_URL=$(openssl x509 -in "$LEAF" -noout -ocsp_uri)
if [ -n "$CRL_URL" ]; then
curl -s "$CRL_URL" -o "$DIR/x.crl"
if openssl crl -inform DER -in "$DIR/x.crl" -noout -text | grep -Eq "Serial Number: ${SERIAL}$"; then
echo "REVOKED(CRL) $HOST $SERIAL"; exit 2
fi
echo "ok(CRL) $HOST"
elif [ -n "$OCSP_URL" ]; then
R=$(openssl ocsp -issuer "$ISSUER" -cert "$LEAF" -url "$OCSP_URL" 2>/dev/null | grep "$LEAF:")
case "$R" in *revoked*) echo "REVOKED(OCSP) $HOST"; exit 2;; *good*) echo "ok(OCSP) $HOST";; *) echo "UNKNOWN $HOST"; exit 1;; esac
else
echo "NO-REVOCATION-INFO $HOST"; exit 1
fi
세 사이트에 돌린 결과는 이랬어요.
| 대상 | 출력 | 종료 코드 |
|---|---|---|
| revoked.badssl.com | REVOKED(CRL) revoked.badssl.com 050A72B7…9549 | 2 |
| smithlab.co.kr | ok(CRL) smithlab.co.kr | 0 |
| github.com | ok(OCSP) github.com | 0 |
운영에 붙일 때 챙길 점이에요.
- 실행 주기: 하루 한 번 이상. SC-063은 CA가 인증서를 폐기로 기록한 뒤 24시간 안에 새 CRL을 내도록 정했어요. 하루 한 번 이상 돌리면 대부분 하루 안에 잡혀요. 크론으로 돌린다면 크론 표현식 읽는 법을 참고해
0 */6 * * *(6시간마다)처럼 잡으면 돼요. - 모든 진입점: 로드밸런서·CDN·사설 프록시가 각자 인증서를 들고 있을 수 있어요. 도메인 하나라도 진입 IP마다 따로 돌려요. eMudhra 사례처럼 교체 공지 뒤에도 일부 서버만 옛 인증서를 내는 일이 흔해요.
- 한계: 이 스크립트는 CRL 서명을 검증하지 않고 리프 인증서만 봐요. 중간 CA가 폐기된 경우까지 보려면 cert2에 대해서도 같은 검사를 반복하거나 SSL Labs 같은 외부 검사를 함께 써요.
자주 묻는 질문
인증서가 폐기됐는지 가장 빨리 확인하는 방법은 무엇인가요?
브라우저 없이 확인하려면 위의 openssl 2단계(CRL 대조)가 가장 확실해요. 인증서에 OCSP 주소만 있다면 3단계로 물어요. 명령이 번거로우면 SSL Labs 서버 검사가 CRL과 OCSP를 모두 확인해 결과에 "Revoked"로 보여 줘요.
CRL과 OCSP 중 무엇을 확인해야 하나요?
인증서에 적힌 쪽이에요. 2025년 이후 Let's Encrypt·Google Trust Services 인증서는 CRL 주소만 있는 경우가 많고, 일부 CA는 아직 OCSP 주소만 넣어요. 점검 도구는 둘 다 지원하게 만드는 편이 안전해요.
폐기된 인증서인데 크롬에서 경고가 안 뜨는 이유는 무엇인가요?
Chrome은 접속 때마다 폐기를 묻지 않고, 구글이 고른 일부 폐기만 담은 CRLSet으로 판단해요. 그 목록에 없는 인증서는 폐기됐어도 열려요. Firefox 데스크톱은 CRLite로 공개 인증서의 폐기 전체를 확인해서 같은 인증서를 막을 수 있어요.
폐기 사유가 "Superseded"면 교체를 미뤄도 되나요?
안 돼요. 사유 코드는 참고 정보일 뿐이고 폐기 효력은 같아요. 엄격하게 확인하는 클라이언트에서는 바로 접속이 끊기니, 사유와 상관없이 새 인증서를 받아 배포해요.
사설 CA로 만든 사내 인증서도 폐기 확인을 해야 하나요?
사설 CA는 CRLite·CRLSet 대상이 아니라서 브라우저도 거의 확인하지 않아요. CRL을 직접 발행하고 서비스 TLS 설정에서 읽게 하거나, 인증서 수명을 짧게 잡아 폐기 의존도를 줄여요.
정리
인증서 폐기는 CA의 목록이 바뀌는 일이고, 차단은 그 목록을 보는 클라이언트가 결정해요. Firefox CRLite는 폐기를 전부 담고, Chrome CRLSet은 일부만 담고, 백엔드 HTTP 클라이언트는 대개 아예 보지 않아요. 그래서 운영자는 브라우저 결과를 믿지 말고 CRL·OCSP로 직접 대조하고, 만료 감시와 따로 폐기 감시를 걸어 둬야 해요.
인증서 지문(SHA-256 fingerprint)의 원리는 MD5·SHA-256 차이 글에 있어요. 위 스크립트를 크론에 걸어 두면 다음 대량 폐기 때 공지보다 먼저 알 수 있어요.