기록 84편을 계약 에이전트로 다시 썼다. 기존 71편(kss 25 · virt 46)과, 계약에만 있고 안 쓰여 있던 새 글감 13편이다. 원장 84개를 열어 단계마다 스킬 영수증과 관문 종료 코드를 적었고 verify-pipeline-run.py 가 error 0 으로 닫는다. SSOT 결함 둘을 고쳤다. - kss 의 `약 58일` 이 반입 중 `약 59일` 로 바뀌어 있었다. 원 증거 파일이 「남은 일수: 88일 … 실제 갱신까지 약 58일」로 산수를 직접 적는다. D-4a 쪽 `약 59일` 은 강제 갱신 뒤(`VALID: 89 days`)라 맞는 값이라 그대로 뒀다. - virt §198 의 `11.6GB` 는 §178 의 원 측정 `Mem: 11648`(MiB)과 어긋나는데 원 가이드의 표기 그대로라 고치지 않고 쓰이는 자리에 대조를 적었다. 기록의 수치 오류 셋을 고쳤다 — CASE 요약의 「게스트 셋에 8240MB」(5120+3120 은 둘이다), k3s 편이 같은 것을 여섯·일곱·여덟로 세던 것, no-docker 편의 「셋을 더 든다」(§281 의 표는 네 행이고 디스크 행이 빠져 있었다). 계약을 셋 고쳤다. - kss 의 sourceRepository 리비전이 cdac9b8 이었는데 그 커밋에는 docs/guides/** 28개가 아예 없다. 9465582b 로 바꾸고, 반입한 바이트가 어느 커밋과도 같지 않다는 것을 측정값과 함께 적었다 — 반입은 커밋이 아니라 그 시점의 작업 트리에서 떠 온 것이다(kss 297/306 · virt 12/14 가 작업 트리와 같고, 200 커밋을 거슬러 전수 대조했을 때 가장 가까운 커밋도 28개가 어긋났다). - virt 계약이 「2026-09-11 재배분」이라고 적는데 SSOT 는 재배분 날짜를 적지 않고 재배분 뒤 값은 이미 2026-09-10 측정에 찍혀 있다. - kss 후보 대장이 지나친 절 아홉에 처분을 적었다(warn 9 → 0). 새 글감은 0건이고 넷은 앵커가 h3 슬러그의 접두가 아니라 중간 토막이라 검사기가 못 본 것이었다. style_profile.mjs 의 결함 둘을 고쳤다 — frontmatter 가 문장으로 세어져 (실측 398자짜리 「문장」 하나) 평균 길이를 기준 안으로 밀어 올리고 있었고, engPerSent 의 분자는 목록을 포함한 글에서, 분모는 목록을 걷어낸 글에서 세고 있었다(Question 기록에서 11.94 → 3.86). verify-pipeline.py 전 항목 PASS · error 0 · unittest 334건 OK. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8.0 KiB
kind, slug, title, topic, topicName, project, status, decisionStatus, sourceRevision, source
| kind | slug | title | topic | topicName | project | status | decisionStatus | sourceRevision | source | ||
|---|---|---|---|---|---|---|---|---|---|---|---|
| PROJECT_DECISION | dns-01-because-the-lab-is-not-on-the-public-internet | 인증서는 DNS-01 로 받는다 — 실험대 주소가 공개 인터넷에 없어 HTTP-01 이 성립하지 않는다 | lab-environment-build | 실험대 환경 구성 | virtualization | 게시 전 | ADOPTED | 9465582b5d1630eb4ae7c4e078021486919bf6b6 |
|
인증서는 DNS-01 로 받는다 — 실험대 주소가 공개 인터넷에 없어 HTTP-01 이 성립하지 않는다
인증서는 Let's Encrypt 의 DNS-01 로 받는다. HTTP-01 은 80 포트와 공개 A 레코드를 요구하는데 이 실험대의 주소는 공개 인터넷에 없고, certbot 이 DNS 공급자 API 로 나가는 DNS-01 만 성립한다. Cloudflare API 토큰이 엣지 VM 안 평문 파일에 놓이는 비용을 감수한다.
근거
- 갱신은 매번 SUCCESS 였고 옛 인증서가 계속 나갔다 — 2305초와 1~2초 이 결정으로 받은 인증서가 갱신 뒤에 실제로 서빙되는지는 이 결정 밖이고 그 기록이 받는다. 발급 방식을 정하는 것과 갱신된 것을 nginx 가 읽게 만드는 것은 다른 일이다.
- 엣지 nginx 를 호스트에서 게스트 VM 으로 옮긴다 — 성능이 아니라 더러워지는 층의 격리 그 결정이 새로 요구한 일곱 가지 가운데 7번이 certbot 과 인증서와 갱신 훅을 게스트로 옮기는 것이었다. 이 결정이 세우는 것을 전부 엣지 게스트에 두는 이유가 거기서 왔다.
- 도구가 낸 출력은 대상의 상태가 아니다 — 그 명령이 무엇을 세는지부터 가른다 체인이 이어졌는지를 openssl s_client 의 단계 수로 판정하는 근거다. cert.pem 을 써서 체인이 끊겨도 브라우저는 캐시나 AIA 로 보완해 정상으로 보인다.
- 가까운 층부터 한 층씩 건너뛰며 확인한다 — 층마다 성공 신호가 다르다 04 단계의 확인을 어느 기계에서 치는지가 그 기준에 걸려 있다. 엣지 게스트 안에서 치면 층의 답이 아니라 친 위치의 답이 돌아온다.
- 단계별 구축 문서는 작성 시점이 아니라 실행 순서로 검증한다 이 결정이 남긴 이름 규칙을 가이드가 틀리게 적었고, §182 가 그것을 결함 여섯 중 하나로 셌다. 결함 여섯을 나란히 적은 표는 그 기록에 있다.
결정문
인증서는 Let's Encrypt 의 DNS-01 로 받는다. 엣지 게스트에 certbot 과 python3-certbot-dns-cloudflare 를 깐다. Cloudflare API 토큰은 /etc/letsencrypt/cloudflare.ini 에 600 으로 두고, 발급 대상은 hyeonworks.com 과 그 와일드카드 둘을 -d 로 준다.
certbot 과 인증서와 갱신 타이머와 deploy 훅은 전부 엣지 게스트에 두고 물리 호스트에는 아무것도 두지 않는다.
판단 이유
두 방식은 검증이 오가는 방향이 반대다. HTTP-01 은 Let's Encrypt 가 우리 서버로 들어오는 인바운드 검증이라 80 포트와 공개 A 레코드가 있어야 한다. DNS-01 은 certbot 이 DNS 공급자 API 로 나가는 아웃바운드 검증이라 공개 인터넷에서 보일 필요가 없다. 이 실험대의 주소는 공개 인터넷에 없으므로 HTTP-01 은 성립하지 않는다.
§190 는 이 선택을 우열로 적지 않는다. 공개 서버라면 HTTP-01 이 맞고, 토큰도 DNS 연동도 없어 관리할 것이 적다고 대안 쪽을 먼저 적는다. 이 결정은 더 나은 방식을 고른 것이 아니라 하나만 성립하는 조건에서 그것을 쓴 것이다. 딸려 온 이득이 하나 있는데, DNS-01 은 와일드카드를 받을 수 있어 hyeonworks.com 아래의 이름을 인증서 한 장으로 덮는다. 그 이득은 뒤늦게 챙겼다. 이 실험대는 처음에 와일드카드를 안 쓰고 이름마다 따로 받았고, 네 번째 이름이 없어 다른 실험에서 app2 를 빌려 써야 했다.
정한 대로 이미 서 있다. 엣지 게스트에는 certbot 과 python3-certbot-dns-cloudflare 가 깔려 있다. certbot plugins 는 dns-cloudflare 와 standalone 과 webroot 세 줄을 내고, 자격증명 파일은 600 으로 놓여 있다. 밖에서 친 openssl s_client 는 체인 0 부터 3 까지 네 단계와 Verify return code: 0 (ok) 를 냈고, curl 은 404 tls=0 을 냈다. 설정 원본은 §184 가 가리키는 저장소의 deploy/lab/edge/ 에 있고 리비전은 frontmatter 에 적었다.
없는 것도 분명하다. HTTP-01 을 실제로 시도해 실패한 기록은 없다. 그 경로가 막혔다는 근거는 시도가 아니라 주소 대역이고, 100.64.0.0/10 이 CGNAT(Carrier-Grade NAT) 예약 대역이라는 것은 이 실험대가 잰 값이 아니라 규격이다.
영향
감수한 비용이 셋이다.
Cloudflare API 토큰 : 엣지 VM 안 평문 파일에 놓인다 발급 검증 시간 : TXT 가 퍼질 때까지 기다리느라 수십 초 걸린다. 정상이므로 중간에 끊지 않는다 DNS 공급자 의존 : 인증서를 받는 일이 Cloudflare 계정에 묶인다
토큰이 평문으로 놓이는 것을 줄이려고 가드레일을 넷 둔다.
권한 범위 : Edit zone DNS · Specific zone · hyeonworks.com 으로 좁힌다. All zones 로 두면 계정의 모든 도메인에서 DNS 를 고칠 권한이 그 파일에 놓이고, Global API Key 는 폐기하면 그 키를 쓰던 다른 것들이 전부 같이 못 쓰게 된다 파일 권한 : install -m 600 /dev/null 로 비어 있을 때 먼저 600 을 만든다. 토큰을 쓰고 나서 권한을 고치면 그사이가 열려 있다 확인 방법 : ls -l 이 -rw------- 을 내는지와 바이트 수가 0 이 아닌지만 보고 값은 찍지 않는다 토큰 검증 : Cloudflare 의 user/tokens/verify 가 내는 status 가 active 이고 success 가 true 인지 본다. code 가 6003 이면 값이 틀렸거나 잘렸고 9109 면 권한 범위가 모자라다. 여기서 걸러 두면 뒤에서 실패했을 때 DNS 문제인지 토큰 문제인지가 섞이지 않는다
그래도 토큰이 엣지 안에만 있는 것은 아니다. 원본은 Cloudflare 쪽 서버가 들고 있고 엣지에 놓인 것은 사본이라, 엣지 게스트를 지우는 것으로는 계정 쪽 토큰이 없어지지 않는다. 폐기는 Cloudflare 에서 따로 해야 하는데 가이드는 그것을 적지 않았다.
그 배치가 노린 것이 하나 더 있다. virsh undefine kc-lab-edge 한 줄로 이 계층을 통째로 되돌린다는 것이 가이드가 적은 이유인데, 그것은 게스트를 지우면 같이 없어진다는 말이지 걷어내는 절차가 아니다. 원본 가이드 04 에는 걷어내는 순서도, 지운 뒤에 무엇을 확인하는지도 없고, 이 실험대가 그렇게 지워 본 적도 없다.
발급 자체에도 순서가 붙는다. --dry-run 을 먼저 돌리는 것은 Let's Encrypt 의 주당 중복 인증서 5장 한도를 dry-run 이 쓰지 않기 때문이다. dry-run 은 인증서를 저장하지 않으므로 그 직후 certbot certificates 가 No certificates found 를 내는 것이 정상이다.
이 결정이 남긴 이름 규칙이 하나 있다. live/hyeonworks.com/ 은 certbot 이 이 묶음을 관리하려고 첫 번째 -d 에서 따온 라벨이고 서빙과 무관하다. 브라우저가 보는 유효 호스트명은 -d 로 준 이름 전부이므로 auth.hyeonworks.com 으로 다시 받을 필요가 없다. 대신 nginx 설정에는 그 디렉터리 경로를 한 글자도 다르지 않게 적어야 한다. live/auth.hyeonworks.com/ 이라고 적으면 cannot load certificate 로 막히고, §182 가 이것을 가이드 결함 여섯 중 하나로 셌다. 와일드카드는 한 단계만 덮는다. a.b.hyeonworks.com 도, apex 인 hyeonworks.com 자신도 와일드카드에 들어가지 않아 -d 를 둘 준다.
이 결정이 끝내지 못한 것이 하나 있다. 받은 인증서가 갱신 뒤에 실제로 서빙되는지는 발급 방식과 별개이고, 근거로 건 첫 기록이 그것을 받는다.