Skip to main content
어제까지 잘 열리던 사이트가 오늘 갑자기 “안전하지 않음” 으로 막히면 서버가 해킹된 것처럼 느껴지죠? 사실은 증표의 날짜가 지나서, 브라우저가 대화를 시작하기 전에 멈춘 장면일 때가 많아요.
TLS, SSL, 인증서는 뭐가 다를까요?에서는 브라우저가 HTTPS 통로를 만들기 전에 도메인 이름, 발급자, 유효 기간을 확인한다는 큰 그림을 봤어요. 그리고 TLS 인증서 체인과 신뢰 오류는 어떻게 읽어야 할까요?에서는 인증서 오류를 이름, 날짜, 체인, 로컬 trust store 네 갈래로 먼저 나누는 법을 봤죠. 이번 글은 그중에서도 운영에서 자주 만나는 인증서 만료 장애를 사례처럼 읽어볼게요. 장면은 이래요.
  • 브라우저에서는 사이트가 열리지 않고 인증서 경고가 떠요.
  • curl 에서는 certificate has expired 류의 오류가 보여요.
  • 앱 서버 로그에는 요청이 거의 안 남아요.
  • 그런데 모니터링은 한참 뒤에야 “사용자 접속 실패”로 시끄러워져요.
처음 보면 이상해요.
“사이트가 안 열리는데 왜 앱 로그에는 아무것도 없죠?”
그 이유는 단순할 수 있어요. HTTP 요청이 앱까지 도착하기 전에, TLS 인증서 검사에서 멈췄기 때문이에요.
여기서는 인증서 만료를 실제 장애를 좁히는 읽기 순서로 다룰게요. 특정 인증기관, 특정 CDN, 특정 웹서버의 갱신 방법을 매뉴얼처럼 다루지는 않아요. 제품별 명령과 화면은 다르지만, 만료 장애를 읽는 신호는 꽤 비슷해요.

먼저 한 장면으로 볼게요

월요일 아침에 운영 채널에 이런 메시지가 올라왔다고 해볼게요.
당장 curl -v 로 확인해보면 이런 식의 출력이 보일 수 있어요. 아래 값은 읽기 연습용 예시예요.
여기서 중요한 줄은 certificate has expired예요. 이건 브라우저나 curl 이 서버 인증서를 받기는 했지만, 그 인증서가 지금 시각 기준으로 더 이상 유효하지 않다고 본 장면이에요. 여기서 첫 번째 반전이 나와요. 인증서 만료 장애는 웹 앱 장애처럼 보이지만, 실제로는 HTTP가 시작되기 전 장애일 수 있어요.

요청은 어디에서 멈췄을까요?

인증서 만료를 볼 때는 먼저 요청 흐름 위에서 멈춘 위치를 잡아야 해요. 이 그림에서 핵심은 만료 오류가 HTTP 상태 코드가 아닐 수 있다는 점이에요. 브라우저는 GET / 요청을 보내기 전에, 먼저 TLS 과정에서 인증서를 확인해요. 그 인증서의 유효 기간이 이미 끝났다면 브라우저는 “이 상대와 보호된 통로를 열 수 없다”고 보고 멈출 수 있어요. 그러면 오리진 앱 입장에서는 요청을 받은 적이 없으니 access log가 조용해질 수 있죠.
다만 인증서 만료처럼 HTTP 전에 멈추는 장애에서는 앱 로그가 비어 있는 것이 오히려 중요한 신호일 수 있어요. “로그가 없다”를 곧장 “증상이 없다”로 읽지 말고, 요청이 어느 단계까지 갔는지 먼저 봐야 해요.

먼저 읽을 신호 다섯 가지

만료 장애에서 처음 볼 신호는 많지 않아요. 아래 다섯 가지면 시작점은 잡을 수 있어요. 이 다섯 가지를 순서대로 보면, 처음부터 인증서 재발급 명령을 치기보다 어디의 어떤 인증서가 만료됐는지를 먼저 좁힐 수 있어요. 이 흐름은 정답표가 아니라 초기 분기표예요. 만료처럼 보여도 실제로는 클라이언트 시간이 크게 틀렸거나, 중간 프록시에 예전 인증서가 남아 있을 수도 있거든요.

notBefore와 notAfter는 날짜표예요

인증서 안에는 유효 기간이 들어 있어요. X.509 인증서의 validity에는 대략 이 시각 전에는 아직 쓰면 안 됨을 뜻하는 notBefore와, 이 시각 뒤에는 더 이상 쓰면 안 됨을 뜻하는 notAfter가 있어요. RFC 5280에서도 인증서 유효 기간을 이 두 시각으로 표현해요. 예시로 보면 이런 느낌이에요.
여기서 조심할 점은 시간대예요. 인증서 출력은 보통 GMT나 UTC 기준으로 보이고, 장애 채널에서는 KST 같은 로컬 시간으로 말할 때가 많아요. 그러니까 Jun 23 00:00:00 GMT 만료라면 한국 시간으로는 2026-06-23 09:00:00 KST 근처에서 갑자기 터지는 것처럼 보일 수 있어요.
“아침 9시에 갑자기 터졌다”는 제보가 사실은 00:00 UTC 만료와 정확히 맞을 수 있어요. 인증서 날짜를 볼 때는 출력 시간대와 운영 채널의 시간대를 꼭 같이 봐야 해요.

