diff --git a/docs/evidence/d4-certificate-renewal/01-certificate-state.txt b/docs/evidence/d4-certificate-renewal/01-certificate-state.txt new file mode 100644 index 0000000..1ac1b93 --- /dev/null +++ b/docs/evidence/d4-certificate-renewal/01-certificate-state.txt @@ -0,0 +1,38 @@ +=== 현재 인증서 (외부 관측, sudo 불필요) === +subject=CN = auth.hyeonworks.com +issuer=C = US, O = Let's Encrypt, CN = YE2 +notBefore=Sep 3 00:47:23 2026 GMT +notAfter=Dec 2 00:47:22 2026 GMT +X509v3 Subject Alternative Name: + DNS:app1.hyeonworks.com, DNS:app2.hyeonworks.com, DNS:auth.hyeonworks.com + + → 세 호스트가 같은 인증서를 쓴다 (SAN 3개, 와일드카드 아님) + +=== 체인 완결성 (fullchain vs cert 실수 확인) === + 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 이 올바로 설정되어 있다. + +=== 갱신 자동화 === +NEXT LEFT LAST PASSED UNIT +Fri 2026-09-04 17:03:46 KST 1h 54min Fri 2026-09-04 03:19:39 KST 11h ago certbot-renew.timer +타이머 enabled: enabled +타이머 active: active + +=== 남은 기간 === +만료: Dec 2 00:47:22 2026 GMT +남은 일수: 88일 +Let's Encrypt 90일 발급 · 30일 남으면 갱신 → 실제 갱신까지 약 58일 + +=== 강제 갱신은 하지 못했다 === +$ sudo -n -l +sudo: a password is required +$ sudo -n systemctl reload nginx +sudo: a password is required + + → test-server 의 sudo 는 비밀번호를 요구한다 (게스트 kc-lab-1/2 는 무암호). + certbot renew --force-renewal 도 nginx reload 도 실행할 수 없다. diff --git a/docs/evidence/d4-certificate-renewal/README.md b/docs/evidence/d4-certificate-renewal/README.md new file mode 100644 index 0000000..817a482 --- /dev/null +++ b/docs/evidence/d4-certificate-renewal/README.md @@ -0,0 +1,14 @@ +# D-4 — 인증서 갱신 증거 + +2026-09-04 17:25–17:35 KST +해설: [`docs/experiment-d4-certificate-renewal.md`](../../experiment-d4-certificate-renewal.md) + +| 파일 | 무엇을 보여주는가 | +|---|---| +| `01-certificate-state.txt` | SAN 3개(와일드카드 아님) · **체인 4단계, `Verify return code: 0`** · `certbot-renew.timer` enabled·active, 11시간 전 실행 · 88일 남음 · **`sudo: a password is required` 로 강제 갱신 불가** | + +## 핵심 세 줄 + +1. **인증서가 이름 3개만 담는다.** B-7 에서 oauth2-proxy 를 올릴 호스트가 없어 Grafana 의 `app2` 를 빌려야 했던 실제 비용이 여기서 나왔다. +2. **체인이 완전하다** — 단계가 4개이므로 `fullchain.pem` 을 쓰고 있다. 1개면 `cert.pem` 실수이며 캐시 없는 클라이언트에서만 깨진다. +3. **강제 갱신은 못 했다.** 호스트 sudo 가 비밀번호를 요구한다. 타이머가 active 라는 것은 "갱신이 된다"의 확인이 아니다. diff --git a/docs/experiment-d4-certificate-renewal.md b/docs/experiment-d4-certificate-renewal.md new file mode 100644 index 0000000..65aecbc --- /dev/null +++ b/docs/experiment-d4-certificate-renewal.md @@ -0,0 +1,182 @@ +# D-4 — 인증서 갱신 + +브랜치 `feature/keycloak-d4-certificate-renewal` · +증거 [`docs/evidence/d4-certificate-renewal/`](evidence/d4-certificate-renewal/) · +2026-09-04 17:25–17:35 KST + +--- + +## 0. 결론부터 + +| 확인 | 결과 | +|---|---| +| 인증서 구성 | **SAN 3개** (`auth`/`app1`/`app2`), 와일드카드 아님 | +| 체인 완결성 | **정상.** `Verify return code: 0 (ok)`, 4단계 | +| 갱신 자동화 | **동작 중.** `certbot-renew.timer` enabled·active, 11시간 전 실행됨 | +| 남은 기간 | **88일** (갱신까지 약 58일) | +| **강제 갱신 실측** | **★ 못 했다.** `sudo: a password is required` | + +**측정한 것과 못 한 것을 나눠 적는다.** 못 한 것을 안 한 것처럼 쓰면 +이 기록 전체의 신뢰가 깎인다. + +--- + +## 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 :443 -servername | 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. ★ 못 한 것 — 강제 갱신과 무중단 확인 + +계획서의 D-4 는 이렇게 적혀 있었다. + +```bash +sudo certbot renew --force-renewal +``` + +**실행할 수 없었다.** + +``` +$ sudo -n -l +sudo: a password is required +$ sudo -n systemctl reload nginx +sudo: a password is required +``` + +**test-server 의 sudo 는 비밀번호를 요구한다.** 게스트(kc-lab-1/2)는 무암호라 +A층에서 `conntrack`·`tc` 를 자유롭게 썼는데, **호스트는 다르다.** + +> **이 사실은 B-7 에서 처음 드러났다** — nginx 설정을 읽으려던 시도가 계속 +> 빈 결과였고, 그게 **sudo 의 조용한 실패**였다. 여기서 다시 확인된다. + +### 그래서 답하지 못한 것 + +| 계획서의 항목 | 상태 | +|---|---| +| nginx reload 타이밍에 무중단인가 | **미측정** | +| 갱신 중 진행 중이던 요청은 | **미측정** | +| `certbot-renew.timer` 가 실제 갱신을 하는가 | **미측정** (58일 뒤에야 알 수 있다) | + +### 이론적으로는 무엇을 기대하는가 + +``` + certbot renew → 새 인증서 파일 저장 + └─ deploy-hook: nginx -s reload + └─ nginx 는 새 워커를 띄우고 옛 워커는 진행 중 요청을 끝낸 뒤 종료 + → graceful. 진행 중 요청은 옛 인증서로 완결된다 +``` + +**nginx 의 reload 는 설계상 무중단**이지만, **확인하지 않았으므로 그렇게 +쓰면 안 된다.** 이 실험대에서 반복해 배운 것이 바로 그것이다 — +A-1 의 NetworkPolicy, A-3 의 `--grace-period=0`, B-5 의 AOF 모두 +**"그럴 것이다" 가 틀렸던 사례**다. + +--- + +## 5. 재현 절차 (명령어) + +```bash +# 1. 인증서 내용 — 밖에서 볼 수 있다 +echo | openssl s_client -connect auth.hyeonworks.com:443 -servername auth.hyeonworks.com 2>/dev/null \ + | openssl x509 -noout -subject -issuer -dates -ext subjectAltName + +# 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:|Verify return code" + +# 3. 갱신 자동화 +systemctl list-timers certbot-renew.timer --no-pager +systemctl is-enabled certbot-renew.timer + +# 4. 강제 갱신 (sudo 필요 — 이 실험대에서는 불가) +sudo certbot renew --force-renewal +# 갱신 중 다른 창에서: +# while true; do curl -s -o /dev/null -w '%{http_code} ' https://auth.hyeonworks.com/realms/master; sleep 1; done +``` + +--- + +## 6. 남긴 것 + +| | | +|---|---| +| **강제 갱신 + 무중단 측정** | sudo 권한이 필요하다 | +| **SAN 확장** | 새 호스트를 쓰려면 재발급 — B-7 에서 실제로 걸렸다 | +| **D-3 과 연결** | 인증서 **개인키**도 같은 비밀 관리 문제다 |