수정 약 3분

SSL 인증서 만료일 확인하는 법

이 글의 순서 5개 절
  1. 브라우저
  2. openssl
  3. PowerShell
  4. 자주 놓치는 것
  5. 도메인이 많으면

만료일은 다들 안다. 사고는 엉뚱한 곳을 보고 안심해서 난다. 브라우저에 뜨는 날짜와 서버 디스크에 있는 파일의 날짜가 다를 수 있고, 곧 만료되는 게 서버 인증서가 아니라 중간 인증서일 때도 있다.

브라우저

주소창 자물쇠를 누르고 인증서 정보에서 유효기간을 본다.

  • Chrome: 자물쇠 → 이 사이트에 대한 인증서가 유효합니다 → 세부 정보
  • Firefox: 자물쇠 → 연결 보안 → 자세히 보기

빠르지만 보이는 건 내가 붙은 서버 하나뿐이다. 로드밸런서 뒤에 서버가 여러 대이고 그중 한 대만 옛 인증서를 들고 있어도 브라우저는 알려 주지 않는다. 지금 내 화면에 온 응답 하나를 확인한 것이지, 서비스 전체를 확인한 게 아니다.

openssl

서버가 실제로 내주는 인증서를 본다.

openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates

출력:

subject=CN = example.com
issuer=C = US, O = Let's Encrypt, CN = YR1
notBefore=Aug 12 04:31:17 2026 GMT
notAfter=Nov 10 04:31:16 2026 GMT

-servername은 빼면 안 된다. SNI는 접속할 때 어느 도메인으로 왔는지 서버에 알려 주는 TLS 확장이다. SNI 없이 붙으면 그 IP의 기본 vhost 인증서가 돌아와서 확인하려던 도메인과 다른 인증서를 보게 된다. “만료일이 왜 다르지” 하는 경우 상당수가 이것이다.

남은 일수만 알고 싶으면 -checkend를 쓴다. 답을 종료 코드로 줘서 스크립트에 넣기 좋다.

if cert=$(openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
    | openssl x509 2>/dev/null); then
  echo "$cert" | openssl x509 -noout -checkend $((30*86400)) \
    && echo "30일 이상 남음" || echo "30일 이내 만료"
else
  echo "접속 실패 또는 인증서를 받지 못함"
fi

인증서를 받았는지부터 가른다. 그냥 파이프로 이으면 DNS나 접속이 실패해도 x509가 실패로 끝나서 「30일 이내 만료」로 읽힌다.

파일로 가진 인증서

openssl x509 -in /etc/ssl/certs/example.crt -noout -dates

디스크의 파일과 서버가 내주는 인증서는 다를 수 있다. 갱신하고 리로드를 안 했으면 파일은 새것이고 서빙은 옛것이다. 둘 다 봐야 한다.

PowerShell

$tcp = [Net.Sockets.TcpClient]::new("example.com", 443)
$ssl = [Net.Security.SslStream]::new($tcp.GetStream(), $false, { $true })
$ssl.AuthenticateAsClient("example.com")
$ssl.RemoteCertificate.GetExpirationDateString()
$ssl.Dispose(); $tcp.Dispose()

{ $true }는 검증을 건너뛰는 콜백이다. 이미 만료된 인증서도 날짜를 읽으려고 넣었다. 예전에 많이 쓰던 HttpWebRequest의 ServicePoint.Certificate는 Windows PowerShell 5.1에서만 값이 나오고 PowerShell 7에서는 비어 있다.

IIS에 설치된 인증서를 직접 보려면:

Get-ChildItem Cert:\LocalMachine\My |
  Select-Object Subject, NotAfter, Thumbprint |
  Sort-Object NotAfter

NotAfter로 정렬하면 곧 만료될 것이 맨 위에 온다. 인증서가 여러 장 깔린 서버라면 실제 바인딩에 쓰이는 게 어느 것인지 Get-WebBinding의 thumbprint와 맞춰 봐야 한다.

자주 놓치는 것

중간 인증서도 만료된다. 서버 인증서만 갱신하고 체인 파일을 그대로 두면 서버 인증서가 멀쩡해도 검증이 깨진다. openssl s_client 출력의 Certificate chain 항목을 하나씩 본다.

만료일과 갱신 시점은 다르다. Let’s Encrypt는 90일짜리 인증서를 60일 시점에 갱신하도록 설계돼 있다. 아직 30일 남았는데 갱신 실패 알림이 오는 게 정상이다. 30일 남은 시점의 실패는 이미 두 번 넘게 실패했다는 뜻이다.

서브도메인은 따로 만료된다. 와일드카드가 아니면 www와 api는 서로 다른 인증서다. 루트 도메인만 보고 넘어가면 놓친다.

도메인이 많으면

열 개가 넘어가면 손으로 확인하는 건 오래 못 간다. 최소한 이 정도는 돌린다.

for d in example.com api.example.com www.example.com; do
  end=$(openssl s_client -connect "$d:443" -servername "$d" </dev/null 2>/dev/null \
        | openssl x509 -noout -enddate | cut -d= -f2)
  printf '%-28s %s\n' "$d" "${end:-접속 실패}"
done

엑셀보다는 낫다. 다만 도메인 자체의 만료(레지스트라 등록 만료)는 여기서 안 잡힌다.

만료일은 멀쩡한데 브라우저가 오류를 낸다면 체인 문제일 가능성이 크다. 크롬은 되는데 파이어폭스만 인증서 오류가 나는 이유와 NET::ERR_CERT_AUTHORITY_INVALID 원인별 해결에 이어서 적었다. 교체 절차는 Nginx 인증서 교체와 무중단 리로드에 있다.