실제 확인은 이렇게 나눠요

진단할 때는 한 도구만 믿기보다, 관점이 다른 도구를 짧게 나눠 보는 편이 좋아요.

1. 브라우저에서는 사용자가 보는 실패를 확인해요

브라우저 경고는 사용자 영향 확인에 좋아요. 다만 문구가 친절하게 바뀌거나, 보안 정책상 세부 원인을 숨겨 보일 수 있어요. 먼저 볼 것은 이 정도예요.
  • 경고가 만료를 말하는지
  • 접속한 주소가 정확히 무엇인지
  • 다른 브라우저나 다른 네트워크에서도 같은지
  • 오류가 apex 도메인, www, api 중 어디에서 나는지
브라우저가 “만료”라고 말해도, 거기서 바로 끝내지 말고 다음으로 CLI에서 같은 호스트를 확인해요.

2. curl -v 에서는 TLS 실패 지점을 더 좁혀요

예시 출력은 이런 식이에요.
여기서 읽을 포인트는 세 가지예요.
  1. subject나 SAN이 내가 접속한 이름과 맞는지
  2. expire date가 현재 시각보다 과거인지
  3. 오류가 expired인지, 아니면 unable to get local issuer certificate 같은 체인 오류인지
curl 출력 문구는 버전과 TLS 라이브러리에 따라 조금 다를 수 있어요. 그래도 날짜 오류인지 체인 오류인지를 나누는 데는 꽤 도움이 돼요.

3. openssl s_client 에서는 서버가 내미는 인증서를 봐요

여기서 -servername은 중요해요. 하나의 IP에 여러 HTTPS 사이트가 같이 있을 때, 서버는 SNI로 받은 이름을 보고 어떤 인증서를 내밀지 고를 수 있거든요. 이걸 빼면 다른 기본 인증서를 보고 엉뚱한 결론을 낼 수 있어요. 출력에서는 이런 부분을 봐요.
Verify return code: 10 같은 숫자는 OpenSSL 쪽 표현이에요. 숫자 자체보다 중요한 건, 이 도구도 만료를 말하고 있는지예요.

4. 프록시와 CDN에서는 “어느 인증서가 나갔는지”를 봐요

현실에서는 인증서가 한 곳에만 있지 않을 때가 많아요. 사용자가 보는 인증서는 보통 가장 앞에서 TLS를 종료하는 곳의 인증서예요. CDN이 TLS를 끝내면 사용자는 CDN 인증서를 보고, 로드 밸런서가 TLS를 끝내면 로드 밸런서 인증서를 봐요. 반대로 TLS 패스스루라면 오리진 인증서가 그대로 사용자에게 보일 수도 있어요. 그래서 만료 장애에서 꼭 물어야 해요.
“지금 사용자가 보고 있는 인증서는 어느 지점의 인증서일까요?”
이 질문을 빼먹으면 오리진 인증서를 갱신했는데도 CDN 엣지에는 예전 인증서가 남아 있거나, 반대로 CDN은 정상인데 오리진 재암호화 구간에서만 실패하는 장면을 놓칠 수 있어요.

만료처럼 보이지만 원인이 조금 다른 경우도 있어요

인증서 날짜 오류가 보인다고 해서 항상 “갱신을 안 했다” 한 줄로 끝나지는 않아요. 운영에서는 보통 아래 갈래를 같이 봐야 해요. 여기서 두 번째 반전이 있어요. 인증서 파일을 갱신한 것과, 실제로 서비스가 새 인증서를 내미는 것은 같은 일이 아니에요. 웹서버가 파일을 다시 읽지 않았을 수도 있고, 로드 밸런서 listener가 옛 인증서를 붙잡고 있을 수도 있고, CDN 배포가 아직 모든 엣지에 퍼지지 않았을 수도 있어요.

복구할 때는 순서를 조심해야 해요

만료 장애를 복구할 때의 큰 흐름은 보통 이래요.
1

인증서 만료를 확인해요

실제 서버가 내미는 인증서의 notAfter, hostname, chain을 확인해요. 만료와 이름 불일치, 신뢰 chain 오류를 먼저 구분해요.
2

영향 호스트와 TLS 종료 지점을 확정해요

어떤 hostname이 실패하는지와 CDN, load balancer, origin 중 어디서 TLS가 끝나는지 찾아요. 같은 서비스도 경로마다 다른 인증서를 내밀 수 있어요.
3

갱신된 인증서를 준비해요

자동 갱신 작업의 성공 여부를 확인하거나 새 인증서를 발급해요. leaf certificate뿐 아니라 필요한 intermediate chain과 private key 짝도 확인해요.
4

