D-4 named the fix and never ran it. Running it:
/etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
#!/bin/sh
nginx -t && nginx -s reload
certbot reports "Hook 'deploy-hook' ran", the master stays at PID 585 and
the worker is replaced 28829 -> 37252, and the served certificate changes.
One trap worth naming: certbot prefixes the hook output with "ran with error
output" whenever the hook writes anything to stderr, and nginx's routine
types_hash warning goes to stderr. Everything inside is success — "test is
successful", "signal process started". A monitor that greps for "error"
would read a working hook as a failure. The worker PID is what to check.
Timing needed a clock correction. test-server has NTP off and runs 106
seconds fast; the dev machine matches Google and the Let's Encrypt ACME
endpoint exactly. Corrected, the hook's nginx -t lands at 12:27:50 UTC
against the new certificate's SCT at 12:27:49.054 — one second. The
correction validates itself: uncorrected, the hook would have run 104
seconds before the certificate existed.
That same skew is why D-4's gap was reported 106 seconds short; corrected to
2305s in the previous commit.
Remaining and left alone: whether certbot-renew.timer performs a real
renewal, which cannot be tested for about 89 days.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
397 lines
20 KiB
Markdown
397 lines
20 KiB
Markdown
# D-4 — 인증서 갱신
|
||
|
||
브랜치 `feature/keycloak-d4-certificate-renewal` ·
|
||
증거 [`docs/evidence/d4-certificate-renewal/`](evidence/d4-certificate-renewal/) ·
|
||
2026-09-04 17:25–17:35 KST
|
||
|
||
---
|
||
|
||
## 구조
|
||
|
||

|
||
|
||
**핵심 발견** — 갱신은 성공했는데 **38분 25초** 동안 반영되지 않았다.
|
||
|
||

