HTTPS 인증서 오류 해결 (만료·체인·날짜)
NET::ERR_CERT_DATE_INVALID / SSL certificate problem: certificate has expired / unable to verify the first certificate
이런 증상일 때
크롬은 "NET::ERR_CERT_DATE_INVALID", 파이어폭스는 "SEC_ERROR_EXPIRED_CERTIFICATE", curl·서버 로그는 "certificate has expired" 또는 "unable to verify the first certificate"를 띄우며 HTTPS 접속이 막힙니다. 데스크톱 크롬은 캐시 때문에 멀쩡한데 모바일·앱 클라이언트만 실패하는 경우는 체인 누락 신호입니다. 인증서 만료 직후, 또는 certbot 자동 갱신이 조용히 실패해 있었을 때 갑자기 나타납니다.
원인
- 서버 인증서의 유효기간(notAfter)이 실제로 지나 만료됨 — Let's Encrypt는 90일 주기라 자동 갱신이 멈춰 있었으면 빈번
- fullchain.pem 대신 cert.pem(리프 인증서)만 nginx/apache에 설정해 중간 인증서가 빠짐 → "unable to verify the first certificate"
- certbot renew가 .well-known/acme-challenge 검증 실패(80포트 차단·이미 만료된 인증서로 강제 HTTPS·IPv6 AAAA 레코드 누락)로 조용히 실패해 갱신이 멈춤
- 접속하는 기기(특히 윈도우·국내 인터넷 PC)의 시스템 시계가 틀어져 정상 인증서를 만료/미발효로 오판
- 브라우저가 갱신 전 인증서 정보를 SSL 캐시에 들고 있어 갱신 후에도 옛 에러를 표시
해결 방법
추측 말고 서버가 실제로 보내는 인증서를 들여다봅니다. -dates로 만료 여부를, -showcerts로 체인(보통 리프+중간 2장 이상)이 다 오는지 봅니다. notAfter가 현재보다 과거면 만료, 인증서가 1장만 오면 체인 누락입니다.
# 만료일(notBefore/notAfter) 확인
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates
# 체인 전체가 오는지 확인 (Certificate chain 블록에 0,1,2... 여러 장이어야 정상)
echo | openssl s_client -connect example.com:443 -servername example.com -showcerts 2>/dev/null | grep -E "notAfter|s:|i:"certbot certificates로 만료일과 인증서명을 확인한 뒤, 아직 만료 전이면 그냥 renew, 이미 만료됐거나 강제로 다시 받으려면 --force-renewal을 씁니다. 갱신 후 웹서버를 리로드해야 새 인증서가 적용됩니다.
# 현재 인증서 상태·만료일 확인
sudo certbot certificates
# 갱신 (만료 임박분만) / 강제 갱신
sudo certbot renew
sudo certbot renew --force-renewal
# 적용
sudo systemctl reload nginx # apache면 reload apache2"unable to verify the first certificate"·모바일만 실패의 대부분은 리프 인증서만 보내는 경우입니다. nginx ssl_certificate를 반드시 fullchain.pem(리프+중간 합본)으로 바꿉니다. 수동 합본이라면 리프 다음에 중간 인증서를 순서대로 cat 합니다.
# nginx.conf / 사이트 설정
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # cert.pem 아님!
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# 수동 합본이 필요할 때 (리프 -> 중간 순서)
cat your_domain.crt intermediate.crt > fullchain.crt
sudo nginx -t && sudo systemctl reload nginx--dry-run은 스테이징 서버로 실제 발급 없이 갱신 전 과정을 시험합니다. 여기서 unauthorized·acme-challenge 오류가 나면 80포트 방화벽, .well-known 서빙 경로, 이미 만료된 인증서로 강제 HTTPS가 걸린 상태가 원인입니다. 만료된 인증서가 갱신을 막으면 강제 HTTPS를 잠깐 끄고 갱신 후 되돌립니다.
# 갱신 과정 시험 (실발급 안 함). -v로 상세 로그
sudo certbot renew --dry-run -v
# 80포트로 오는 ACME 검증이 막히면 webroot/standalone 확인
sudo certbot certonly --webroot -w /var/www/html -d example.com --dry-run서버 인증서는 멀쩡한데 특정 PC만 ERR_CERT_DATE_INVALID라면 그 기기의 시계가 틀린 것입니다. 국내 사내 PC·노트북에서 CMOS 배터리·시간 서버 미설정으로 흔합니다. 관리자 권한 명령 프롬프트에서 Windows Time 서비스를 재시작하고 강제 동기화합니다.
net stop w32time
net start w32time
w32tm /resync
# 동기화 소스가 "Local CMOS Clock"이면 시간서버 수동 지정
w32tm /config /manualpeerlist:"time.windows.com,0x1" /syncfromflags:manual /update
w32tm /resync서버는 새 인증서인데 브라우저가 옛 세션 캐시를 들고 있으면 에러가 그대로입니다. 시크릿 창에서 정상이면 캐시가 원인입니다. openssl로 새 만료일(미래)이 보이는데 브라우저만 옛 에러면 캐시·시계만 점검하면 됩니다.
# 새 인증서가 실제 적용됐는지 서버 측 재확인 (만료일이 미래여야 정상)
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -enddate
# 크롬: chrome://net-internals/#sockets -> Flush socket pools, 또는 시크릿 창으로 재현 확인