Files
keycloak-pattern/docs/evidence/d4-certificate-renewal/13-verdict.txt
T
DongHyeonkaandClaude Opus 5 faf55dc1be docs(d4): the renewal succeeded and was not served for 36 minutes 39 seconds
The forced renewal did work. What did not work was anything making nginx
notice.

  disk     cert2.pem written 2026-09-04 17:22:13 KST
  network  old serial through 08:58:47 UTC, new serial from 08:58:52 UTC
  gap      2199 seconds, and the old certificate was observed 428 times in it

The gap closed only because a person ran `nginx -s reload`. Nothing else
would have: certbot-renew.service is just `ExecStart=/usr/bin/certbot -q
renew` with no ExecStartPost, all three of renewal-hooks/{deploy,post,pre}
are empty, and certbot 5.7.0 here has no nginx plugin (dns-cloudflare,
manual, null, standalone, webroot). Three paths, all empty. Any one of them
would have been enough.

nginx holds the certificate in memory from startup, and certbot swaps the
live/ symlink rather than the path — so the config still looks right while
the served certificate is stale. Master 585 and worker 586 sharing a start
time 22.4 hours old is what proved no reload had happened.

This is invisible for 88 days. The timer ran twice today and exited
0/SUCCESS both times, because a renewal is not due yet. The day it becomes
due, the symptom is an expired certificate and the log still says SUCCESS.

The other half of the plan's question now has a measurement rather than an
expectation. The reload is genuinely graceful: 8856 polled connections, zero
non-200, p95 205.7ms before against 204.3ms after. And an in-flight request
— 845KB pulled at 20k/s, so still transferring 12 seconds in when the reload
landed — completed with all 845361 bytes over a single connection.

The earlier note that nginx reload "is graceful by design but must not be
written that way without checking" was right to hold back. The neighbouring
belief, that renewing gets the new certificate served, was the one that was
false, and there was no way to tell in advance which it would be.

Adds diagrams/d4-renewal-gap.svg and evidence 09-13.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 18:04:44 +09:00

100 lines
5.2 KiB
Plaintext

