지난 두 글에서 TLS 핸드셰이크 흐름과 키 교환 원리를 풀었는데요, 한 가지가 계속 매달려 있었습니다. "그래서 서버가 보낸 인증서가 진짜라는 걸 브라우저는 도대체 어떻게 믿는 거지?" 키 교환이 아무리 안전해도, 처음 받은 인증서가 가짜라면 전부 도루묵이거든요.
저도 예전에 사내 API에 자체 서명 인증서를 붙여놓고 "왜 자꾸 SSL 오류가 나지?" 한참 헤맨 적이 있습니다. 결국 신뢰 체인이라는 개념을 모르고 있었던 거였어요. 오늘은 브라우저가 인증서를 받아들고 어떤 단계로 검증하는지, 그리고 한 번 발급된 인증서를 어떻게 "폐기"할 수 있는지까지 풀어보겠습니다.
개념 / 원리
신뢰 체인 (Chain of Trust)
브라우저가 신뢰하는 건 사실 서버의 인증서가 아니라 그 인증서를 발급한 CA, 그 CA를 발급한 상위 CA, 그렇게 거슬러 올라간 최상위 Root CA입니다.
체인은 보통 이런 3단 구조예요.
Root CA (브라우저/OS에 미리 내장)
└── Intermediate CA (CA 회사의 중간 인증서)
└── Leaf 인증서 (실제 도메인 인증서)검증 과정은 거꾸로 흘러갑니다.
- 서버가 Leaf + Intermediate를 보내옴.
- 브라우저는 Leaf의 "발급자(Issuer)" 필드를 보고 Intermediate를 찾는다.
- Intermediate의 공개키로 Leaf의 서명을 검증.
- Intermediate의 Issuer를 보고 Root CA를 OS/브라우저 저장소에서 찾는다.
- Root CA의 공개키로 Intermediate의 서명 검증.
- Root CA가 자기 저장소에 있는 신뢰된 Root와 일치하면 → 신뢰 완료.
Root CA가 OS에 미리 깔려 있다는 게 핵심입니다. 이 "Trust Store"에 없는 CA가 서명한 인증서는, 수학적으로 아무리 맞아도 "신뢰할 수 없음"으로 떨어집니다.
인증서 폐기 (Revocation)
인증서는 보통 만료일이 있지만, 그 안에라도 개인키가 유출되거나 잘못 발급됐으면 즉시 무효 처리가 필요합니다. 이걸 위해 두 가지 메커니즘이 있어요.
| 방식 | 동작 | 단점 |
|---|---|---|
| CRL (Certificate Revocation List) | CA가 폐기된 인증서 목록을 파일로 배포 | 파일이 크고, 갱신 주기가 느림 |
| OCSP (Online Certificate Status Protocol) | 인증서 하나 단위로 CA에 "이거 유효해?" 실시간 질의 | CA 서버가 죽으면 검증 지연/실패 |
| OCSP Stapling | 서버가 미리 OCSP 응답을 받아두고, 핸드셰이크 때 같이 첨부 | 별도 설정 필요 |
요즘은 OCSP의 부담을 줄이려고 OCSP Stapling을 거의 표준으로 씁니다. 일부 브라우저(특히 크롬)는 자체 폐기 목록(CRLSet)을 푸시로 받기도 해요.
실전 예제
1) 체인 전체를 눈으로 보기
openssl s_client의 -showcerts를 쓰면 서버가 보낸 인증서 체인이 다 나옵니다.
openssl s_client -connect www.google.com:443 -servername www.google.com -showcerts </dev/null 2>/dev/null \
| grep -E "^(s|i):"
출력 예시:
s:CN=*.google.com
i:C=US, O=Google Trust Services, CN=WR2
s:C=US, O=Google Trust Services, CN=WR2
i:C=US, O=Google Trust Services LLC, CN=GTS Root R1s: 가 Subject(이 인증서의 주인), i: 가 Issuer(이 인증서를 발급한 CA)입니다. 한 줄의 i: 와 다음 줄의 s: 가 일치하면서 체인이 이어지는 게 보이실 거예요.
2) 체인 검증을 명시적으로 하기
-verify_return_error 옵션을 붙이면 검증 결과를 명확히 띄워줍니다.
openssl s_client -connect www.google.com:443 -servername www.google.com \
-verify_return_error -CApath /etc/ssl/certs </dev/null 2>&1 \
| grep -E "Verify return code|verify error"
Verify return code: 0 (ok) 가 나오면 통과. 0이 아니면 코드별로 원인이 다릅니다 (19는 self-signed, 20은 issuer 못 찾음, 10은 만료).
3) OCSP 실시간 질의
인증서 하나를 직접 OCSP responder에 물어볼 수도 있습니다.
# 1. 서버 인증서와 체인을 파일로 저장
openssl s_client -connect www.google.com:443 -servername www.google.com -showcerts </dev/null 2>/dev/null \
> chain.pem
# 2. OCSP URL 추출
OCSP_URL=$(openssl x509 -in chain.pem -noout -ocsp_uri)
# 3. OCSP 질의
openssl ocsp -issuer chain.pem -cert chain.pem -url "$OCSP_URL" -no_nonce \
-header "Host=$(echo $OCSP_URL | awk -F/ '{print $3}')"
응답에 Cert Status: good 이 나오면 유효, revoked 면 폐기된 인증서입니다.
4) OCSP Stapling 동작 확인
서버가 OCSP Stapling을 켜뒀다면 핸드셰이크 응답에 OCSP 결과가 같이 따라옵니다.
openssl s_client -connect www.google.com:443 -servername www.google.com -status </dev/null 2>/dev/null \
| grep -A 5 "OCSP Response"
OCSP Response Status: successful 과 Cert Status: good 가 같이 보이면 Stapling이 잘 동작하는 거예요. 운영 서버라면 이 한 줄을 헬스체크에 넣어두면 좋습니다.
5) nginx에서 OCSP Stapling 켜기
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/ssl/chain.pem;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
resolver 설정을 빼먹으면 nginx가 OCSP responder의 도메인을 못 풀어서 Stapling이 조용히 꺼집니다. 자주 까먹는 부분이에요.
자주 하는 실수
- Intermediate 인증서를 서버에 안 올림: Leaf만 올리고 끝내면 브라우저가 Root까지 못 따라갑니다. 데스크톱 크롬은 캐시 덕분에 통과해도, 처음 들어온 모바일 브라우저는 실패해요.
ssl_certificate에는 항상fullchain.pem(leaf + intermediate)을 쓰세요. - Root CA를 fullchain에 같이 넣음: Root는 클라이언트가 이미 가지고 있으니 굳이 보낼 필요가 없고, 핸드셰이크 패킷만 무겁게 만듭니다. fullchain에는 leaf와 intermediate까지만.
- OCSP responder 막힌 줄 모르고 방치: 사내망에서 외부로 OCSP 통신이 막혀 있으면 클라이언트마다 동작이 갈립니다. "OCSP Soft-fail" 정책이라 일부 브라우저는 그냥 통과시키는데, 그게 더 위험해요. Stapling으로 서버가 대신 처리하게 하는 게 안전합니다.
- 사내 자체 CA를 OS Trust Store에 안 넣음: 사내 API에 자체 CA로 발급한 인증서를 붙였다면, 그 CA의 Root를 모든 클라이언트 OS의 신뢰 저장소에 배포해야 합니다. 안 그러면 영원히 "신뢰할 수 없음"이에요.
- 인증서 만료일만 보고 안심: 만료 전이라도 폐기될 수 있습니다. 모니터링은 "만료까지 N일"과 "OCSP 상태 = good"을 둘 다 봐야 완전해요.
저는 예전에 OCSP Stapling을 켰는데 resolver를 빠뜨려서 한참 동안 Stapling이 안 되고 있었던 적이 있습니다. 외부에서 보면 인증서는 멀쩡한데, OCSP 라운드트립이 매 요청마다 일어나서 응답 속도가 미묘하게 나빴어요. 모니터링 안 했으면 아예 몰랐을 거예요.
마무리
정리하면 세 줄입니다.
- 브라우저는 Leaf → Intermediate → Root 순으로 서명을 검증하고, Root가 OS Trust Store에 있어야 신뢰가 완성된다.
- 폐기는 CRL(파일 목록), OCSP(실시간 질의), OCSP Stapling(서버가 미리 받아 첨부) 세 가지로 처리한다.
- 운영에서는 fullchain 구성 + OCSP Stapling 활성화 + 만료/폐기 모니터링, 이 셋만 챙기면 인증서 사고는 거의 안 난다.
'보안' 카테고리의 다른 글
| [보안] HSTS와 보안 헤더 - HTTPS를 정말 강제하는 방법 (0) | 2026.05.22 |
|---|---|
| [보안] Let's Encrypt와 ACME 프로토콜 - 인증서 자동 발급과 갱신의 동작 원리 (0) | 2026.05.21 |
| [보안] TLS 키 교환의 비밀 - 대칭키와 비대칭키는 어떻게 협력할까 (0) | 2026.05.19 |
| [보안] HTTPS와 TLS 핸드셰이크 - 브라우저 자물쇠 안에서 벌어지는 일 (0) | 2026.05.18 |
| [보안] JWT 보안 - 흔히 하는 실수 5가지 (0) | 2026.05.14 |