|
||
|
||
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
|
||
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
|
||
|
||
---
|
||
|
||
## 0. 결론부터
|
||
|
||
| 확인 | 결과 |
|
||
|---|---|
|
||
| 인증서 구성 | **SAN 3개** (`auth`/`app1`/`app2`), 와일드카드 아님 |
|
||
| 체인 완결성 | **정상.** `Verify return code: 0 (ok)`, 4단계 |
|
||
| 갱신 자동화 | 타이머는 **동작한다.** 오늘 두 번 `status=0/SUCCESS` |
|
||
| **★ 갱신 반영** | **안 된다.** 새 인증서가 디스크에 있는 채로 **38분 25초** 동안 옛 인증서를 서빙했다 |
|
||
| **★ reload 무중단** | **무중단이다.** 8856건 전부 200 · 전송 한가운데였던 42초 요청이 845361바이트 전량 수신 |
|
||
|
||
### 한 문장으로
|
||
|
||
> **"갱신 성공"과 "새 인증서 서빙"은 다른 사건이다.**
|
||
> 그리고 이 실험대에는 앞의 사건을 뒤의 사건으로 잇는 것이 **아무것도 없다.**
|
||
|
||
이게 왜 무서운가 — 이 결함은 **88일 동안 보이지 않는다.** 타이머는 정상이고
|
||
매번 `SUCCESS` 로 끝난다. 만료 30일 전까지는 certbot 이 갱신 자체를 하지 않아
|
||
"반영이 안 된다"는 사실이 발현할 기회가 없기 때문이다. 발현하는 날의 증상은
|
||
**인증서 만료**이고, 그날에도 로그에는 `SUCCESS` 라고 적혀 있을 것이다.
|
||
|
||
---
|
||
|
||
## 1. 인증서 구성 — B-7 에서 실제로 걸린 제약
|
||
|
||
```
|
||
X509v3 Subject Alternative Name:
|
||
DNS:app1.hyeonworks.com, DNS:app2.hyeonworks.com, DNS:auth.hyeonworks.com
|
||
```
|
||
|
||
**세 이름뿐이고 와일드카드가 아니다.**
|
||
|
||
> **이 제약이 B-7 에서 실제 비용을 만들었다.**
|
||
> oauth2-proxy 를 올릴 호스트명이 없어 **Grafana 가 쓰던 `app2` 를 빌려야 했고**,
|
||
> 그 때문에 관측 스택의 웹 UI 가 실험 동안 내려가 있었다.
|
||
>
|
||
> **"인증서에 이름을 몇 개 넣을 것인가" 는 TLS 설정이 아니라
|
||
> 나중에 무엇을 배포할 수 있는가를 정하는 결정이다.**
|
||
|
||
| | 이 실험대 | 와일드카드였다면 |
|
||
|---|---|---|
|
||
| 새 호스트 추가 | **인증서 재발급 필요** | 바로 가능 |
|
||
| DNS-01 검증 | 필요 | 필요 (와일드카드는 DNS-01 만 가능) |
|
||
| 노출 | 이름 3개만 | **하위 전체가 한 키에 묶인다** |
|
||
|
||
---
|
||
|
||
## 2. 체인이 완전한가 — 흔한 실수 확인
|
||
|
||
```
|
||
0 s:CN = auth.hyeonworks.com ← 리프
|
||
1 s:C = US, O = Let's Encrypt, CN = YE2 ← 중간
|
||
2 s:C = US, O = ISRG, CN = Root YE
|
||
3 s:C = US, O = Internet Security Research Group, CN = ISRG Root X2
|
||
Verify return code: 0 (ok)
|
||
```
|
||
|
||
**중간 인증서가 함께 제공된다.**
|
||
|
||
### 개념 — `fullchain.pem` vs `cert.pem`
|
||
|
||
certbot 은 두 파일을 만든다.
|
||
|
||
| 파일 | 내용 | nginx 에 넣으면 |
|
||
|---|---|---|
|
||
| `cert.pem` | **리프만** | **일부 클라이언트에서 검증 실패** |
|
||
| **`fullchain.pem`** | 리프 + 중간 | 정상 |
|
||
|
||
**브라우저는 중간 인증서를 캐시하고 있어 `cert.pem` 으로도 대개 동작한다.**
|
||
그래서 실수해도 개발 중에는 안 드러나고, **캐시가 없는 클라이언트
|
||
(모바일 앱, curl, 다른 서버)에서만 깨진다.**
|
||
|
||
```bash
|
||
openssl s_client -connect <host>:443 -servername <host> | grep -E "^ *[0-9] s:"
|
||
```
|
||
|
||
**단계가 2개 이상이면 fullchain 이고, 1개면 cert.pem 을 쓴 것이다.**
|
||
이 실험대는 4단계로 정상이다.
|
||
|
||
---
|
||
|
||
## 3. 갱신 자동화는 동작한다
|
||
|
||
```
|
||
NEXT LEFT LAST PASSED
|
||
Fri 2026-09-04 17:03:46 KST 1h 54min Fri 2026-09-04 03:19:39 KST 11h ago
|
||
타이머 enabled: enabled / active: active
|
||
```
|
||
|
||
**하루 두 번 돌고, 11시간 전에 실제로 실행됐다.**
|
||
|
||
```
|
||
만료: Dec 2 00:47:22 2026 GMT
|
||
남은 일수: 88일
|
||
```
|
||
|
||
**아직 갱신하지 않은 것이 정상이다** — Let's Encrypt 는 90일 발급이고
|
||
certbot 은 **30일 남았을 때** 갱신한다. 지금 실행돼도 아무것도 안 한다.
|
||
|
||
> **타이머가 돌았다는 것과 갱신이 됐다는 것은 다르다.**
|
||
> "타이머가 active 니까 괜찮다" 는 확인이 아니다. **실제 갱신은 58일 뒤**이며,
|
||
> 그때 처음으로 절차가 시험된다.
|
||
|
||
---
|
||
|
||
## 4. ★ 강제 갱신 — 두 개의 답
|
||
|
||
### 4-1. 먼저 대조군을 잡았다
|
||
|
||
주입 전에 평시를 재둔다. **이걸 건너뛰면 주입 결과를 해석할 수 없다** —
|
||
A-6 에서 −41% 대조군을 "영향 없음"이라 적었던 것도, A-8 에서 표본 9개로
|
||
무중단을 주장한 것도 같은 종류의 실수였다.
|
||
|
||
| 대조군 | 결과 |
|
||
|---|---|
|
||
| 새 연결 (0.2초 × 900회 / 180초) | **900 전부 200, 오류 0** · 중앙 98ms · p95 195ms |
|
||
| 진행 중 요청 (845KB @ 20k/s) | 200 · 845361바이트 · 연결수 1 · **42.3초 완주** |
|
||
|
||
두 번째 장치가 왜 따로 필요했는가 — 첫 번째 폴링은 **TLS 핸드셰이크가
|
||
900/900** 이다. 매 요청이 새 연결이라는 뜻이고, 그래서 "새 연결을 받아주는가"만
|
||
잰다. 계획서가 물은 것은 **"진행 중이던 요청은 어떻게 되는가"** 이므로,
|
||
reload 순간에 실제로 전송 중인 요청이 있어야 한다. 845KB 짜리 관리 콘솔
|
||
번들을 일부러 느리게 받아 요청 하나를 42초 동안 살려 두었다.
|
||
|
||
### 4-2. 갱신은 성공했는데, 인증서가 바뀌지 않았다
|
||
|
||
`sudo certbot renew --force-renewal` 을 실행한 뒤에도 밖에서 본 인증서가
|
||
그대로였다. 처음에는 갱신이 실패한 줄 알았다. 아니었다.
|
||
|
||
```
|
||
디스크 (/etc/letsencrypt/archive/auth.hyeonworks.com/)
|
||
cert1.pem 2026-09-03 10:47:40 KST 처음 발급
|
||
cert2.pem 2026-09-04 17:22:13 KST ← 강제 갱신, 새로 써졌다
|
||
|
||
네트워크 (5초 간격 564표본)
|
||
08:10:51 ~ 08:58:47 serial=0520BB…B853 notAfter=Dec 2 ← 옛 것
|
||
08:58:52 serial=06C7CB…EA1D notAfter=Dec 3 ← 바뀐 순간
|
||
```
|
||
|
||
| | 시각 (UTC) |
|
||
|---|---|
|
||
| 새 인증서 디스크 기록 | 08:22:13 |
|
||
| 실제 서빙 시작 (`nginx -s reload`) | 08:58:52 |
|
||
| **공백** | **2305초 = 38분 25초** (시계 보정 후 — 아래 참조) |
|
||
|
||
> **★ 시계 보정 (D-4a 에서 발견)** — 처음 적은 **2199초(36분 39초)** 는
|
||
> `archive/` 파일 시각(test-server 시계)과 일련번호 관측(dev 시계)을 **그대로 뺀**
|
||
> 값이었다. 외부 기준(Google · Let's Encrypt `Date` 헤더)으로 재보니
|
||
> **dev 가 정확하고 test-server 가 106초 빠르다**(3회 측정 전부 +106.1초,
|
||
> `NTPSynchronized=no`). 보정하면 **2305초 = 38분 25초**다.
|
||
> 보정값은 D-4a 새 인증서의 SCT(`Sep 4 12:27:49.054 GMT`, CT 로그가 서명한
|
||
> 독립 시계)가 보정한 훅 시각의 1초 앞에 놓이는 것으로 교차 검증된다.
|
||
|
||
그 사이 **428번** 옛 인증서로 관측됐다. 그리고 이 36분은 **우연히 짧았을
|
||
뿐이다** — reload 를 시킨 것은 사람이지 자동화가 아니다. 아무도 안 했다면
|
||
다음 nginx 재시작까지, 즉 무기한 옛 인증서를 서빙했을 것이다.
|
||
|
||
#### 개념 — 왜 파일이 바뀌어도 nginx 는 모르는가
|
||
|
||
**무엇인가.** nginx 는 `ssl_certificate` 가 가리키는 파일을 **기동 시점에
|
||
한 번 읽어 메모리에 들고 있다.** 요청마다 디스크를 다시 보지 않는다.
|
||
|
||
**왜 여기 나오나.** certbot 은 `live/` 심볼릭 링크가 새 `archive/` 파일을
|
||
가리키도록 바꾼다. **경로는 그대로인데 내용만 바뀐다.** 그래서 nginx 설정을
|
||
고칠 필요가 없고, 바로 그 때문에 "설정이 그대로니 괜찮다"고 착각하기 쉽다.
|
||
필요한 것은 설정 변경이 아니라 **reload** 다.
|
||
|
||
**없거나 틀리면.** 인증서가 만료되어 브라우저가 `NET::ERR_CERT_DATE_INVALID`
|
||
를 띄운다. 그 시점에 디스크에는 멀쩡한 인증서가 들어 있고 갱신 로그도
|
||
`SUCCESS` 다 — 그래서 원인을 찾는 데 오래 걸린다.
|
||
|
||
**확인.** nginx 는 reload 하면 마스터를 유지한 채 워커만 새로 띄운다.
|
||
```bash
|
||
ps -eo pid,ppid,etimes,lstart,args | grep nginx
|
||
# 585 1 80529 Thu Sep 3 19:00:39 nginx: master process
|
||
# 586 585 80529 Thu Sep 3 19:00:39 nginx: worker process
|
||
# ↑ 워커가 마스터 기동 직후의 첫 fork(585→586) 그대로 22.4시간째
|
||
# = reload 가 한 번도 없었다
|
||
```
|
||
|
||
#### 원인은 하나가 아니라 셋이 겹쳤다
|
||
|
||
| # | 경로 | 상태 |
|
||
|---|---|---|
|
||
| 1 | `certbot-renew.service` 의 `ExecStartPost` | **없다** — `ExecStart=/usr/bin/certbot -q renew` 가 전부 |
|
||
| 2 | `/etc/letsencrypt/renewal-hooks/{deploy,post,pre}/` | **셋 다 비어 있다** (`total 8`, `.` 과 `..` 뿐) |
|
||
| 3 | certbot 의 nginx 플러그인 | **없다** — `dns-cloudflare, manual, null, standalone, webroot` |
|
||
|
||
**세 경로 전부가 비어 있다.** 하나라도 있었으면 자동으로 반영됐다.
|
||
certbot 5.7.0 에서 확인한 값이다.
|
||
|
||
#### 고치는 법
|
||
|
||
```bash
|
||
# 가장 단순한 방법 — deploy 훅 하나
|
||
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh <<'EOS'
|
||
#!/bin/sh
|
||
nginx -t && nginx -s reload
|
||
EOS
|
||
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
|
||
```
|
||
|
||
`deploy/` 는 **실제로 갱신된 인증서가 있을 때만** 실행된다. `post/` 는 갱신
|
||
여부와 무관하게 매번 돌므로, 하루 두 번 쓸데없이 reload 하게 된다.
|
||
|
||
**확인 방법** — 훅을 넣었다고 끝이 아니다. 다음 강제 갱신 때 `nginx` 워커
|
||
PID 가 바뀌는지 봐야 한다.
|
||
|
||
> **★ 검증됨 ([D-4a](experiment-d4a-deploy-hook.md))** — 이 처방을 실제로
|
||
> 넣고 강제 갱신했다. 워커가 `28829 → 37252` 로 자동 교체됐고,
|
||
> **발급에서 서빙까지 1~2초**였다. 다만 certbot 출력의
|
||
> `Hook 'deploy-hook' ran with error output` 은 **실패가 아니다** —
|
||
> nginx 의 경고가 stderr 로 나갔을 뿐이다. 판정은 문구가 아니라 워커 PID 로 한다.
|
||
|
||
### 4-3. 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 의 흔적이 아니다.
|
||
|
||
**진행 중이던 요청** — 여기가 계획서가 정확히 물은 지점이다.
|
||
|
||
```
|
||
08:58:40 요청 시작 (845KB @ 20k/s)
|
||
08:58:52 ← nginx -s reload. 요청 시작 12초 뒤, 전송 한가운데
|
||
08:59:21 종료: 코드=200 바이트=845361(전량) 연결수=1 curl종료=0
|
||
```
|
||
|
||
| 관측 | 읽는 법 |
|
||
|---|---|
|
||
| 바이트가 전량이다 | 잘리지 않았다 |
|
||
| **연결수가 1이다** | 중간에 끊겨 재연결한 게 아니다 |
|
||
| 코드 200 | 옛 워커가 이 요청을 끝까지 책임졌다 |
|
||
|
||
옛 인증서로 시작한 연결이 **새 워커 전환을 관통해** 끝까지 갔다.
|
||
in-flight 전체 50건 중 종료코드 ≠ 0 은 **0건**이다.
|
||
|
||
> 문서가 "nginx 의 reload 는 설계상 무중단이지만 **확인하지 않았으므로 그렇게
|
||
> 쓰면 안 된다**"고 유보했던 명제가, 이제 측정으로 뒷받침된다.
|
||
> **그리고 유보가 옳았다** — 같은 자리에서 유보 없이 썼다면 맞았겠지만,
|
||
> 바로 옆의 "갱신하면 반영된다"는 명제는 틀렸다. 어느 쪽이 틀릴지는 미리 알 수 없다.
|
||
|
||
### 4-4. 남은 미측정
|
||
|
||
| 항목 | 상태 |
|
||
|---|---|
|
||
| `certbot-renew.timer` 가 **실제 갱신**을 하는가 | **미측정.** 약 89일 뒤에야 조건이 성립한다(갱신으로 만료일이 밀렸다) |
|
||
| deploy 훅을 넣으면 자동 반영되는가 | **측정 완료 →** [D-4a](experiment-d4a-deploy-hook.md). **듣는다** — 발급에서 서빙까지 **1~2초**(여기 38분 25초의 약 1150분의 1) |
|
||
|
||
---
|
||
|
||
## 증거 파일
|
||
|
||
**증거 수집: 2026-09-04 · 감시 08:10:51 – 09:02 UTC** (파일 mtime 기준)
|
||
|
||
| 파일 | 종류 | 무엇을 보여주는가 |
|
||
|---|---|---|
|
||
| [`01-certificate-state.txt`](evidence/d4-certificate-renewal/01-certificate-state.txt) | 터미널 | SAN 3개 · 체인 4단계 `Verify return code: 0` |
|
||
| [`05-control-no-injection.txt`](evidence/d4-certificate-renewal/05-control-no-injection.txt) | 터미널 | **대조군 1** — 900/900 200, 오류 0 · 핸드셰이크 900/900 |
|
||
| [`06-inflight-control.txt`](evidence/d4-certificate-renewal/06-inflight-control.txt) | 터미널 | **대조군 2** — 42초 in-flight 장치, 845361바이트 완주 |
|
||
| [`07-renewal-hook-missing.txt`](evidence/d4-certificate-renewal/07-renewal-hook-missing.txt) | 터미널 | nginx 워커 PID 로 본 "reload 없음" · 유닛에 `ExecStartPost` 없음 · crt.sh 로는 못 가림 |
|
||
| [`08-inflight-artifact.txt`](evidence/d4-certificate-renewal/08-inflight-artifact.txt) | 터미널 | 76건 실패가 **로컬 아티팩트**임을 대조 폴링으로 가려낸 기록 |
|
||
| [`09-serial-timeline.txt`](evidence/d4-certificate-renewal/09-serial-timeline.txt) | 터미널 | 일련번호 564표본 — 08:58:52 에 바뀌는 순간 |
|
||
| [`10-reload-poll-window.txt`](evidence/d4-certificate-renewal/10-reload-poll-window.txt) | 터미널 | reload 전후 60초 새 연결 원문 (비200 0건) |
|
||
| [`11-inflight-full.txt`](evidence/d4-certificate-renewal/11-inflight-full.txt) | 터미널 | in-flight 전체 — reload 를 관통한 08:58:40 요청 포함 |
|
||
| [`12-certbot-state.txt`](evidence/d4-certificate-renewal/12-certbot-state.txt) | 터미널 | `certbot certificates` · `archive/` mtime · **훅 3개 디렉터리 전부 비었음** |
|
||
| [`13-verdict.txt`](evidence/d4-certificate-renewal/13-verdict.txt) | 터미널 | **판정** — 38분 25초 공백 + reload 무중단, 수치 전량 |
|
||
|
||
파일별 상세는 [`evidence/d4-certificate-renewal/README.md`](evidence/d4-certificate-renewal/README.md).
|
||
|
||
## 5. 재현 절차 (명령어)
|
||
|
||
```bash
|
||
# ─────────────────────────────────────────────────────────────
|
||
# 1. 인증서 내용 — 밖에서 볼 수 있다(sudo 불필요)
|
||
# ─────────────────────────────────────────────────────────────
|
||
for H in auth app1 app2; do
|
||
echo "-- $H.hyeonworks.com"
|
||
echo | openssl s_client -connect $H.hyeonworks.com:443 -servername $H.hyeonworks.com 2>/dev/null \
|
||
| openssl x509 -noout -serial -dates -subject -ext subjectAltName
|
||
done
|
||
# 세 이름의 일련번호가 같으면 → 인증서 1장에 SAN 3개다.
|
||
# B-7 이 네 번째 이름을 못 쓴 이유가 여기 있다.
|
||
|
||
# ─────────────────────────────────────────────────────────────
|
||
# 2. 체인 완결성 — 단계가 1개면 cert.pem 을 쓴 것이다
|
||
# ─────────────────────────────────────────────────────────────
|
||
echo | openssl s_client -connect auth.hyeonworks.com:443 -servername auth.hyeonworks.com 2>/dev/null \
|
||
| grep -E '^ *[0-9]+ s:|^ *i:|Verify return code'
|
||
|
||
# ─────────────────────────────────────────────────────────────
|
||
# 3. ★ 대조군을 먼저 잡는다 — 이걸 건너뛰면 주입 결과를 해석할 수 없다
|
||
# ─────────────────────────────────────────────────────────────
|
||
# 3-a. 새 연결 가용성: 0.2초 × 900회 = 180초
|
||
i=0
|
||
while [ $i -lt 900 ]; do
|
||
curl -s -o /dev/null -w '%{http_code} %{time_total} %{time_appconnect}\n' \
|
||
--max-time 5 https://auth.hyeonworks.com/realms/master
|
||
i=$((i+1)); sleep 0.2
|
||
done > /tmp/d4-control.txt
|
||
awk '{print $1}' /tmp/d4-control.txt | sort | uniq -c # 비200 이 몇 개인가
|
||
|
||
# 3-b. 진행 중 요청: 845KB 를 20k/s 로 받아 42초 동안 살려 둔다
|
||
JS=$(curl -s https://auth.hyeonworks.com/admin/master/console/ \
|
||
| grep -oE '/resources/[a-z0-9]+/admin/[^"]+\.js' | head -1)
|
||
curl -s --limit-rate 20k -o /tmp/inflight.bin \
|
||
-w '코드=%{http_code} 바이트=%{size_download} 시간=%{time_total} 연결수=%{num_connects}\n' \
|
||
"https://auth.hyeonworks.com$JS"
|
||
|
||
# ─────────────────────────────────────────────────────────────
|
||
# 4. 감시를 켠다 — 일련번호가 바뀌면 3분 더 재고 스스로 멈춘다
|
||
# ─────────────────────────────────────────────────────────────
|
||
BASE=$(echo | openssl s_client -connect auth.hyeonworks.com:443 \
|
||
-servername auth.hyeonworks.com 2>/dev/null | openssl x509 -noout -serial | cut -d= -f2)
|
||
rm -f /tmp/d4-stop
|
||
|
||
# 감시 0 — 일련번호. 바뀌는 순간이 nginx 가 새 인증서를 집은 순간이다
|
||
setsid bash -c '
|
||
BASE='"$BASE"'
|
||
while [ ! -f /tmp/d4-stop ]; do
|
||
S=$(echo | openssl s_client -connect auth.hyeonworks.com:443 \
|
||
-servername auth.hyeonworks.com 2>/dev/null | openssl x509 -noout -serial -enddate | tr "\n" " ")
|
||
echo "$(date -u +%H:%M:%S) $S"
|
||
case "$S" in *"$BASE"*) ;; *) sleep 180; touch /tmp/d4-stop;; esac
|
||
sleep 5
|
||
done' > /tmp/d4-serial.txt 2>&1 < /dev/null &
|
||
|
||
# 감시 1 — 새 연결
|
||
setsid bash -c '
|
||
while [ ! -f /tmp/d4-stop ]; do
|
||
echo "$(date -u +%H:%M:%S.%2N) $(curl -s -o /dev/null -w "%{http_code} %{time_total}" \
|
||
--max-time 5 https://auth.hyeonworks.com/realms/master)"
|
||
sleep 0.2
|
||
done' > /tmp/d4-poll.txt 2>&1 < /dev/null &
|
||
|
||
# 감시 2 — 진행 중 요청을 끊김 없이 연달아 (reload 순간에 반드시 하나가 떠 있게)
|
||
setsid bash -c '
|
||
while [ ! -f /tmp/d4-stop ]; do
|
||
echo "$(date -u +%H:%M:%S) $(curl -s --limit-rate 20k -o /dev/null \
|
||
-w "코드=%{http_code} 바이트=%{size_download} 시간=%{time_total} 연결수=%{num_connects}" \
|
||
https://auth.hyeonworks.com'"$JS"')"
|
||
done' > /tmp/d4-inflight.txt 2>&1 < /dev/null &
|
||
# ★ setsid 가 필요하다. 그냥 & 로 띄우면 부모 셸이 끝날 때 같이 죽는다
|
||
# (A-3 에서 파드 안 & 가 exec 종료와 함께 죽은 것과 같은 함정이다).
|
||
|
||
# ─────────────────────────────────────────────────────────────
|
||
# 5. 주입 — 여기만 sudo 가 필요하다
|
||
# ─────────────────────────────────────────────────────────────
|
||
ssh test-server
|
||
sudo certbot renew --force-renewal
|
||
|
||
# ─────────────────────────────────────────────────────────────
|
||
# 6. 판정
|
||
# ─────────────────────────────────────────────────────────────
|
||
grep -vE ' 200 ' /tmp/d4-poll.txt # 새 연결이 끊긴 순간 (대조군은 0건)
|
||
grep -v '코드=200' /tmp/d4-inflight.txt # 진행 중 요청이 끊긴 순간
|
||
grep -v "$BASE" /tmp/d4-serial.txt | head # 새 인증서가 서빙되기 시작한 시각
|
||
```
|
||
|
||
## 6. 남긴 것
|
||
|
||
| | |
|
||
|---|---|
|
||
| **강제 갱신 + 무중단 측정** | sudo 권한이 필요하다 |
|
||
| **SAN 확장** | 새 호스트를 쓰려면 재발급 — B-7 에서 실제로 걸렸다 |
|
||
| **D-3 과 연결** | 인증서 **개인키**도 같은 비밀 관리 문제다 |
|