NET::ERR_CERT_DATE_INVALID 원인과 해결
이 글의 순서 11개 절
NET::ERR_CERT_DATE_INVALID는 Chrome이 인증서의 유효기간 확인에서 실패했다는 뜻이다.
인증서에는 시작(notBefore)과 끝(notAfter)이 적혀 있고, 검증하는 쪽의 “지금”이 그 사이에
없으면 이 에러가 난다.
틀린 건 둘 중 하나다. 인증서에 적힌 날짜거나, “지금”을 재는 시계거나. 앞쪽은 서버 운영자가 고치고 뒤쪽은 사용자 기기에서 고친다. 서버 인증서는 멀쩡한데 체인에 섞인 중간 인증서 하나가 만료돼 같은 에러가 나기도 한다.
경고 화면
Chromium 내부 코드는 -201이다. 정의 옆 주석은 원인 후보로 셋을 꼽는다. 공격자가 개인키를 손에
넣은 옛 인증서를 내미는 경우, 서버 설정이 잘못돼 유효한 인증서를 내보내지 않는 경우, 브라우저
시계가 틀린 경우다. 현장에서 마주치는 건 대부분 뒤의 둘이다.
경고 화면의 상세 문구(영문 UI)에 판단 재료가 같이 나온다. 만료면 이렇다.
This server could not prove that it is www.example.com; its security certificate
expired 3 days ago. This may be caused by a misconfiguration or an attacker
intercepting your connection. Your computer's clock is currently set to
Sunday, October 4, 2026. Does that look right? If not, you should correct your
system's clock and then refresh this page.
아직 유효하지 않으면 its security certificate is supposedly from 2 days in the future처럼 나온다.
화면에 그 기기의 현재 날짜도 찍힌다. 사용자에게 스크린숏을 받으면 이 날짜부터 본다.
Chrome은 시계부터 의심한다
Chrome은 날짜 에러가 나면 일반 경고를 띄우기 전에 기기 시계를 따진다. 시계가 틀렸다고 판단하면
Your clock is ahead 또는 Your clock is behind 제목의 시계 오류 화면을 대신 띄운다. 소스의 판단
기준은 이렇다. 네트워크 시각을 받아 둔 상태라면 시스템 시계가 네트워크 시각과 (불확실성 + 5분) 넘게
다를 때다. 네트워크 시각이 없으면 시스템 시계가 Chrome 빌드 시각보다 2일 넘게 이르거나 365일 넘게
늦을 때다.
시계 오류 화면이 떴으면 아래 기기 시계 절로 바로 간다. 일반 경고 화면이면 서버 쪽부터 본다.
서버인가, 이 기기인가
다른 기기, 다른 망(휴대폰 데이터 등)에서 같은 주소를 열어 본다. 전부 실패하면 서버 쪽이다. 한 기기에서만 실패하면 그 기기의 시계나 그 망의 검사 장비다. 서버는 바깥에서 직접 본다. 아래 openssl 명령이나 certdesk 진단으로 서버가 지금 내주는 인증서의 만료일을 확인한다.
“일부 사용자만”이면 둘 다 가능하다. 실패하는 사용자의 공통점이 기기(OS, 버전)면 기기 시계나 만료된 중간 인증서를, 지역이나 통신사면 일부 노드만 갱신된 경우를 먼저 본다.
| 증상 | 원인 | 확인 | 조치 |
|---|---|---|---|
| 모든 사용자·모든 기기에서 실패 | 서버 인증서 만료 | s_client의 0번 NotAfter |
갱신 → 설치 → 리로드 |
| 갱신했는데 그대로 | 리로드 누락, 복사본 파일, CDN·LB의 옛 인증서 | 디스크 파일과 서버 응답의 시리얼 비교 | 리로드, 설정 경로 정리, CDN·LB 인증서 교체 |
| 일부 사용자·지역만, 새로고침하면 될 때도 있음 | 일부 노드만 갱신 | A/AAAA 주소마다 따로 접속 | 모든 노드에 배포 |
| 특정 PC·폰에서만 | 기기 시계 | date -u와 HTTP Date 헤더 비교 |
자동 시간 동기화 |
| 오래된 기기·라이브러리에서만 | 만료된 중간 인증서·옛 교차 서명 | 체인 전체의 NotAfter, 에러의 depth |
체인 파일에서 만료 인증서 제거 |
| 갱신 직후 일부 기기에서만 | 아직 유효하지 않음 | NotBefore와 기기 시계 |
기기 시계, 사설 CA 발급 서버 시계 |
| 특정 회사망에서만, 발급자가 낯섦 | TLS 검사 장비 | 망 안팎에서 발급자 비교 | 장비 인증서 갱신 |
체인의 날짜를 전부 읽는다
서버 인증서 한 장만 보면 만료된 중간 인증서를 놓친다. 서버가 보낸 인증서의 날짜를 모두 읽는다.
openssl s_client -connect www.example.com:443 -servername www.example.com -showcerts </dev/null
expired.badssl.com의 결과(OpenSSL 4.0.3)다. PEM 블록과 이름 앞부분은 ...으로 줄였다.
depth=2 ... CN=COMODO RSA Certification Authority
verify return:1
depth=1 ... CN=COMODO RSA Domain Validation Secure Server CA
verify return:1
depth=0 OU=Domain Control Validated, OU=PositiveSSL Wildcard, CN=*.badssl.com
verify error:num=10:certificate has expired
notAfter=Apr 12 23:59:59 2015 GMT
...
Certificate chain
0 s:OU=Domain Control Validated, OU=PositiveSSL Wildcard, CN=*.badssl.com
i:... CN=COMODO RSA Domain Validation Secure Server CA
v:NotBefore: Apr 9 00:00:00 2015 GMT; NotAfter: Apr 12 23:59:59 2015 GMT
1 s:... CN=COMODO RSA Domain Validation Secure Server CA
i:... CN=COMODO RSA Certification Authority
v:NotBefore: Feb 12 00:00:00 2014 GMT; NotAfter: Feb 11 23:59:59 2029 GMT
2 s:... CN=COMODO RSA Certification Authority
i:C=SE, O=AddTrust AB, OU=AddTrust External TTP Network, CN=AddTrust External CA Root
v:NotBefore: May 30 10:48:38 2000 GMT; NotAfter: May 30 10:48:38 2020 GMT
...
Verify return code: 10 (certificate has expired)
Certificate chain 아래가 서버가 보낸 목록이다. 보낸 순서 그대로이고, v: 줄에 인증서마다
NotBefore와 NotAfter가 있다. 이 줄은 OpenSSL 3.0부터 나온다.
위쪽 depth= 줄은 OpenSSL이 만든 검증 경로라서 서버가 보낸 목록과 다를 수 있다. verify error
바로 위의 depth가 문제 인증서다. 0은 서버 인증서, 1 이상은 중간이나 루트다.
이 예에서는 depth 0만 실패했다. 목록의 2번도 2020-05-30에 만료된 인증서인데 depth 2에는 에러가 없다. OpenSSL이 서버가 보낸 2번 대신 신뢰저장소에 있는 같은 이름의 자체 서명 루트로 경로를 만들었기 때문이다.
OpenSSL 1.1.1 이하나 macOS 기본 /usr/bin/openssl(LibreSSL)에는 v: 줄이 없다. 이때는 인증서를
하나씩 잘라 날짜를 찍는다.
echo | openssl s_client -connect www.example.com:443 -servername www.example.com -showcerts 2>/dev/null \
| awk '/-----BEGIN CERTIFICATE-----/{c=""} {c=c $0 "\n"}
/-----END CERTIFICATE-----/{cmd="openssl x509 -noout -subject -enddate"; printf "%s", c | cmd; close(cmd)}'
subject=OU=Domain Control Validated, OU=PositiveSSL Wildcard, CN=*.badssl.com
notAfter=Apr 12 23:59:59 2015 GMT
subject=... CN=COMODO RSA Domain Validation Secure Server CA
notAfter=Feb 11 23:59:59 2029 GMT
subject=... CN=COMODO RSA Certification Authority
notAfter=May 30 10:48:38 2020 GMT
스크립트에서는 openssl x509 -noout -checkend 0을 쓴다. 만료면 Certificate will expire와 종료
코드 1을 낸다. 인자는 초 단위라서 2592000을 주면 30일 안에 만료되는지 본다. 감시 스크립트로
만드는 법은 인증서 만료 알림 체계 직접 만들기에 있다.
서버 인증서가 만료됐다면
체인 출력에서 0번의 NotAfter가 지났으면 서버 인증서가 만료된 것이다. 자동 갱신이 멈춘 경우는
Let’s Encrypt 자동갱신이 실패하는 흔한 이유에 있다. 더
헷갈리는 건 갱신은 됐는데도 만료 인증서가 나가는 경우다.
리로드 누락, 복사본 파일
nginx 문서는 설정 변경이 reload나 재시작 전까지 적용되지 않는다고 적는다. 인증서 파일도
마찬가지라서 디스크의 파일만 새것이고 프로세스는 옛 인증서를 계속 내보낸다. 예외는
ssl_certificate 경로에 변수를 쓴 구성이다. 이때는 핸드셰이크마다 파일을 읽는다.
디스크 파일과 서버 응답의 시리얼을 비교한다.
# 디스크 (Certbot 기본 경로)
openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -noout -serial -enddate
# 서버가 지금 내주는 것
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -serial -enddate
시리얼이 다르면 리로드를 안 했거나, 웹서버가 복사본을 보고 있다.
리로드는 nginx -t && nginx -s reload로 한다(Apache는 apachectl -k graceful). Certbot에서는
--deploy-hook이나 /etc/letsencrypt/renewal-hooks/deploy/의 실행 파일로 갱신에 성공할 때마다
리로드하게 만든다.
복사본은 인증서를 /etc/nginx/ssl/ 같은 곳에 복사해 두고 설정이 그 복사본을 가리키는 경우다.
Certbot이 갱신 때 새 파일로 바꾸는 곳은 /etc/letsencrypt/live/ 쪽이다. Certbot 문서도 복사하지
말고 그 파일을 직접(또는 심볼릭 링크로) 가리키라고 적는다. 실제 ssl_certificate 경로는
nginx -T로 확인한다.
리로드가 조용히 실패하는 경우는 Nginx 인증서 교체와 무중단 리로드에, Apache와 IIS는 Apache·IIS 인증서 교체 절차에 있다.
일부 노드만 갱신
증상이 독특하다. 일부 사용자만 실패하고, 새로고침하면 될 때도 있고, 지역이나 통신사에 따라 갈린다. 서버마다 인증서를 따로 넣거나 서버마다 Certbot을 돌리는 구성에서 한두 대가 빠진 경우다.
-connect www.example.com:443은 리졸버가 준 주소 하나에만 붙는다. 사용자의 브라우저는 다른 주소에
붙을 수 있다. DNS 라운드로빈이나 서버 여러 대 뒤라면 주소마다 따로 본다. -connect에는 IP를,
-servername에는 원래 이름을 준다.
host=www.example.com
for ip in $(dig +short A "$host" | grep -E '^[0-9.]+$'); do
printf '%-16s ' "$ip"
echo | openssl s_client -connect "$ip:443" -servername "$host" 2>/dev/null \
| openssl x509 -noout -enddate -serial | tr '\n' ' '
echo
done
grep은 dig +short가 섞어 내는 CNAME 줄을 걸러 낸다. 한 노드만 옛 인증서면 출력이 이런
모양이다(주소와 값은 예시).
203.0.113.10 notAfter=Dec 28 20:03:05 2026 GMT serial=05C1...
203.0.113.11 notAfter=Oct 2 08:12:40 2026 GMT serial=03A9...
이렇게 한 주소만 notAfter·serial이 다르면 그 노드다. IPv4는 갱신했는데 IPv6 쪽 서버를 빠뜨린
경우도 같은 모양으로 나온다. IPv6는 dig +short AAAA로 같은 루프를 돌린다. -connect에 IPv6
주소를 쓸 때는 -connect "[$ip]:443"처럼 대괄호로 감싼다. curl에서는 --resolve로 이름을 특정
주소에 묶는다.
curl -sS -o /dev/null --resolve www.example.com:443:203.0.113.11 https://www.example.com/
# curl: (60) SSL certificate problem: certificate has expired
dig가 보여 주는 주소가 전부가 아닐 수 있다. 지역마다 다른 주소를 주는 DNS면 리졸버 위치에 따라
답이 다르다. DNS 조회로 국내 통신사, 공용 DNS, 권한 네임서버의 응답을 나란히 비교하거나,
로드밸런서나 CDN 콘솔에서 노드 목록을 본다.
고치는 건 그 노드에 배포하는 것이다. 재발을 막으려면 배포 뒤 주소별 확인을 절차에 넣는다.
CDN·로드밸런서에 남은 옛 인증서
TLS를 CDN이나 로드밸런서에서 끝내면 브라우저가 보는 인증서는 거기에 올려 둔 인증서다. CDN 뒤라면 위 루프에 나오는 것도 CDN 엣지의 인증서다. 원서버 인증서를 갱신해도 엣지의 인증서는 바뀌지 않는다.
관리형이라고 다 자동 갱신되는 것도 아니다. 한 클라우드 인증서 관리 서비스 문서는 가져온(import) 인증서는 관리형 갱신 대상이 아니라고 명시한다. 직접 산 인증서를 올려 뒀다면 만료 전에 다시 올려야 한다. 관리형 인증서의 특성은 클라우드 관리형 인증서 가이드에 있다.
확인은 -connect에 엣지 주소와 원서버 IP를 각각 주고 -servername은 같은 이름을 준다. 엣지만 옛
인증서면 CDN이나 LB 쪽에서 교체한다.
만료된 중간 인증서와 옛 교차 서명
서버 인증서는 유효한데 함께 보내는 중간 인증서나 교차 서명(cross-sign) 인증서가 만료된 경우다. 검증 경로가 그 인증서를 지나가면 날짜 에러가 난다. 테스트 체인(루트 → 2025-01-01 만료 중간 → 2027년까지 유효한 서버 인증서)으로 재현하면 depth 1에서 실패한다.
$ openssl verify -CAfile root.pem -untrusted inter.pem leaf.pem
CN=Test Intermediate
error 10 at 1 depth lookup: certificate has expired
error leaf.pem: verification failed
s_client로 붙으면 depth=1 CN=Test Intermediate 바로 아래에 verify error:num=10이 찍힌다.
Firefox는 이 경우를 SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE로 따로 구분한다.
대체 경로가 있으면 최신 클라이언트는 넘어간다
앞의 expired.badssl.com이 그 예다. 서버는 2020-05-30 10:48:38 UTC에 만료된 교차 서명
인증서(AddTrust External CA Root가 서명한 COMODO RSA Certification Authority)를 아직 보낸다.
그런데 OpenSSL은 신뢰저장소에 있는 같은 이름의 루트로 경로를 만들어 그 인증서를 쓰지 않았다.
최신 브라우저와 라이브러리는 이렇게 만료되지 않은 다른 경로를 찾는다. 오래된 클라이언트는 그러지
못한다.
| 날짜 | 일 |
|---|---|
| 2020-05-30 | AddTrust External CA Root 만료. Sectigo는 업데이트된 기기는 영향이 없고, 최신 USERTrust 루트가 없는 레거시 기기는 신뢰하지 못한다고 안내했다 |
| 2021-09-30 | DST Root CA X3 만료. Let’s Encrypt는 OpenSSL 1.0.x 클라이언트가 ISRG Root X1을 신뢰해도 기본 체인에서 실패한다고 안내했다 |
| 2024-02-08 | Let’s Encrypt가 기본 체인에서 DST Root CA X3 교차 서명을 뺐다 |
| 2024-06-06 | 교차 서명이 든 긴 체인 제공 중단 |
| 2024-09-30 | 구형 Android용으로 유지하던 ISRG Root X1 교차 서명(DST Root CA X3 서명) 만료 |
2021년에 왜 그랬는지는 OpenSSL 블로그에 나와 있다. OpenSSL 1.0.2는 서버가 보낸(신뢰하지 않은) 체인을 항상 우선한다. 그 체인이 만료된 DST Root CA X3로 이어지면 그 경로를 골라 만료를 보고한다. 신뢰저장소에 ISRG Root X1이 있어도 소용없다. Let’s Encrypt는 OpenSSL 1.1.0 이상을 쓰라고 안내했다.
지금 이 문제가 남는 곳은 오래전에 손으로 만든 체인 파일이다. 2024-09-30 이후 DST Root CA X3가 서명한 ISRG Root X1 인증서는 만료 상태다. 그 시절 체인 파일을 계속 쓰는 서버는 만료된 인증서를 보내고 있다. 최신 브라우저에서는 조용하고 오래된 OpenSSL이나 임베디드 기기에서만 실패하니 늦게 발견된다.
고칠 때는 체인의 NotAfter를 전부 보고 지난 인증서를 체인 파일에서 뺀다. TLS 1.3 규격(RFC 8446)도
신뢰 앵커(루트)는 체인에서 생략해도 된다고 정한다. 체인은 손으로 이어 붙이지 말고 CA가 지금
제공하는 것으로 다시 받는다. Certbot이라면 live/의 fullchain.pem(서버 인증서 다음에 중간
인증서)을 웹서버가 직접 가리키게 한다.
클라이언트를 고칠 수 있으면 라이브러리와 신뢰저장소를 올린다. OpenSSL 1.0.2에 묶인 환경이면 OpenSSL
블로그가 적어 둔 우회책이 있다. 만료 루트를 신뢰저장소에서 빼거나, -trusted_first(코드에서는
X509_V_FLAG_TRUSTED_FIRST)로 신뢰저장소 인증서를 먼저 쓰게 한다. 이 동작은 1.0.2에서 기본으로
꺼져 있다. Sectigo는 AddTrust 교차 인증서를 서버에 더 설치하지 않아도 되고 서버 인증서도 다시 발급할
필요가 없다고 안내했다.
브라우저마다 체인을 다루는 방식이 달라 결과가 갈리는 문제는 크롬은 되는데 파이어폭스만 인증서 오류에 있다.
기기 시계가 틀렸다
특정 PC나 폰에서만, 여러 사이트가 한꺼번에 실패한다. 시계가 크게 틀리면 정상 사이트 대부분이 깨지고, Chrome이 시계 오류 화면을 띄우기도 한다.
기기 시계를 UTC로 보고 믿을 만한 서버의 Date 헤더와 비교한다. HTTP 규격(RFC 9110)에 따르면 시계가
있는 원서버는 2xx, 3xx, 4xx 응답에 Date를 넣어야 한다. 시계가 틀린 기기에서는 HTTPS 자체가 실패할
수 있으니 http로 묻는다.
date -u
curl -sI http://example.com | grep -i '^date:'
Sun Oct 4 05:05:02 UTC 2026
Date: Sun, 04 Oct 2026 05:05:04 GMT
몇 초 차이는 문제가 아니다. 분이나 시간, 날짜 단위로 어긋나 있으면 시계가 원인이다.
| OS | 확인 | 동기화 |
|---|---|---|
| Windows | w32tm /query /status |
w32tm /resync, 설정의 시간 자동 설정 |
| macOS | sntp time.apple.com (-S 없이는 측정만 함) |
시스템 설정의 날짜 및 시간 자동 설정 |
| Linux (systemd) | timedatectl의 System clock synchronized: yes |
timedatectl set-ntp true |
Chrome은 되는데 다른 앱만 실패한다면
Chrome은 인증서 검증에 네트워크 시각을 쓴다. 받아 둔 네트워크 시각으로 먼저 검증하고, 날짜나 폐기
관련 오류로 실패하면 시스템 시각으로 한 번 더 검증한다. 둘 중 하나로 통과하면 통과다. 소스의
기본값을 보면 이 기능(CertVerificationNetworkTime)은 Chrome 133부터 데스크톱, 135부터 Android에서
켜졌고 ChromeOS에서는 꺼져 있다. 네트워크 시각 조회가 꺼진 환경(iOS, ChromeOS)이나
BrowserNetworkTimeQueriesEnabled 정책을 끈 환경에서는 효과가 없다.
그래서 같은 PC에서 Chrome은 열리는데 Firefox, curl, 업무용 앱만 날짜 에러를 내면 시계를 의심한다. Chrome이 열린다고 그 기기 시계가 맞는 건 아니다.
시계가 틀어지는 경로
자동 시간 설정을 끄고 손으로 맞춘 경우가 있다. 시간대를 잘못 고른 채 표시 시각만 맞추면 UTC가 시간대 차이만큼 어긋난다. Firefox의 시계 오류 안내도 날짜, 시각과 함께 시간대를 맞추라고 한다.
VM이면 스냅샷 복원도 의심한다. 스냅샷으로 되돌리면 VM의 메모리, 설정, 디스크가 스냅샷을 찍은 시점의 상태로 돌아간다. VMware Tools 문서는 스냅샷 복원 뒤를 게스트 시계를 맞추는 시점으로 꼽는다. 게스트 도구의 시각 동기화를 꺼 둔 VM은 복원 직후 시계부터 확인한다.
CMOS 배터리도 있다. 한 PC 제조사 문서는 CMOS 배터리 고장의 가장 흔한 증상으로 BIOS의 날짜와 시각이 틀리거나 늦어지는 것을 든다. 배터리가 다 되면 켤 때마다 날짜와 시각을 다시 넣으라는 안내가 나온다고도 적었다. 이런 PC는 부팅할 때마다 틀린 시계로 출발한다.
자동 시간 동기화를 켜면 된다(위 표). 회사 PC는 도메인의 시간 동기화 정책이 있으니 담당자와 확인한다. 사용자에게는 “날짜·시간 자동 설정을 켜고 브라우저를 다시 시작”이라고 안내하는 게 가장 짧다.
아직 유효하지 않은 인증서
RFC 5280은 유효기간을 notBefore부터 notAfter까지(양 끝 포함)로 정의한다. 기기 시계가 notBefore보다 이르면 아직 유효하지 않은 인증서다(OpenSSL 9번).
공인 인증서는 notBefore를 발급 시각보다 조금 앞당겨 찍는다. CA/B Forum Baseline Requirements
(2.3.1)는 서버 인증서의 notBefore를 “서명 시점에서 48시간 이내의 값”으로 정한다. Let’s Encrypt는
1시간쯤 앞당긴다. 2026-09-04 발급된 letsencrypt.org 인증서는 notBefore가 14:34:32 UTC이고, 같은
인증서에 든 CT 로그 타임스탬프(SCT)는 15:33:02 UTC다. Let’s Encrypt 팀 관계자는 이 앞당김이 약간의
시계 차이를 흡수하려는 것일 뿐이니 다른 용도로 기대면 안 된다고 했다.
그래서 공인 인증서라면 이 에러는 보통 갱신 직후에, 시계가 늦은 기기에서만 난다. 기기 시계가 앞당긴 폭(약 1시간)보다 더 늦으면 갓 갱신한 인증서가 그 기기에는 아직 미래의 인증서다. 기기 시계로 notBefore가 지나면 저절로 풀리니 “몇 시간 뒤 괜찮아졌다”는 보고가 따라온다. 원인은 기기 시계다. 인증서 수명이 짧아져 갱신이 잦아질수록 이런 창도 자주 열린다. 수명이 줄어드는 일정은 인증서 유효기간 47일 시대에 있다.
openssl verify -attime으로 재현해 볼 수 있다. 지정한 시각(유닉스 초)을 “지금”으로 놓고 검증하는
옵션이다. 위 letsencrypt.org 인증서(2026-09-04 14:34:32 UTC부터 유효)를 그보다 이른 시각으로
검증했다. chain.pem은 서버가 보낸 중간 인증서들이다.
# 2026-09-04 14:00:00 UTC (macOS: date -j -u -f '%Y-%m-%d %H:%M:%S' '2026-09-04 14:00:00' +%s)
openssl verify -attime 1788530400 -untrusted chain.pem leaf.pem
CN=letsencrypt.org
error 9 at 0 depth lookup: certificate is not yet valid or the system clock is incorrect
error leaf.pem: verification failed
-attime을 빼면 leaf.pem: OK다. 사용자 기기의 시각을 넣어 보면 그 기기에서 무엇이 실패할지 서버
앞에서 확인할 수 있다.
사설 CA나 자체 서명 인증서라면 발급하는 쪽 시계가 원인일 수 있다. 시계가 앞선 서버에서 발급하면 notBefore가 미래로 찍혀서 시계가 맞는 클라이언트가 모두 실패한다. 발급 서버의 시계를 고치고 다시 발급한다.
어느 쪽인지는 체인 출력의 v:NotBefore와 기기 시계를 나란히 놓으면 갈린다. 바깥에서 봤을 때
notBefore가 이미 지났으면 기기 쪽, 아직 미래면 발급 쪽이다.
중간에서 인증서를 바꿔 끼우는 장비
회사망의 보안 게이트웨이나 프록시가 HTTPS를 복호화해 검사하면, 브라우저가 보는 인증서는 장비가 만든 것이다. 한 TLS 검사 게이트웨이 벤더 문서는 이런 검사가 동작하려면 사용자 기기에 루트 인증서를 설치하고 신뢰하게 해야 한다고 적는다. 그 인증서에도 만료일이 있다. 같은 문서는 자사 기본 인증서가 2025-02-02 16:05 UTC에 만료됐다고 공지했고, 루트를 새로 만들 때는 5년 뒤 만료를 권한다. 장비 쪽 인증서가 날짜 문제를 일으키면 그 망 안의 사용자만, 사이트를 가리지 않고 실패한다.
판별하려면 망 안의 실패하는 기기에서 본 발급자와 망 밖(휴대폰 데이터 등)에서 본 발급자를 비교한다. 망 안에서만 보안 장비나 사내 CA 이름이 보이면 장비의 인증서다. 서버 운영자가 고칠 수 없으니 장비 담당에게 넘긴다. 장비 루트를 신뢰시키는 일은 ERR_CERT_AUTHORITY_INVALID 원인별 해결에 있다.
다른 클라이언트에서는
| 클라이언트 | 만료 | 아직 유효하지 않음 |
|---|---|---|
| Chrome·Edge | NET::ERR_CERT_DATE_INVALID |
같음 |
| Firefox | SEC_ERROR_EXPIRED_CERTIFICATE, 발급자(중간) 인증서면 SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE |
MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE, 발급자면 ..._NOT_YET_VALID_ISSUER_CERTIFICATE |
| Apple URLSession | serverCertificateHasBadDate (-1201, 만료 또는 아직 유효하지 않음) |
serverCertificateNotYetValid (-1204) |
| Apple Security 프레임워크 | errSecCertificateExpired (-67818), errSSLCertExpired (-9814) |
errSecCertificateNotValidYet (-67819), errSSLCertNotYetValid (-9815) |
Android WebView SslError |
SSL_EXPIRED (1) |
SSL_NOTYETVALID (0). 그 밖의 날짜 문제는 SSL_DATE_INVALID (4) |
| Java | CertificateExpiredException: NotAfter: ... |
CertificateNotYetValidException: NotBefore: ... |
| OpenSSL | 10 certificate has expired |
9 certificate is not yet valid |
| Windows CryptoAPI·Schannel | CERT_E_EXPIRED (0x800B0101, 유효기간 밖), SEC_E_CERT_EXPIRED (0x80090328) |
CERT_E_EXPIRED와 같음 |
Microsoft는 2018-12-06 데스크톱 Edge를 Chromium 호환 플랫폼으로 옮긴다고 발표했고, Edge 사용자
문의에도 같은 NET::ERR_CERT_DATE_INVALID가 나온다. Apple Security 코드는 macOS SDK SecBase.h
기준이다.
Java는 경로 검증 단계에서 실패하므로 바깥 메시지가
PKIX path validation failed: java.security.cert.CertPathValidatorException: validity check failed이고,
날짜는 원인 예외에 찍힌다. 경로 구성 단계 에러와 구분하는 법은
PKIX path building failed 원인별 해결에 있다.
OpenSSL 3.6부터 9번 문구가 certificate is not yet valid or the system clock is incorrect로
바뀌었다(3.5까지는 앞부분만). 문자열로 알림을 거는 스크립트는 앞부분으로 매칭한다.
2026-10-04에 만료 테스트 사이트 expired.badssl.com에 붙여 본 결과다.
# curl 8.7.1 (macOS 기본)
curl: (60) SSL certificate problem: certificate has expired
# Python 3.14 (urllib)
[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:1082)
# Node.js 24 — err.code, err.message
CERT_HAS_EXPIRED, certificate has expired
# Go 1.27 (macOS 는 OS 검증기 문구가 뒤에 붙는다)
tls: failed to verify certificate: x509: certificate has expired or is not yet valid: “*.badssl.com” certificate is expired
Go 자체 검증기는 뒷부분을 current time ... is after ...처럼 찍는다. 어느 문구도 어느 인증서가
만료됐는지는 말해 주지 않는다. OpenSSL 10번은 서버 인증서든 중간 인증서든 같은 번호다. 그래서 체인
출력의 depth를 봐야 한다.
certdesk에서는
certdesk 진단은 한국 측정 지점 A에서 서버가 실제로 보낸 체인을 받아 신뢰저장소로 검증하고, 날짜 문제를 아래 사유로 나눈다.
| 사유 코드 | 화면 문구 | 심각도 |
|---|---|---|
EXPIRED |
서버가 만료된 인증서를 제공 중 | 위험 |
NOT_YET_VALID |
인증서 유효기간이 아직 시작되지 않음 (서버·발급 시각 확인) | 위험 |
EXPIRED_INTERMEDIATE |
체인의 중간 인증서가 만료됨 | 위험 |
NEWER_NOT_DEPLOYED |
새 인증서가 발급됐지만 서버에 반영되지 않음 | 참고 (정상 갱신 시점이 지났으면 주의) |
EXPIRED_INTERMEDIATE는 검증이 날짜 오류(OpenSSL 10번)로 실패했는데 서버 인증서는 유효할 때
붙는다. 상세에는 서버가 보낸 인증서 중 만료된 것의 이름이 나온다. NEWER_NOT_DEPLOYED는 CT 로그에
같은 이름 목록으로 더 나중에 발급된 인증서가 있는데 서버는 이전 인증서를 내줄 때 붙는다. 갱신은
됐는데 반영이 안 된 경우를 바깥에서 잡는 신호다.
측정 지점별 결과(measuredFrom)는 한 줄씩 나온다. 한국 측정 지점 A, 그리고 공용 측정망의 한국
측정 지점 B와 해외 측정 지점(도쿄)마다 “검증 통과”, “검증 실패”와 그 사유, “측정 못 함” 중 하나가
찍힌다. 지점마다 결과가 갈리면 POINT_MISMATCH(주의), 받은 인증서가 다르면 DIFFERENT_CERTS(참고),
IPv4와 IPv6의 인증서가 다르면 FAMILY_MISMATCH(주의)가 붙는다. 체인 그림은 서버가 보낸 순서대로
인증서마다 만료일을 보여 주고, 지난 것은 “만료 (N일 경과)“로 표시한다.
2026-10-04에 expired.badssl.com을 같은 결과를 주는 API(/api/v1/certificate)로 조회했다(일부
필드 생략).
{
"status": "CRITICAL",
"headline": "서버가 만료된 인증서를 제공 중. 2015-04-13 만료 (4193일 경과)",
"reasons": [
{ "code": "EXPIRED", "severity": "critical", "message": "서버가 만료된 인증서를 제공 중" },
{ "code": "NEWER_NOT_DEPLOYED", "severity": "warn",
"message": "새 인증서가 발급됐지만 서버에 반영되지 않음",
"detail": "CT 로그: 2026-12-29 만료 · Let's Encrypt" }
]
}
날짜는 KST로 표시된다(UTC 2015-04-12 23:59:59). 이 테스트 사이트는 일부러 옛 인증서를 내주는데,
같은 이름 목록의 새 인증서가 CT 로그에 있다. 실제 서비스였다면 서버 인증서 만료 쪽을 먼저 의심할
결과다. 체인 그림에는 2020-05-30에 만료된 교차 서명도 나오지만 사유는 EXPIRED다. 서버 인증서
자체가 만료면 그쪽을 사유로 잡고, 최신 OpenSSL은 대체 경로가 있는 교차 서명을 오류로 보지 않기
때문이다. 오래된 클라이언트가 실패할 체인인지는 체인 그림의 날짜로 판단한다.
A·AAAA 주소가 여럿이면 주소마다 인증서를 받아 비교한다. 일부 노드만 다른 인증서를 내면
NODE_MISMATCH, 일부 노드만 곧 만료되면 NODE_EXPIRING이 붙는다. 443이 아닌 포트는 host:port로
넣는다. 465·993처럼 처음부터 TLS인 포트와 25·587·143·110·389의 STARTTLS도 검사한다.
만료 사고가 왜 반복되는지는 인증서 만료 사고는 왜 반복되는가에 따로 적었다.
참조
- Chromium: net_error_list.h
- Chromium: ssl_errors_strings.grdp
- Chromium: security_interstitials_strings.grdp
- Chromium: error_classification.cc
- Chromium: ssl_error_handler.cc
- Chromium: cert_verify_proc_builtin.cc
- Chromium: browser_features.cc (main)
- Chromium: browser_features.cc (M133, branch 6943)
- Chromium: browser_features.cc (M135, branch 7049)
- Chromium: network_time_tracker.cc
- Chromium: BrowserNetworkTimeQueriesEnabled 정책
- Firefox 소스: certError.ftl
- Firefox 소스: nsserrors.properties
- Microsoft: Microsoft Edge의 Chromium 전환 발표 (2018-12-06)
- Microsoft Q&A: Edge의 NET::ERR_CERT_DATE_INVALID 문의
- Apple: URLError.serverCertificateHasBadDate
- Apple: URLError.serverCertificateNotYetValid
- Android 소스: SslError.java
- OpenJDK: BasicChecker.java
- OpenJDK: CertificateValidity.java
- OpenJDK: PKIXValidator.java
- Microsoft: COM Error Codes (CERT_E_EXPIRED, SEC_E_CERT_EXPIRED)
- OpenSSL 소스: x509_txt.c (master)
- OpenSSL 소스: x509_txt.c (3.5)
- OpenSSL 3.0 소스: apps/s_client.c
- OpenSSL: openssl-s_client 문서 원본
- OpenSSL: openssl-x509 문서 원본
- OpenSSL: Old Let’s Encrypt root certificate expiration and OpenSSL 1.0.2 (2021-09-13)
- Go 소스: crypto/x509/verify.go
- curl: man page
- RFC 5280: Internet X.509 PKI Certificate and CRL Profile
- RFC 8446: TLS 1.3
- RFC 9110: HTTP Semantics
- CA/Browser Forum: Baseline Requirements
- Let’s Encrypt: DST Root CA X3 Expiration (September 2021)
- Let’s Encrypt: Shortening the Let’s Encrypt Chain of Trust (2023-07-10)
- Let’s Encrypt Community: notBefore 앞당김에 대한 팀 답변
- Sectigo: AddTrust External CA Root Expiring May 30, 2020
- nginx: Beginner’s Guide
- nginx: ngx_http_ssl_module
- nginx: Command-line parameters
- Apache HTTP Server: Stopping and Restarting
- Certbot: User Guide
- 클라우드 인증서 관리 서비스 문서: 관리형 갱신 대상
- TLS 검사 게이트웨이 벤더 문서: 사용자 측 루트 인증서
- VMware vSphere: Revert a Virtual Machine Snapshot
- VMware Tools: Time Synchronization Concepts
- PC 제조사 지원 문서: CMOS 배터리 교체
- Microsoft: Windows Time service tools and settings
- systemd: timedatectl