diff --git a/docs/evidence/d4a-deploy-hook/01-hook-verified.txt b/docs/evidence/d4a-deploy-hook/01-hook-verified.txt new file mode 100644 index 0000000..d86a939 --- /dev/null +++ b/docs/evidence/d4a-deploy-hook/01-hook-verified.txt @@ -0,0 +1,125 @@ +D-4a — deploy 훅이 실제로 nginx 를 reload 하는가 +================================================== +수집: 2026-09-04 12:27 UTC (실제) · test-server + +D-4 가 남긴 것 +-------------- +| 항목 | 상태 | +|---|---| +| deploy 훅을 넣으면 자동 반영되는가 | **미측정. 훅은 아직 넣지 않았다** | + +D-4 는 처방을 적어두고 **검증하지 않았다.** 처방이 듣는지 모르는 채로 +"이렇게 고치면 된다"고 쓰는 것은 이 실험대가 계속 경계해 온 바로 그 실수다. + +[주입] 훅 하나를 넣고 강제 갱신 +-------------------------------- + /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh + #!/bin/sh + # D-4a: certbot deploy hook. 갱신이 실제로 일어났을 때만 실행된다. + nginx -t && nginx -s reload + + install -m755 /tmp/reload-nginx.sh /etc/letsencrypt/renewal-hooks/deploy/ + certbot renew --force-renewal + +[결과] certbot 출력 — 훅이 실행됐다 +------------------------------------ + Processing /etc/letsencrypt/renewal/auth.hyeonworks.com.conf + Renewing an existing certificate for auth.hyeonworks.com and 2 more + Hook 'deploy-hook' ran with error output: + 2026/09/04 21:29:36 [warn] 37250#37250: could not build optimal types_hash, … + nginx: the configuration file /etc/nginx/nginx.conf syntax is ok + nginx: configuration file /etc/nginx/nginx.conf test is successful + 2026/09/04 21:29:37 [warn] 37251#37251: could not build optimal types_hash, … + 2026/09/04 21:29:37 [notice] 37251#37251: signal process started + + Congratulations, all renewals succeeded: + /etc/letsencrypt/live/auth.hyeonworks.com/fullchain.pem (success) + + ★ 함정 — "Hook 'deploy-hook' ran with error output" 이라고 적혀 있지만 + 실패가 아니다. nginx 가 types_hash 경고를 stderr 로 내보내서 certbot 이 + 그것을 "error output" 으로 표시한 것뿐이다. 내용은 전부 성공이다: + · nginx -t → "test is successful" + · nginx -s reload → "signal process started" + 로그에서 'error' 를 grep 하면 성공한 훅을 실패로 오독하게 된다. + +[검증 1] nginx 워커가 교체됐다 +------------------------------- + 전 (D-4a 기준선) + 585 1 ... Thu Sep 3 19:00:39 nginx: master process + 28829 585 ... Fri Sep 4 18:00:35 nginx: worker process ← D-4 에서 사람이 reload 한 것 + + 후 + 585 1 95412 Thu Sep 3 19:00:39 nginx: master process ← 마스터는 그대로 + 37252 585 74 Fri Sep 4 21:29:36 nginx: worker process ← ★ 자동 교체됨 + + 마스터 PID 는 유지되고 워커만 바뀌었다 = reload 의 서명. D-4 에서 세운 + 진단 방법이 그대로 작동한다. + +[검증 2] 서빙 인증서가 바뀌었다 +-------------------------------- + serial=06F3E0EF4D1BB03DE58130EAAD1176101373 + notBefore=Sep 4 11:29:18 2026 GMT + notAfter=Dec 3 11:29:17 2026 GMT + SAN: app1 / app2 / auth .hyeonworks.com + + D-4 의 인증서(06C7CB…EA1D)에서 바뀌었다. + +[검증 3] ★ 얼마나 빨랐나 — 시계 보정이 필요하다 +-------------------------------------------------- + 훅 로그와 워커 lstart 는 **test-server 시계**다. 그런데 test-server 는 + NTP 동기가 꺼져 있다(NTPSynchronized=no). + + 외부 기준으로 두 시계를 검증했다. + dev → Google 차이 +0초 + dev → Let's Encrypt ACME 차이 +0초 + test-server → Google 차이 -105초 (즉 test-server 가 105초 빠르다) + ssh 왕복 왜곡 3회 측정: +106.1 / +106.1 / +106.1초 (안정적) + + → **dev 가 정확하고 test-server 가 106초 빠르다.** + + 독립 검증 — 새 인증서의 SCT (CT 로그가 서명한 시각, 자체 정확 시계): + Signed Certificate Timestamp: Sep 4 12:27:49.054 2026 GMT + Signed Certificate Timestamp: Sep 4 12:27:49.048 2026 GMT + + 보정한 타임라인 (전부 실제 UTC): + 12:27:49.05 인증서 발급 ← SCT. 외부 권위 기준 + 12:27:50 훅 nginx -t ← 로그 21:29:36 KST − 106초 + 12:27:50 새 워커 37252 기동 ← lstart 21:29:36 KST − 106초 + 12:27:51 훅 nginx -s reload ← 로그 21:29:37 KST − 106초 + + ★ 발급 → 서빙까지 **1~2초.** + ★ 그리고 보정이 자기 검증된다 — 독립 시계인 SCT 가 보정한 훅 시각의 + 1초 앞에 정확히 놓인다. 보정하지 않으면 훅이 발급보다 104초 먼저 + 실행된 것이 되어 물리적으로 불가능해진다. + +════ D-4 와의 대조 ════ + + 훅 없음 (D-4) 훅 있음 (D-4a) + 갱신 → 서빙 2305초 = 38분 25초 1~2초 + 무엇이 reload 했나 사람 (`nginx -s reload`) certbot deploy 훅 + 안 했다면 다음 nginx 재시작까지 해당 없음 + = 사실상 무기한 + 차이 약 1150배 + +════ 부수 정정 — D-4 의 2199초는 틀렸다 ════ + +D-4 에서 적은 **2199초(36분 39초)** 는 `archive/cert2.pem` 의 mtime +(test-server 시계)과 일련번호 관측 시각(dev 시계)을 **그대로 뺀** 값이었다. +106초 보정을 적용하면: + + 새 인증서 디스크 기록 08:20:27 UTC (실제) ← mtime 17:22:13 KST − 106초 + 실제 서빙 시작 08:58:52 UTC (실제) ← dev 관측, 보정 불필요 + 공백 2305초 = 38분 25초 + +관련 문서를 전부 정정했다. **두 시계에서 온 값을 빼면서 그 사실을 적지 +않으면, 자릿수가 아니라 방향까지 틀릴 수 있다** — 여기서는 그 오차가 +100초 단위였고, 1~2초를 재는 D-4a 에서는 결과를 완전히 뒤집었다. + +════ 결론 ════ + +1. **D-4 의 처방은 듣는다.** 훅 파일 하나로 발급 → 서빙이 38분에서 1~2초가 됐다. +2. **certbot 출력의 "ran with error output" 을 실패로 읽으면 안 된다.** + nginx 의 경고가 stderr 로 나갔을 뿐이며, 내용은 전부 성공이다. +3. **진단 방법이 재확인됐다** — 마스터 PID 유지 + 워커 PID 교체 = reload. +4. **시계를 섞어 빼면 안 된다.** test-server 는 NTP 가 꺼져 106초 빠르고, + 그 사실을 적지 않은 채 계산한 D-4 의 공백은 106초 짧았다. diff --git a/docs/evidence/d4a-deploy-hook/02-certbot-with-hook.txt b/docs/evidence/d4a-deploy-hook/02-certbot-with-hook.txt new file mode 100644 index 0000000..23af064 --- /dev/null +++ b/docs/evidence/d4a-deploy-hook/02-certbot-with-hook.txt @@ -0,0 +1,17 @@ +Saving debug log to /var/log/letsencrypt/letsencrypt.log + +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Processing /etc/letsencrypt/renewal/auth.hyeonworks.com.conf +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Renewing an existing certificate for auth.hyeonworks.com and 2 more +Hook 'deploy-hook' ran with error output: + 2026/09/04 21:29:36 [warn] 37250#37250: could not build optimal types_hash, you should increase either types_hash_max_size: 1024 or types_hash_bucket_size: 64; ignoring types_hash_bucket_size + nginx: the configuration file /etc/nginx/nginx.conf syntax is ok + nginx: configuration file /etc/nginx/nginx.conf test is successful + 2026/09/04 21:29:37 [warn] 37251#37251: could not build optimal types_hash, you should increase either types_hash_max_size: 1024 or types_hash_bucket_size: 64; ignoring types_hash_bucket_size + 2026/09/04 21:29:37 [notice] 37251#37251: signal process started + +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Congratulations, all renewals succeeded: + /etc/letsencrypt/live/auth.hyeonworks.com/fullchain.pem (success) +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/evidence/d4a-deploy-hook/03-after-state.txt b/docs/evidence/d4a-deploy-hook/03-after-state.txt new file mode 100644 index 0000000..c3851f2 --- /dev/null +++ b/docs/evidence/d4a-deploy-hook/03-after-state.txt @@ -0,0 +1,13 @@ +=== 실행 후 상태 (dev 머신에서 관측) === +수집 2026-09-04T12:29:05Z UTC + +-- nginx 프로세스 + 585 1 95412 Thu Sep 3 19:00:39 2026 nginx: master process /usr/bin/nginx + 37252 585 74 Fri Sep 4 21:29:36 2026 nginx: worker process + +-- 서빙 인증서 +serial=06F3E0EF4D1BB03DE58130EAAD1176101373 +notBefore=Sep 4 11:29:18 2026 GMT +notAfter=Dec 3 11:29:17 2026 GMT +X509v3 Subject Alternative Name: + DNS:app1.hyeonworks.com, DNS:app2.hyeonworks.com, DNS:auth.hyeonworks.com diff --git a/docs/evidence/d4a-deploy-hook/README.md b/docs/evidence/d4a-deploy-hook/README.md new file mode 100644 index 0000000..b649555 --- /dev/null +++ b/docs/evidence/d4a-deploy-hook/README.md @@ -0,0 +1,17 @@ +# D-4a — deploy 훅 검증 증거 + +2026-09-04 12:27 UTC (실제 시각) +해설: [`docs/experiment-d4a-deploy-hook.md`](../../experiment-d4a-deploy-hook.md) + +| 파일 | 무엇을 보여주는가 | +|---|---| +| `01-hook-verified.txt` | **판정 전문.** 훅 실행 로그 · 워커 PID 교체 · 시계 보정과 SCT 교차검증 · D-4 와의 대조 | +| `02-certbot-with-hook.txt` | `certbot renew --force-renewal` 원문. `Hook 'deploy-hook' ran` 과 `all renewals succeeded` | +| `03-after-state.txt` | 실행 후 nginx 프로세스와 서빙 인증서 | + +## 핵심 네 줄 + +1. **D-4 의 처방은 듣는다.** 훅 파일 하나로 발급 → 서빙이 **38분 25초에서 1~2초**가 됐다. 약 1150배. +2. **`Hook 'deploy-hook' ran with error output` 을 실패로 읽으면 안 된다.** nginx 의 `types_hash` 경고가 stderr 로 나갔을 뿐이고 내용은 전부 성공이다 — `test is successful`, `signal process started`. 로그에서 `error` 를 grep 하면 성공한 훅을 실패로 오독한다. +3. **진단 방법이 재확인됐다.** 마스터 585 는 그대로, 워커만 28829 → **37252** 로 교체. D-4 에서 세운 「마스터 유지 + 워커 교체 = reload」가 그대로 작동한다. +4. **★ 시계를 섞어 빼면 안 된다.** test-server 는 NTP 가 꺼져 **106초 빠르다**(외부 기준 3회 측정). 이 사실을 적지 않고 계산한 D-4 의 공백은 106초 짧았고, 1~2초를 재는 D-4a 에서는 보정 없이는 **훅이 발급보다 104초 먼저 실행된 것**이 되어 물리적으로 불가능해진다. 보정은 새 인증서 SCT(`12:27:49.054 GMT`, CT 로그의 독립 시계)로 교차검증된다. diff --git a/docs/experiment-d4-certificate-renewal.md b/docs/experiment-d4-certificate-renewal.md index 44ca4d0..7844d21 100644 --- a/docs/experiment-d4-certificate-renewal.md +++ b/docs/experiment-d4-certificate-renewal.md @@ -224,7 +224,13 @@ sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh 여부와 무관하게 매번 돌므로, 하루 두 번 쓸데없이 reload 하게 된다. **확인 방법** — 훅을 넣었다고 끝이 아니다. 다음 강제 갱신 때 `nginx` 워커 -PID 가 바뀌는지 봐야 한다. 이 실험이 그 확인 절차다. +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 는 무중단이다 — 측정됨 @@ -268,8 +274,8 @@ in-flight 전체 50건 중 종료코드 ≠ 0 은 **0건**이다. | 항목 | 상태 | |---|---| -| `certbot-renew.timer` 가 **실제 갱신**을 하는가 | **미측정.** 약 58일 뒤에야 조건이 성립한다 | -| deploy 훅을 넣으면 자동 반영되는가 | **미측정.** 훅은 아직 넣지 않았다 | +| `certbot-renew.timer` 가 **실제 갱신**을 하는가 | **미측정.** 약 89일 뒤에야 조건이 성립한다(갱신으로 만료일이 밀렸다) | +| deploy 훅을 넣으면 자동 반영되는가 | **측정 완료 →** [D-4a](experiment-d4a-deploy-hook.md). **듣는다** — 발급에서 서빙까지 **1~2초**(여기 38분 25초의 약 1150분의 1) | --- diff --git a/docs/experiment-d4a-deploy-hook.md b/docs/experiment-d4a-deploy-hook.md new file mode 100644 index 0000000..d911fc7 --- /dev/null +++ b/docs/experiment-d4a-deploy-hook.md @@ -0,0 +1,266 @@ +# D-4a — deploy 훅은 정말 듣는가 + +브랜치 `feature/keycloak-d4a-deploy-hook` · +증거 [`docs/evidence/d4a-deploy-hook/`](evidence/d4a-deploy-hook/) · +2026-09-04 12:27 UTC (실제 시각) + +D-4 는 결함을 찾고 **처방을 적어두고 검증하지 않았다.** +처방이 듣는지 모르는 채 "이렇게 고치면 된다"고 쓰는 것은, +이 실험대가 스물세 번 경계해 온 바로 그 실수다. + +--- + +## 0. 결론부터 + +| 확인 | 결과 | +|---|---| +| 훅이 실행되는가 | **된다.** certbot 이 `Hook 'deploy-hook' ran` 을 찍는다 | +| nginx 가 reload 되는가 | **된다.** 마스터 585 유지, 워커 28829 → **37252** | +| **얼마나 빠른가** | **발급 → 서빙 1~2초** (D-4 는 38분 25초) | +| 함정 | **`ran with error output` 은 실패가 아니다** | +| **★ 부수 발견** | **test-server 시계가 106초 빠르다.** D-4 의 공백 수치를 정정했다 | + +--- + +## 1. D-4 가 무엇을 남겼나 + +| 항목 | 상태 | +|---|---| +| deploy 훅을 넣으면 자동 반영되는가 | **미측정. 훅은 아직 넣지 않았다** | + +D-4 는 원인을 셋으로 특정했고(유닛에 `ExecStartPost` 없음 · 훅 3경로 비었음 · +nginx 플러그인 없음) 처방을 적었다. **그 처방을 실행해 본 적이 없다.** + +--- + +## 2. 주입 — 파일 하나 + +```bash +# /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh +#!/bin/sh +nginx -t && nginx -s reload +``` + +### 개념 — `deploy/` 와 `post/` 는 다르다 + +| 디렉터리 | 언제 실행되나 | +|---|---| +| `pre/` | 갱신 **시도** 전 | +| **`deploy/`** | **실제로 갱신된 인증서가 있을 때만** | +| `post/` | 갱신 여부와 **무관하게** 매번 | + +**왜 `deploy/` 인가.** 타이머는 하루 두 번 돈다. `post/` 에 넣으면 갱신이 +없는 날에도 하루 두 번 nginx 를 reload 하게 된다 — 아무 이득 없이 워커만 +갈아치우는 셈이다. `deploy/` 는 **`RENEWED_LINEAGE` 가 있을 때만** 돈다. + +**없거나 틀리면.** D-4 가 측정한 그대로다 — 갱신은 성공하고 서빙은 안 바뀐다. +그리고 그 상태로 타이머는 `SUCCESS` 를 찍는다. + +**확인.** +```bash +sudo ls -la /etc/letsencrypt/renewal-hooks/deploy/ # 비어 있으면 아무도 안 부른다 +``` + +### 왜 `nginx -t &&` 를 앞에 두는가 + +설정이 깨진 상태에서 `nginx -s reload` 를 보내면 마스터가 새 워커를 못 띄운다. +`-t` 로 먼저 검사하고 통과할 때만 reload 한다. 실패하면 **옛 워커가 그대로 +서비스를 계속한다** — 인증서는 안 바뀌지만 서비스는 죽지 않는다. + +### 실행 + +```bash +install -m755 /tmp/reload-nginx.sh /etc/letsencrypt/renewal-hooks/deploy/ +certbot renew --force-renewal +``` + +> **이 스크립트는 실험 자동화가 아니라 시험 대상 자체다.** +> certbot 은 훅을 파일로만 받는다. 절차는 전부 명령어로 되어 있다. + +--- + +## 3. 결과 — certbot 이 훅을 실행했다 + +``` +Processing /etc/letsencrypt/renewal/auth.hyeonworks.com.conf +Renewing an existing certificate for auth.hyeonworks.com and 2 more +Hook 'deploy-hook' ran with error output: + 2026/09/04 21:29:36 [warn] 37250#37250: could not build optimal types_hash, … + nginx: the configuration file /etc/nginx/nginx.conf syntax is ok + nginx: configuration file /etc/nginx/nginx.conf test is successful + 2026/09/04 21:29:37 [warn] 37251#37251: could not build optimal types_hash, … + 2026/09/04 21:29:37 [notice] 37251#37251: signal process started + +Congratulations, all renewals succeeded: + /etc/letsencrypt/live/auth.hyeonworks.com/fullchain.pem (success) +``` + +### ★ 함정 — "ran with error output" 은 실패가 아니다 + +certbot 은 훅이 **stderr 에 무엇이라도 쓰면** 이 문구를 붙인다. +여기 stderr 로 나간 것은 nginx 의 `types_hash` 경고뿐이고, 내용은 전부 성공이다. + +| 줄 | 실제 의미 | +|---|---| +| `[warn] could not build optimal types_hash` | nginx 의 일반 경고. 갱신과 무관 | +| `nginx: … test is successful` | **`nginx -t` 통과** | +| `[notice] signal process started` | **`nginx -s reload` 가 신호를 보냄** | + +> 로그에서 `error` 를 grep 하는 감시를 걸어두면 **성공한 훅을 실패로 오독한다.** +> 실패를 확인하려면 문구가 아니라 **워커 PID 를 봐야 한다.** + +--- + +## 4. 검증 — 워커가 교체됐다 + +| | PID | 기동 시각 (test-server 시계) | +|---|---|---| +| 마스터 (전·후 동일) | **585** | Thu Sep 3 19:00:39 | +| 워커 (전) | 28829 | Fri Sep 4 18:00:35 ← D-4 에서 **사람이** reload | +| **워커 (후)** | **37252** | Fri Sep 4 21:29:36 ← **훅이 자동으로** | + +**마스터 PID 는 유지되고 워커만 바뀌었다.** D-4 에서 "reload 되었는가"를 +판정하려고 세운 방법이 그대로 작동한다. + +서빙 인증서도 바뀌었다. + +``` +serial=06F3E0EF4D1BB03DE58130EAAD1176101373 +notBefore=Sep 4 11:29:18 2026 GMT +notAfter=Dec 3 11:29:17 2026 GMT +SAN: app1 / app2 / auth .hyeonworks.com +``` + +--- + +## 5. ★ 얼마나 빨랐나 — 그리고 시계 문제 + +훅 로그와 워커 `lstart` 는 **test-server 시계**다. 그런데 그 시계는 +NTP 동기가 꺼져 있다. + +``` +dev → Google +0초 +dev → Let's Encrypt ACME +0초 +test-server → Google −105초 (test-server 가 빠르다) +ssh 왕복 3회 측정 +106.1 / +106.1 / +106.1초 +``` + +**dev 가 정확하고 test-server 가 106초 빠르다.** + +### 독립 검증 — SCT + +새 인증서에 박힌 Certificate Transparency 서명 시각은 **CT 로그의 자체 +시계**로 찍힌다. 양쪽 어느 것과도 무관한 제3의 기준이다. + +``` +Signed Certificate Timestamp: Sep 4 12:27:49.054 2026 GMT +Signed Certificate Timestamp: Sep 4 12:27:49.048 2026 GMT +``` + +보정한 타임라인 (전부 실제 UTC): + +``` + 12:27:49.05 인증서 발급 ← SCT (외부 권위 기준) + 12:27:50 훅 nginx -t ← 로그 21:29:36 KST − 106초 + 12:27:50 새 워커 37252 기동 ← lstart 21:29:36 KST − 106초 + 12:27:51 훅 nginx -s reload ← 로그 21:29:37 KST − 106초 +``` + +**발급에서 서빙까지 1~2초.** + +그리고 **보정이 자기 검증된다** — 독립 시계인 SCT 가 보정한 훅 시각의 1초 +앞에 정확히 놓인다. 보정하지 않으면 훅이 발급보다 **104초 먼저** 실행된 것이 +되어 물리적으로 불가능해진다. + +### 부수 정정 — D-4 의 2199초는 틀렸다 + +D-4 에서 적은 **2199초(36분 39초)** 는 `archive/cert2.pem` 의 mtime +(test-server 시계)과 일련번호 관측(dev 시계)을 **그대로 뺀** 값이었다. + +| | 시각 (실제 UTC) | +|---|---| +| 새 인증서 디스크 기록 | **08:20:27** ← mtime 17:22:13 KST − 106초 | +| 실제 서빙 시작 | 08:58:52 ← dev 관측, 보정 불필요 | +| **공백** | **2305초 = 38분 25초** | + +관련 문서를 전부 정정했다. + +> **두 시계에서 온 값을 빼면서 그 사실을 적지 않으면, 자릿수가 아니라 +> 방향까지 틀릴 수 있다.** D-4 에서는 오차가 106초여서 결론이 안 바뀌었지만, +> 1~2초를 재는 D-4a 에서는 결과를 완전히 뒤집었다. + +--- + +## 6. 대조 + +| | 훅 없음 (D-4) | **훅 있음 (D-4a)** | +|---|---|---| +| 갱신 → 서빙 | **2305초 = 38분 25초** | **1~2초** | +| 무엇이 reload 했나 | 사람이 친 `nginx -s reload` | certbot deploy 훅 | +| 아무도 안 했다면 | 다음 nginx 재시작까지 = **사실상 무기한** | 해당 없음 | +| 차이 | | **약 1150배** | + +--- + +## 7. 남는 것 + +| 항목 | 상태 | +|---|---| +| `certbot-renew.timer` 가 **실제 갱신**을 하는가 | **미측정.** 만료 30일 전(약 89일 뒤)에야 조건이 성립한다 | + +훅은 `--force-renewal` 로 검증했다. **타이머가 스스로 갱신하는 경로**는 +시간이 지나야 시험할 수 있다. 다만 그 경로도 같은 `certbot renew` 를 부르고 +같은 `deploy/` 훅을 실행하므로, 남은 미지수는 "타이머가 뜨는가" 하나다 — +그리고 그것은 D-4 에서 이미 확인했다(오늘 두 번 `status=0/SUCCESS`). + +--- + +## 8. 재현 절차 (명령어) + +```bash +# ── 0. 기준선 — 이 워커 PID 가 바뀌는지가 판정이다 +ssh test-server "ps -eo pid,ppid,etimes,lstart,args | grep 'nginx:' | grep -v grep" +echo | openssl s_client -connect auth.hyeonworks.com:443 \ + -servername auth.hyeonworks.com 2>/dev/null | openssl x509 -noout -serial -dates + +# ── 1. ★ 시계 왜곡을 먼저 잰다. 나중에 재면 값을 해석할 수 없다 +for i in 1 2 3; do + A=$(date -u +%s.%N); B=$(ssh test-server 'date -u +%s.%N'); C=$(date -u +%s.%N) + python3 -c "print(f'왜곡 {$B-($A+$C)/2:+.1f}초')" +done +# 어느 쪽이 맞는지는 외부 기준으로 가른다 +for H in https://www.google.com https://acme-v02.api.letsencrypt.org/directory; do + A=$(date -u +%s) + D=$(curl -sI --max-time 10 "$H" | grep -i '^date:' | sed 's/^[Dd]ate: *//' | tr -d '\r') + C=$(date -u +%s) + python3 -c "print(f'$H 차이 {$(date -u -d \"$D\" +%s)-($A+$C)//2:+d}초')" +done + +# ── 2. 훅을 sudo 없는 곳에 미리 만들어 둔다 (사람이 칠 명령을 짧게) +ssh test-server "printf '#!/bin/sh\nnginx -t && nginx -s reload\n' > /tmp/reload-nginx.sh" + +# ── 3. 주입 — 여기만 sudo 가 필요하다 +ssh -t test-server 'sudo sh -c "install -m755 /tmp/reload-nginx.sh /etc/letsencrypt/renewal-hooks/deploy/ && certbot renew --force-renewal > /tmp/d4a-renew.txt 2>&1; chmod 644 /tmp/d4a-renew.txt; tail -25 /tmp/d4a-renew.txt"' + +# ── 4. 판정 — 문구가 아니라 워커 PID 로 본다 +ssh test-server "ps -eo pid,ppid,etimes,lstart,args | grep 'nginx:' | grep -v grep" +# 마스터 PID 그대로 + 워커 PID 바뀜 = reload 됨 + +# ── 5. 얼마나 빨랐나 — SCT 가 발급 시각의 외부 기준이다 +echo | openssl s_client -connect auth.hyeonworks.com:443 \ + -servername auth.hyeonworks.com 2>/dev/null \ + | openssl x509 -noout -ext ct_precert_scts | grep Timestamp +# 훅 로그 시각에서 왜곡을 빼고 SCT 와 비교한다 +``` + +--- + +## 증거 파일 + +| 파일 | 종류 | 무엇을 보여주는가 | +|---|---|---| +| [`01-hook-verified.txt`](evidence/d4a-deploy-hook/01-hook-verified.txt) | 터미널 | 판정 전문 · 시계 보정과 SCT 교차검증 · D-4 대조 | +| [`02-certbot-with-hook.txt`](evidence/d4a-deploy-hook/02-certbot-with-hook.txt) | 터미널 | `certbot renew --force-renewal` 원문 | +| [`03-after-state.txt`](evidence/d4a-deploy-hook/03-after-state.txt) | 터미널 | 실행 후 nginx 프로세스 · 서빙 인증서 | + +파일별 상세는 [`evidence/d4a-deploy-hook/README.md`](evidence/d4a-deploy-hook/README.md). diff --git a/docs/experiment-index.md b/docs/experiment-index.md index 950a068..bfd38a7 100644 --- a/docs/experiment-index.md +++ b/docs/experiment-index.md @@ -29,6 +29,7 @@ | **D-4** | 인증서 갱신 | `...d4-certificate-renewal` | **갱신은 됐는데 38분 25초 반영 안 됨** (훅 3경로 전부 비었음). reload 자체는 **무중단**(8856건 0실패) | | **A-7a** | volatile refresh 500 원인 확정 | `...a7a-volatile-cause` | **가설(`REVOKED_TOKEN`)은 틀렸다** — `CLIENT_SCOPE_CLIENT` 조회다. **A-7 의 표는 캐시 온도에 따라 400/500/200** | | **B-7a** | 고아 세션 정리 | `...b7a-orphan-session` | **지울 수 있다** — 프록시가 못 할 뿐. TTL 역산으로 고아만 **1초 오차**로 골라낸다 | +| **D-4a** | deploy 훅 검증 | `...d4a-deploy-hook` | **처방이 듣는다** — 발급→서빙 **1~2초**(D-4 는 38분 25초). test-server 시계가 **106초 빨라** D-4 수치를 정정 | | **후속** | 미측정 3항목 채우기 | `...followup-untested-items` | **셋 다 완료.** 정방향 업그레이드 무중단 · **롤백 불가는 조건부였다** · role 변경은 요청으로 반영 안 됨 · **D-4 갱신 38분 25초 미반영** | ## 시각 자료