정확한 종료 지점에 배포해요

확인한 CDN, load balancer, proxy 설정에 새 인증서를 적용해요. 다른 계층에 배포하면 파일은 바뀌어도 사용자에게는 옛 인증서가 계속 보일 수 있어요.
5

서비스가 새 인증서를 제공하는지 확인해요

각 TLS 종료 지점에 직접 접속해 serial, 유효 기간, chain이 바뀌었는지 봐요. 설정 reload나 인스턴스별 배포 누락도 함께 확인해요.
6

사용자 경로에서 다시 확인해요

실제 DNS와 CDN을 거치는 외부 경로에서 hostname별 접속을 재검증해요. 지역이나 edge에 따라 옛 인증서가 남지 않았는지도 살펴봐요.
7

원인과 재발 방지를 기록해요

갱신 실패 원인, 감지 시점, 영향 범위와 복구 시간을 남겨요. 만료 전 알림과 자동 배포 검증을 추가해 같은 장애를 더 일찍 발견하도록 해요.
여기서 제일 많이 실수하는 지점은 정확한 위치에 배포와 서비스가 새 인증서를 내미는지 확인 사이예요. 파일을 바꿨거나 콘솔에서 갱신 버튼을 눌렀다고 해서, 실제 사용자 경로에서 새 인증서가 보인다는 보장은 없어요. 확인할 때는 적어도 이렇게 봐요.
그리고 가능하면 실패했던 같은 호스트 이름, 같은 경로, 같은 네트워크 위치에서 다시 확인해요. 오리진 직접 접속은 정상인데 사용자 경로의 CDN 인증서가 여전히 만료일 수 있고, 반대로 사용자 경로는 정상인데 내부 health check가 보는 오리진 인증서가 만료일 수도 있거든요.
급하다고 -k, --insecure, 인증서 검증 비활성화를 서비스 설정에 넣어버리면 장애는 숨겨져도 신뢰 검사가 사라져요. 진단용으로 잠깐 쓰는 것과 운영 경로에 남기는 것은 완전히 다른 일이에요.

잘못 읽기 쉬운 함정 여섯 가지

하나, 앱 로그가 조용하니 앱 문제가 아니라고만 생각하기.
TLS에서 멈추면 앱 로그가 조용할 수 있어요. 하지만 그 조용함은 요청이 앱까지 못 갔다는 신호로 읽어야 해요.
둘, 인증서를 갱신했으니 복구됐다고 생각하기.
새 인증서가 실제 listener, CDN, 프록시, 오리진 경로에서 나가고 있는지 확인해야 해요.
셋, apex와 www를 같은 인증서라고 가정하기.
example.com, www.example.com, api.example.com은 인증서와 배포 위치가 다를 수 있어요.
넷, SNI 없이 확인하고 엉뚱한 인증서를 보는 것.
가상호스트 환경에서는 openssl s_client에 -servername을 넣어야 실제 호스트용 인증서를 볼 가능성이 높아요.
다섯, UTC 만료 시각을 로컬 시간으로 읽는 것.
00:00 GMT 만료가 한국에서는 오전 9시 장애처럼 보일 수 있어요.
여섯, --insecure로 성공하면 문제가 사라졌다고 착각하기.
검증을 끄면 만료 인증서도 지나갈 수 있어요. 그건 원인을 고친 게 아니라 검사를 건너뛴 거예요.

재발 방지는 “갱신”보다 “관측”이 먼저예요

인증서 만료는 기술적으로는 갱신하면 끝나는 문제처럼 보이지만, 운영적으로는 미리 알 수 있었는데 놓친 문제일 때가 많아요. 재발 방지에서는 보통 아래를 확인해요. 여기서 중요한 건 인증서 저장소에 있는 만료일만 보는 게 아니라, 사용자가 실제로 만나는 경로에서 어떤 인증서가 보이는지를 보는 거예요. 특히 CDN, 로드 밸런서, 오리진이 함께 있는 구조에서는 내부 파일 하나의 상태보다 바깥에서 보이는 결과가 더 중요할 수 있어요.

자, 정리해볼까요?

  • 인증서 만료 장애는 웹 앱 장애처럼 보이지만, 실제로는 HTTP 요청 전에 TLS 검사에서 멈춘 장면일 수 있어요.
  • 먼저 브라우저 경고, curl -v, 인증서 notAfter, 영향 호스트, 앱/프록시 로그 유무를 같이 봐야 해요.
  • notAfter 출력은 보통 UTC/GMT 기준이라서, 운영 채널의 로컬 시간과 맞춰 읽어야 해요.
  • 인증서를 갱신한 것과 실제 사용자 경로에서 새 인증서가 나가는 것은 다를 수 있어요.
  • 재발 방지는 발급 자동화뿐 아니라 외부에서 보이는 인증서 만료일 관측까지 포함해야 해요.

이어서 보면 좋은 글