Let's Encrypt는 인증서를 90일짜리로 발급하고 자동으로 갱신합니다 — 그래서 설정한 지 일주일이면 사람들은 그것에 대해 생각하기를 그만둡니다. 문제는 자동 갱신이 조용히 망가진다는 것입니다. 오류를 아무도 보지 못하고, 석 달 뒤 사이트는 방문자를 브라우저 경고로 맞이합니다. 2025년부터 Let's Encrypt는 만료 알림 메일도 보내지 않게 되었으니 마지막 외부 알림마저 사라졌습니다.

아래는 갱신이 멈추는 이유들이며, 빈도가 높은 순서입니다.

1. 인증서는 갱신됐는데 서비스가 다시 읽지 않았다

가장 흔한 경우입니다. certbot renew는 돌았고 새 파일은 디스크에 있는데, nginx나 Apache는 옛것을 메모리에 담은 채 방문자에게 그것을 계속 내줍니다 — 설정을 다시 읽히기 전까지. 형식적으로는 다 괜찮고, 실제로는 사이트에 만료된 인증서가 있습니다.

치료법은 성공적인 갱신 뒤에 실행되는 훅입니다.

sudo certbot renew --deploy-hook "systemctl reload nginx"

이것은 갱신 파일(/etc/letsencrypt/renewal/example.com.conf의 [renewalparams] 섹션, renew_hook 줄)에 한 번 쓰이고 그다음부터는 스스로 작동합니다.

2. 웹 서버는 바뀌었는데 challenge는 그대로다

HTTP-01 challenge는 /.well-known/acme-challenge/의 파일이 평범한 HTTP로 제공되기를 요구합니다. 대개 이렇게 망가집니다.

  • 설정에 HTTP → HTTPS 무조건 리다이렉트를 넣었다 — challenge가 리다이렉트를 따라가다 실패합니다;
  • deny all이 붙은 location ~ /\. 규칙이 생겨 점이 든 모든 디렉터리를 닫았고, .well-known도 함께 닫혔습니다;
  • 사이트 루트를 옮겼는데 certbot의 설정에는 옛 --webroot-path가 남았습니다;
  • 국가 차단이나 basic 인증을 켰는데 검증 서버까지 모두와 함께 걸렸습니다.

설정에서의 올바른 순서: 먼저 /.well-known/acme-challenge/를 위한 전용 location, 그다음에야 일반 리다이렉트.

3. 타이머가 꺼져 있다

갱신은 systemd 타이머나 cron 작업에서 돌아가며, 둘 다 문제를 살피는 도중에 실수로 꺼지거나 서버를 옮기면서 잃어버릴 수 있습니다.

systemctl list-timers | grep certbot
sudo certbot renew --dry-run

두 번째 명령은 쿼터를 쓰지 않고 시험 모드로 전체 갱신 주기를 수행합니다. 「한 달 뒤에 이것이 갱신될 것인가」에 사후가 아니라 사전에 답하는 유일한 확인입니다.

4. 도메인이 더는 이쪽을 가리키지 않는다

인증서가 다섯 도메인을 담고 있는데 그중 하나가 다른 호스트로 옮겨 가고 DNS가 바뀌었습니다. 그 도메인의 검증이 통과하지 않게 되고, certbot은 인증서 전체를 갱신하지 않습니다 — 멀쩡한 네 도메인까지 함께. 여분의 도메인은 메시지와 함께 무시할 것이 아니라 인증서에서 빼야 합니다.

5. 한도에 부딪혔다

Let's Encrypt는 도메인당 주간 인증서 수를 제한합니다. 대개 문제를 살피는 도중의 「안 됐네, 다시 해 보자」 반복의 결과입니다. 실험용 staging 서버가 있고(--dry-run이나 --test-cert), 한도를 다 쓴 뒤가 아니라 그전에 그것을 써야 합니다.

저장된 것이 아니라 제공되는 것을 확인하라

디스크의 파일에 기댄 확인에는 하나의 결함이 있습니다. 파일은 새것인데 방문자는 옛 인증서를 받을 수 있다는 것입니다. 클라이언트 쪽에서 보세요.

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

-servername 옵션은 필수입니다. 그것 없이는 사이트가 여럿인 서버에서 당신이 뜻한 것이 아니라 먼저 오는 가상 호스트의 인증서를 받습니다. 출력의 notAfter가 실제 만료일입니다.

온 김에 체인도 확인하세요.

echo | openssl s_client -connect example.com:443 -servername example.com -showcerts 2>/dev/null | grep -c 'BEGIN CERTIFICATE'

인증서가 하나뿐이면 중간 인증서가 제공되지 않는 것입니다. 브라우저는 대개 그 구성을 용서합니다 — 체인을 스스로 만드니까요 — 그러나 curl, 모바일 애플리케이션, Java 클라이언트는 검증에 실패합니다. 전형적인 「나한테는 잘 열리는데 고객에게는 안 된다」입니다.

며칠 전에 경고할 것인가

30일은 합리적인 첫 경보입니다. certbot이 갱신을 시도하기 시작할 때 남는 기간이 그만큼이니, 그때까지 아무 일도 없었다면 메커니즘이 망가진 것입니다. 14일은 두 번째 경보이며 이미 오늘 손을 써야 합니다. 7일은 비상입니다.

중요한 것은 모든 인증서를 한꺼번에 지켜보는 일이며, Let's Encrypt에서 오지 않은 것도 포함합니다. 1년짜리로 산 인증서가 가장 확실하게 잊힙니다. 1년이면 그 존재도, 알림이 누구의 메일함으로 가는지도 모두가 잊으니까요. 도메인과 날짜와 남은 일수가 담긴 한 페이지가 이 문제를 통째로 닫아 줍니다 — 아래 데모에서처럼.