D-4 판정 — 갱신은 성공했고, 36분 39초 동안 반영되지 않았다
=============================================================
수집: 2026-09-04 · 감시 08:10:51 ~ 09:02 UTC (KST-9)
════ 답 1 — "갱신 실패"가 아니라 "갱신됐는데 안 집었다" ════
07-renewal-hook-missing.txt 에서 두 갈래로 남겼던 것이 갈렸다.
(a) 강제 갱신이 실패했다
(b) 성공했는데 훅이 없어 reload 가 안 됐다
→ ★ (b) 였다.
디스크 (12-certbot-state.txt)
/etc/letsencrypt/archive/auth.hyeonworks.com/
cert1.pem 2026-09-03 10:47:40 KST ← 처음 발급
cert2.pem 2026-09-04 17:22:13 KST ← ★ 강제 갱신, 새로 써졌다
fullchain2.pem 2026-09-04 17:22:13 KST
privkey2.pem 2026-09-04 17:22:13 KST
Certificate Name: auth.hyeonworks.com
Serial Number: 6c7cb6df1da8a6d7995d93c264bb9ecea1d
Expiry Date: 2026-12-03 07:21:52+00:00 (VALID: 89 days)
밖에서 본 것 (09-serial-timeline.txt, 5초 간격 564표본)
08:10:51 ~ 08:58:47 serial=0520BB...B853 notAfter=Dec 2 ← 옛 것
08:58:52 serial=06C7CB...EA1D notAfter=Dec 3 ← 바뀐 순간
08:22:13 ~ 08:58:52 구간에서 옛 인증서로 관측된 횟수: 428회
시간 계산
새 인증서 디스크 기록 : 08:22:13 UTC (= 17:22:13 KST, archive mtime)
실제 서빙 시작 : 08:58:52 UTC (= 17:58:52 KST, nginx -s reload)
────────────────────────────────────────────
공백 : 2199초 = 36분 39초
그리고 이 36분은 **우연히 짧았을 뿐이다.** reload 를 시킨 것은 사람이지
자동화가 아니다. 아무도 안 했다면 다음 nginx 재시작까지 — 즉 무기한 —
옛 인증서를 계속 서빙했을 것이다.
왜 아무도 reload 하지 않았나 — 원인 세 가지가 겹쳤다
1. certbot-renew.service 에 ExecStartPost 가 없다 (07 참조)
2. /etc/letsencrypt/renewal-hooks/{deploy,post,pre}/ 가 전부 비어 있다
deploy/ total 8 (. 과 .. 뿐)
post/ total 8
pre/ total 8
3. certbot 에 nginx 플러그인이 없다 — 그래서 certbot 이 스스로 못 고친다
Discovered plugins: dns-cloudflare, manual, null, standalone, webroot
(certbot 5.7.0)
★ 세 경로 전부가 비어 있다. 하나라도 있었으면 자동으로 반영됐다.
왜 이게 88일 동안 안 보이나
타이머는 정상이고 오늘도 두 번 status=0/SUCCESS 로 끝났다.
만료 30일 전(= 약 58일 뒤)까지는 certbot 이 갱신 자체를 하지 않으므로
"반영되지 않는다"는 결함이 발현할 기회가 없다. 발현하는 날 증상은
**인증서 만료** 이고, 그때 로그는 여전히 SUCCESS 라고 적혀 있을 것이다.
════ 답 2 — reload 는 무중단이다 (측정됨) ════
계획서의 질문: "nginx reload 중 진행 중이던 요청은 어떻게 되는가"
[새 연결] 0.2초 폴링, 08:10:51 ~ 09:02
전체 표본 8856건 / 비200 **0건**
응답시간 n 중앙 p95 최대
─────────────────────────────────────────────────────────
장기 평시 08:20~08:50 5398 98.0ms 205.7ms 1942.9ms
reload 직전 2분56초 489 116.0ms 200.8ms 387.7ms
reload 직후 2분08초 342 132.5ms 204.3ms 475.0ms
p95 가 205.7 → 204.3 으로 사실상 동일하고, 최대값은 오히려 낮다.
10초 구간 중앙값은 reload 전후 모두 80~190ms 사이를 오간다(WiFi 잡음).
→ **reload 로 인한 지연 증가는 관측되지 않았다.**
[진행 중이던 요청] 845KB 를 20k/s 로 받아 42초간 살려 둔 요청
08:58:40 시작 · 소요 40.84초 → 08:59:21 종료
reload 는 08:58:52, 즉 **요청 시작 12초 뒤 전송 한가운데**였다
코드=200 바이트=845361(전량) 연결수=1 curl종료=0
· 바이트가 전량이다 → 잘리지 않았다
· 연결수가 1이다 → 중간에 끊겨 재연결한 것이 아니다
· 옛 인증서로 시작한 연결이 새 워커 전환을 관통해 끝까지 갔다
in-flight 전체 50건 중 종료코드≠0 은 0건.
(앞서 76건 실패는 로컬 아티팩트였다 — 08-inflight-artifact.txt)
════ 세 줄 요약 ════
1. **"갱신 성공"과 "새 인증서 서빙"은 다른 사건이다.** 36분 39초 동안
디스크에는 새 인증서가, 네트워크에는 옛 인증서가 있었다. 428번 관측했다.
2. **reload 는 정말 무중단이었다.** 8856건 전부 200, p95 변화 없음,
전송 한가운데였던 42초짜리 요청이 845361바이트를 온전히 받았다.
D-4 문서가 "확인하지 않았으므로 그렇게 쓰면 안 된다"고 유보했던 명제가
이제 측정으로 뒷받침된다.
3. **위험한 쪽은 reload 가 아니라 reload 를 부르는 사람이 없다는 것이다.**
훅 디렉터리 3개가 전부 비었고, 유닛에 ExecStartPost 가 없고,
nginx 플러그인도 없다. 그리고 그 상태로 타이머는 SUCCESS 를 찍는다.