LabHub
배우기 러닝패스 코스

인증서는 갱신됐는데 브라우저는 옛것을 보여줬다 · 디스크는 새것, 서버는 옛것 · 이론

갱신 도구는 파일을 바꾸고 서버는 메모리를 내보낸다

LabHub 에서 이어서 보기

한 줄 요약

갱신 도구는 파일을 바꾸고, 서버는 메모리에 있는 인증서를 내보낸다. 둘 사이를 잇는 재기동(reload)이 없으면 디스크에는 새 인증서가 있는데 브라우저는 옛것을 본다. 그래서 갱신 감시는 파일이 아니라 서버가 실제로 내보내는 인증서를 봐야 한다.

왜 이게 필요했나

만료 사흘 전 알림이 왔다. 갱신 작업은 이미 어제 성공했다고 로그에 적혀 있다. 그런데 브라우저는 여전히 사흘 뒤 만료되는 인증서를 보여 준다. 담당자는 파일을 열어 본다 — 새 인증서가 맞다. 이 모순은 서버가 인증서를 언제 읽는지 모르면 풀리지 않는다. nginx·Envoy·JVM 앱·파이썬의 ssl 모듈까지, 대부분의 서버는 인증서를 기동 시점에 한 번 메모리로 올린다. 파일이 바뀌어도 다시 읽지 않는다. 갱신 도구가 파일을 덮어쓴 것과 서버가 그것을 쓰는 것은 별개의 사건이다.

이 저장소에서도 같은 종류의 물음이 있었다. cert-manager 가 인증서를 자동으로 갱신하도록 설정을 고쳤는데, 그 설정으로 갱신을 한 번도 겪지 않은 상태였다. "주기적으로 갱신되겠지" 에 답하려면 실제로 한 번 시켜 보고, 그 뒤 브라우저가 받는 인증서의 날짜가 바뀌는지 봐야 했다. 판정 근거는 파드 상태도 로그도 아니라 openssl s_client 가 받아 온 인증서의 날짜였다.

어떻게 동작하나

파이썬 [ssl](https://docs.python.org/3/library/ssl.html) 모듈로 보면 구조가 그대로 드러난다. SSLContext.load_cert_chain(certfile, keyfile) 은 호출 시점에 파일을 읽어 컨텍스트에 담고, 그 뒤의 모든 핸드셰이크는 컨텍스트가 든 것을 내보낸다. 파일을 덮어써도 컨텍스트는 모른다. 다시 읽게 하려면 프로세스를 재기동하거나, 서버가 제공하는 재적재 신호(nginx 의 reload, Envoy 의 SDS 처럼 파일 변화를 감시하는 구조)를 써야 한다. 쿠버네티스에서 cert-manager 가 Secret 을 갱신하면 볼륨으로 마운트된 파일은 얼마 뒤 바뀌지만, 그 파일을 읽는 것은 여전히 앱의 몫이다 — 재적재 로직이 없는 앱은 파드를 재시작해야 한다.

관찰 도구는 [openssl s_client](https://docs.openssl.org/3.0/man1/openssl-s_client/) 다. -connect host:port 로 핸드셰이크를 하고 -servername 으로 SNI 를 보낸 뒤, 받은 인증서를 x509 로 넘겨 serial·만료일·지문을 읽는다.

$ echo | openssl s_client -connect 127.0.0.1:8443 -servername www.lab.internal 2>/dev/null \    | openssl x509 -noout -serial -enddateserial=2037629D1C3A75C63253D1BE593E399BA192FF71      ← 서버가 내보내는 것$ openssl x509 -in /root/renew/live.crt -noout -serialserial=2037629D1C3A75C63253D1BE593E399BA192FF72      ← 디스크에 놓인 것

두 serial 이 다르면 사고다. 감시 스크립트는 이 두 값을 비교하면 되고, 만료 감시는 디스크 파일이 아니라 s_client 로 받은 인증서에 [-checkend](https://docs.openssl.org/3.0/man1/openssl-x509/) 를 걸어야 한다. 파일에 걸면 "갱신됐으니 안심" 이라는 거짓 초록불이 켜진다.

갱신 자체는 같은 키로 새 CSR 을 서명하는 일이다. serial 과 validity 만 바뀌므로 키 교체 없이도 인증서는 새 판이 된다(키까지 바꾸는 것이 더 안전하지만 이 모듈의 주제는 아니다). 유효기간을 짧게 두면 갱신이 자주 일어나 이런 사고가 일찍 드러난다 — 길게 두면 1년에 한 번, 담당자가 바뀐 뒤에 터진다.

현장에서 만나는 모습

증상은 늘 "갱신은 성공했다는데" 로 시작한다. 확인 순서는 셋이다. 첫째, 서버가 내보내는 인증서를 s_client 로 받아 serial 을 본다. 둘째, 디스크의 파일과 비교한다. 셋째, 다르면 재적재 방법을 찾는다 — 어떤 프로세스인지에 따라 reload 신호, 파드 재시작, 사이드카의 SDS 갱신이 답이다. 로드밸런서 뒤에 서버가 여럿이면 하나만 옛 인증서를 내보내는 경우도 있어서, -connect 에 각 서버의 주소를 직접 넣어 한 대씩 본다. 갱신 자동화를 처음 켰을 때는 만료를 기다리지 말고 한 번 강제로 갱신을 일으켜 서버가 새 인증서를 내보내는 것까지 확인한다. 자동화는 실제 발급 이력이 있어야 믿을 수 있다.

다음 실습에서 할 것

3일짜리 v1 인증서로 파이썬 HTTPS 서버를 띄우고 s_client 로 serial 을 읽는다. 같은 키로 30일짜리 v2 를 발급해 파일을 덮어쓴 뒤, 재기동하지 않은 서버가 여전히 v1 을 내보내는 것을 관찰한다. 디스크와 서버를 비교하는 certdiff.sh, 서버가 내보내는 인증서의 만료를 보는 servedcheck.sh 를 만들어 채점기의 서버로 양방향 검사를 받는다. 재기동 뒤 v2 가 나가는 것을 확인하고 보고서에 두 serial 을 적는다.