refactor: 문서 개선 중

This commit is contained in:
donghyeon-ka
2026-09-21 14:30:55 +09:00
parent c93cdea150
commit 805a18f486
1497 changed files with 525837 additions and 59152 deletions
@@ -9,17 +9,17 @@ project: virtualization
status: 게시 전
studio: "https://hyeonworks.com/studio/documents/f972ca27-7e18-41c1-9494-59cc6f676ae2/edit"
pinnedVersions:
- name: libvirt
- name: libvirt (observed; 이 Setup이 설치 버전을 고정하지 않음)
version: 12.7.0
- name: Debian GNU/Linux
version: 12 (bookworm)
- name: cloud-init
- name: cloud-init (observed; bookworm/latest 이미지라 exact version 미고정)
version: 22.4.2
source:
- final/document.md#187-단계-01-게스트-세-대
- final/document.md#185-가이드-묶음이-스스로-정한-규약
- final/document.md#184-이-부의-출처와-범위
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
---
# cloud-init 시드로 게스트 세 대를 만들고 SSH 가 키로 붙게 한다
@@ -227,7 +227,7 @@ grep -c 'ssh-ed25519\|ssh-rsa' kc-lab-1.yaml
**예상 결과** — `0` 과 `2`. 첫 줄이 `0` 이 아니면 `__LAB_HOST_KEY__` 같은 문자열이 남아 있고, 둘째 줄이 `2` 가 아니면 키 하나가 안 들어갔다. 두 숫자가 맞아야 시드를 만든다.
첫 파일에서는 여기까지다. cloud-config 스키마까지 보는 검증기는 `cloud-init schema` 인데, 그것은 게스트의 cloud-init `22.4.2` 이고 첫 파일을 쓰는 시점에는 게스트가 아직 없다. lab host 에 cloud-init 이 깔려 있는지는 원본 가이드에 적혀 있지 않고 재지도 않았으며(unknown), `yamllint` 은 이 실험대에 없다. 게스트가 한 대라도 뜬 뒤에는 둘째 파일부터 그 검증기로 본다. 파일을 보내고, 들어가고, 검사하고, 지운다.
첫 파일에서는 여기까지다. cloud-config 스키마까지 보는 검증기는 `cloud-init schema` 인데, 2026-09-17 이 실험대에서 관측한 게스트의 cloud-init `22.4.2` 였다. 하지만 base URL이 `bookworm/latest` 이므로 다음 다운로드도 같은 cloud-init 버전이라는 계약은 없다. 첫 파일을 쓰는 시점에는 게스트가 아직 없다. lab host 에 cloud-init 이 깔려 있는지는 원본 가이드에 적혀 있지 않고 재지도 않았으며(unknown), `yamllint` 은 이 실험대에 없다. 게스트가 한 대라도 뜬 뒤에는 둘째 파일부터 그 검증기로 본다. 파일을 보내고, 들어가고, 검사하고, 지운다.
```bash label="[lab host] ③ 검사할 파일을 게스트로 보낸다"
scp kc-lab-2.yaml donghyeon@192.168.122.11:~/
@@ -267,7 +267,7 @@ Valid cloud-config: /home/donghyeon/kc-lab-edge.yaml
이 실험대는 접속과 리다이렉션과 권한 설정을 두 줄에 몰아넣었다(observed).
```bash label="[lab host] 이 실험대가 실제로 친 형태"
```bash label="[lab host] [reference] 이 실험대가 실제로 친 형태"
ssh donghyeon@192.168.122.11 'umask 077; cat > ~/kc-lab-2.yaml' < kc-lab-2.yaml
ssh donghyeon@192.168.122.11 'cloud-init schema -c ~/kc-lab-2.yaml; rm -f ~/kc-lab-2.yaml'
```
@@ -469,7 +469,7 @@ Domain creation completed.
| 디스크 | base 위의 오버레이(`backing_store=`). 복사가 아니다 |
| 시드 | `seed-<이름>.iso` — `CIDATA` 라벨 · `user-data`·`meta-data` · `bus=virtio` |
| cloud-init 패키지 | `[curl, nftables]` |
| 스키마 검사기 | 게스트의 cloud-init `22.4.2` |
| 스키마 검사기 | 2026-09-17 관측: cloud-init `22.4.2` · `bookworm/latest` 사용으로 재다운로드 버전은 미고정 |
메모리는 처음 만들 때 세 대 다 3584MB 였고, 실험을 늘리며 5120 과 4096 으로 재배분했다(observed). 세 게스트의 배정 합은 5120+4096+1024 = 10,240MB 이고, 호스트 RAM 은 11,648MiB 다. 배정 합이 더 작아서 배정률은 10240/11648 = 87.9% 이고, 배정하지 않고 남은 것이 1,408MiB 다. 그래서 이 배치는 「Guest configured memory 총량이 Host physical RAM보다 크다」는 Memory Overcommit 에 해당하지 않는다.
@@ -9,7 +9,7 @@ project: virtualization
status: 게시 전
studio: "https://hyeonworks.com/studio/documents/74a7bacf-e5d8-4129-926a-c8cf5cacb8c9/edit"
pinnedVersions:
- name: nginx (엣지 게스트)
- name: nginx (엣지 게스트, observed; apt 설치 버전 미고정)
version: 1.22.1
- name: Debian GNU/Linux
version: 12 (bookworm)
@@ -20,7 +20,7 @@ source:
- final/document.md#179-엣지를-물리-호스트에서-vm-으로-옮기면-무엇이-새로-필요해지나
- final/document.md#180-nftables-는-앞-체인의-accept-로-뒤-체인의-reject-를-막지-못한다
- final/document.md#184-이-부의-출처와-범위
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
---
# 엣지 게스트에 nginx 를 세우고 호스트 DNAT 으로 밖에서 들어오는 길을 연다
@@ -17,7 +17,10 @@ source:
- final/document.md#188-단계-02-k3s-server-와-agent
- final/document.md#185-가이드-묶음이-스스로-정한-규약
- final/document.md#184-이-부의-출처와-범위
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
evidence:
- ../../../final/evidence/raw/lab-state-before-rebuild/22-k3s-agent-install.txt
- ../../../final/evidence/raw/lab-state-before-rebuild/23-k3s-cluster-verify.txt
---
# k3s server 와 agent 를 게스트 두 대에 깔고 lab host 에서 kubectl 로 본다
@@ -121,7 +124,7 @@ ssh donghyeon@192.168.122.11 'hostname; cat /etc/os-release | head -1'
**목적** — `kc-lab-1` 을 control-plane 으로 세우고, 그 노드가 자기 주소를 `192.168.122.11` 로 알게 한다.
```bash label="[lab host] ① server 설치 스크립트를 원격으로 돌린다"
ssh kc-lab-1 'curl -sfL https://get.k3s.io | sudo sh -s - server --node-ip 192.168.122.11'
ssh kc-lab-1 "curl -sfL https://get.k3s.io | sudo env INSTALL_K3S_VERSION=v1.36.4+k3s1 sh -s - server --node-ip 192.168.122.11"
```
```bash label="[lab host] ② 유닛이 떴고 자기 자신을 노드로 등록했는지 본다"
@@ -255,7 +258,7 @@ chmod 600 ~/node-token
```
```bash label="[kc-lab-2] ⑤ agent 를 깐다"
curl -sfL https://get.k3s.io | sudo sh -s - agent \
curl -sfL https://get.k3s.io | sudo env INSTALL_K3S_VERSION=v1.36.4+k3s1 sh -s - agent \
--server https://192.168.122.11:6443 \
--token-file ~/node-token \
--node-ip 192.168.122.12
@@ -315,9 +318,9 @@ exit
**왜 필요한가** — 설치 스크립트는 `sudo` 아래 root 로 도니 홈의 `600` 파일도 읽는다. `--token` 대신 `--token-file` 을 쓰면 토큰이 명령줄에 안 들어가므로 프로세스 목록과 셸 히스토리에 남지 않고, 토큰을 꺼낸 셸과 같은 셸에서 쳐야 한다는 제약도 없어진다.
이 실험대는 두 줄로 했다(observed).
이 실험대는 두 줄로 했다(observed). 아래는 당시 실행을 보존한 historical/reference block이고, 따라 하는 절차는 위 ①~⑪처럼 검증과 삭제를 분리한다.
```bash label="[lab host] 이 실험대가 실제로 친 형태"
```bash label="[lab host] [reference] 이 실험대가 실제로 친 형태"
ssh kc-lab-1 'sudo cat /var/lib/rancher/k3s/server/node-token' \
| ssh kc-lab-2 'sudo tee /tmp/token >/dev/null'
ssh kc-lab-2 "curl -sfL https://get.k3s.io | sudo sh -s - agent \
@@ -528,7 +531,7 @@ subject=O = system:nodes, CN = system:node:kc-lab-2
## 무엇이 관측이고 무엇이 아닌가
- (observed) `v1.36.4+k3s1`, 두 노드 `Ready`, INTERNAL-IP 두 개, 토큰 108자, `ExecStart` 두 줄, agent 의 `localhost:8080` 오류와 인증서 subject.
- (observed) 2026-09-17 당시 stable installer가 선택한 `v1.36.4+k3s1`, 두 노드 `Ready`, INTERNAL-IP 두 개, 토큰 길이, `ExecStart` 두 줄, agent 의 `localhost:8080` 오류와 인증서 subject. 위 canonical 설치 명령은 재실행 때도 같은 k3s를 받도록 `INSTALL_K3S_VERSION=v1.36.4+k3s1`을 명시한다.
- (observed) `kube-system` 에 뜬 파드 여덟 줄과 `local-path (default)` StorageClass. `svclb-traefik-` 두 줄이 DaemonSet 이 두 노드에 다 떴다는 증거가 된다.
- (external) 쿠버네티스 1.20 이전의 평문 8080, Node authorizer 와 NodeRestriction 의 동작은 이 실험대에서 잰 값이 아니다.
- (unknown) 3번과 4번의 나눈 형태는 이 실험대에서 치지 않았다. 이 실험대가 친 것은 원격 셸 둘을 파이프로 이은 두 줄이고, 나눈 형태로 같은 클러스터가 서는지는 다시 재지 않았다.
@@ -13,7 +13,7 @@ pinnedVersions:
version: v1.36.4+k3s1
- name: curlimages/curl
version: 8.11.1
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
source:
- final/document.md#191-단계-05-keycloak-2노드와-postgresql
- final/document.md#184-이-부의-출처와-범위
@@ -24,7 +24,7 @@ source:
- final/document.md#333-안전한-종료-순서
- final/document.md#334-복구-순서-종료의-역순
- final/document.md#313-k3s-server와-agent-죽였을-때가-다르다
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
---
# 실험대를 껐다 켜고 게스트 메모리를 다시 나눈다
@@ -9,15 +9,15 @@ project: virtualization
status: 게시 전
studio: "https://hyeonworks.com/studio/documents/7c66a553-0008-4294-a27a-687bd1bda0c1/edit"
pinnedVersions:
- name: libvirt
- name: libvirt (observed; pacman 설치는 버전 미고정)
version: 12.7.0
- name: QEMU
- name: QEMU (observed; pacman 설치는 버전 미고정)
version: 11.1.1
source:
- final/document.md#186-단계-00-lab-host-가상화-준비
- final/document.md#185-가이드-묶음이-스스로-정한-규약
- final/document.md#184-이-부의-출처와-범위
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
---
# lab host 에 가상화 패키지를 깔고 virsh 가 sudo 없이 돌게 만든다
@@ -185,7 +185,7 @@ virsh --version
qemu-system-x86_64 --version
```
**예상 결과** — 판 번호 두 줄. 이 실험대는 libvirt `12.7.0` 과 `QEMU emulator version 11.1.1` 이었다(observed).
**예상 결과** — 판 번호 두 줄. 이 실험대는 libvirt `12.7.0` 과 `QEMU emulator version 11.1.1` 이었다(observed). 다만 위 `pacman -S --needed` 는 Arch 현재 저장소에서 받으므로 이 두 버전을 재설치에도 고정하는 명령이 아니다. frontmatter의 번호도 **tested/observed version**이지 package pin이 아니다.
**왜 필요한가** — 여기서 나오는 libvirt 판 번호가 뒤의 옵션 이름을 가른다. 훨씬 낮은 판이면 `virt-install --cloud-init` 같은 옵션의 동작이 달라질 수 있으니, 다음 단계에서 막힐 때 이 번호를 같이 본다.
@@ -13,7 +13,7 @@ pinnedVersions:
version: v1.36.4+k3s1
- name: Debian GNU/Linux
version: 12 (bookworm)
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
source:
- final/document.md#192-단계-06-prometheus-와-grafana
- final/document.md#184-이-부의-출처와-범위
@@ -23,7 +23,9 @@ source:
- final/document.md#202-철거-실제-출력-전문
- final/document.md#204-재구축할-때-무엇이-남아-있나
- final/document.md#195-이-부의-출처와-범위
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
evidence:
- ../../../final/evidence/raw/lab-state-before-rebuild/58-authenticator-and-token.txt
---
# 실험대를 철거하고 무엇이 남는지 확인한다
@@ -39,7 +41,7 @@ sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
- **5120MB 를 줬는데 353MB 를 쓰고 있었다 — 선언한 양과 실제로 드는 양**
회수된 디스크 3.1GB 를 읽는 방법이 그 기록과 같다. 선언한 크기와 실제로 차지한 크기는 다른 값이다.
- **이 실험대의 certbot 은 HTTP-01 로 받고 있는가 DNS-01 로 받고 있는가**
인증서를 지우지 않고 남기는 까닭이 이 물음이 안 닫혔기 때문이다. 답이 나오면 정책이 바뀐다.
이 물음은 `dns-cloudflare` 관측으로 닫혔다. 여기서는 그 사실과 별개로 현재 자격증명이 재발급에 쓸 수 있는지를 철거 조건으로 본다.
- **가이드대로 다시 쳐서 이 실험대가 같은 상태로 서는가**
철거는 그 물음을 재기 위한 전제라, 지우고 다시 세워 봐야 답이 나온다.
- **도구가 낸 출력은 대상의 상태가 아니다 — 그 명령이 무엇을 세는지부터 가른다**
@@ -159,9 +161,9 @@ error: XML error: Cannot use host name '' in network 'default'
zsh 에서 루프로 돌리면 이 오류를 만난다. zsh 는 따옴표 없는 변수를 단어 분리하지 않아서, bash 에서 되던 `set -- $entry` 가 `$1` 에 문자열 전체를 넣고 `$2` 와 `$3` 을 비워서 `name=""` 이 된다. 세 줄을 값 그대로 쓰는 편이 안전하다.
## 인증서는 건드리지 않는
## 인증서는 edge guest에서 백업한
**이 절의 전제가 03·04 뒤로 깨져 있다**(2026-09-17, observed). 아래 본문은 인증서가 랩 호스트의 `/etc/letsencrypt/` 에 있다고 보고 그 디렉터리를 정책으로 남긴다. 그런데 03 이 nginx `kc-lab-edge` 로 옮겼고 04 certbot 을 거기 깔았다. **지금 서빙하는 인증서는 그 게스트 안에 산다.** 그리고 이 절차의 1번은 게스트를 `--remove-all-storage` 로 지운다 — 인증서는 그 안에서 같이 사라진다.
2026-09-17 현재 서빙하는 인증서는 **`kc-lab-edge` 안의 `/etc/letsencrypt/`** 에 있다(observed). 03에서 nginx를 edge guest로 옮겼고 04에서 certbot도 그 guest에 설치했다. 이 절차의 1번은 guest를 `--remove-all-storage`로 지우므로, 인증서도 백업하지 않으면 guest 디스크와 함께 사라진다.
두 기계를 나란히 열어 보면 이렇다.
@@ -175,32 +177,34 @@ ssh kc-lab-edge 'sudo ls -la /etc/letsencrypt/'
| 랩 호스트 | 있다 (읽으려면 `sudo`) | `/usr/bin/certbot` | 없다 | 안 한다 — 80·443 을 안 듣는다 |
| `kc-lab-edge` | 있다 — `cli.ini` · `renewal-hooks` | `/usr/bin/certbot` | `certbot.timer` | **한다** |
그래서 아래 백업 명령은 **틀린 기계를 묶는다.** 묶어야 할 것은 게스트 쪽이다.
따라서 **철거 전에 백업할 대상은 edge guest의 `/etc/letsencrypt/` 하나다.** 파일 이름은 host에서 한 번 만든 시각값으로 고정해, 원격 tar와 scp가 같은 파일을 가리키게 한다.
```bash label="[lab host] 실제로 서빙하는 인증서를 묶는다"
ssh kc-lab-edge "sudo tar czf /tmp/letsencrypt-backup-$(date +%Y%m%d-%H%M%S).tgz -C /etc letsencrypt"
scp kc-lab-edge:/tmp/letsencrypt-backup-*.tgz ~/
```bash label="[lab host] 실제로 서빙하는 인증서를 edge guest에서 묶는다"
STAMP=$(date +%Y%m%d-%H%M%S)
ssh kc-lab-edge "sudo tar czf /tmp/letsencrypt-backup-$STAMP.tgz -C /etc letsencrypt"
scp "kc-lab-edge:/tmp/letsencrypt-backup-$STAMP.tgz" "$HOME/"
```
**「철거해도 남는 것」 표의 패키지 줄도 같은 이유로 틀렸다.** 그 줄은 `nginx` 와 `certbot` 을 남는 쪽에 적어 두었는데, 둘 다 게스트 안에 있으므로 게스트와 함께 사라진다. 랩 호스트에 남는 것은 `libvirt` · `qemu` · `kubectl` 과, 쓰이지 않는 호스트 쪽 `certbot` 이다.
**예상 결과** — host의 `$HOME/letsencrypt-backup-$STAMP.tgz`가 생긴다. 이 파일은 edge guest를 `--remove-all-storage`로 지우기 전에 host 쪽으로 빠져나온 복구본이다.
`/etc/letsencrypt/` 는 정책으로 남긴다. 한도 때문이 아니다 — Let's Encrypt 의 「같은 이름 조합에 주당 중복 5장」 제한은 가끔 재구축하는 정도로는 근처에도 못 간다.
패키지도 host와 guest를 나눠 본다. `kc-lab-edge` 안의 `nginx`와 `certbot`은 guest와 함께 사라지고, host에는 `libvirt`·`qemu`·`kubectl`과 현재 서빙에는 쓰이지 않는 host `certbot`이 남는다.
남기는 까닭은 지금 재발급이 되는지를 모르기 때문이다. 이 실험대의 이름 셋은 tailnet 주소를 가리키고, `100.64.0.0/10` 은 CGNAT(Carrier-Grade NAT, 통신사 공용 주소 변환)용 예약 대역이라 공개 인터넷에서 라우팅되지 않는다. HTTP-01 검증은 Let's Encrypt 가 우리 서버로 들어오는 방식이므로 그 주소로는 검증이 성립하지 않는다. 지금 설정이 DNS-01 이면 지우고 다시 받으면 끝이고, HTTP-01 이면 검증 방식부터 손봐야 한다. 어느 쪽인지는 certbot 설정을 읽는 열린 물음이 한 줄로 닫는다.
정책으로 남기는 것은 guest 안의 디렉터리 자체가 아니라 **host로 복사한 인증서 backup tar**다. 한도 때문도 아니고 검증 방식이 미확정이어서도 아니다. 2026-09-17 renewal 설정에서 `authenticator = dns-cloudflare` 를 직접 읽어 **DNS-01 등록은 확인했다**.
§204 는 HTTP-01 인 경우를 「못 한다」가 아니라 「일이 하나 생긴다」로 적었다. 최악이라도 A 레코드를 공인 IP 로 잠깐 돌려 받거나 DNS-01 로 전환하면 되고, 다만 재구축을 시작하자마자 그 일부터 하게 된다. 그래서 이 정책은 「어느 쪽인지 모르는 채로는 지우지 않는다」가 전부다.
backup tar를 남기는 까닭은 **현재 자격증명으로 다시 받을 수 있는지가 확인되지 않았기 때문**이다. 같은 후속 원문에서 `/etc/letsencrypt/cloudflare.ini` 의 토큰 길이는 0자로 측정됐고 Cloudflare 검증은 `Invalid request headers` 로 실패했다. 따라서 DNS-01이라는 사실만으로 “guest를 지워도 다시 받으면 된다”고 결론내리지 않는다.
그래도 지워야 한다면 먼저 백업한다.
유효한 Cloudflare 토큰을 다시 준비하고 `certbot renew --dry-run` 이 성공한 원문까지 남기면 그때 보존 정책을 완화할 수 있다. 그 전에는 guest를 지우기 전에 실제 서빙 중인 `/etc/letsencrypt/` 를 host로 백업하고 그 tar를 보존한다.
```bash label="[lab host] 인증서를 통째로 묶어 둔다"
sudo tar czf ~/letsencrypt-backup-$(date +%Y%m%d-%H%M%S).tgz -C /etc letsencrypt
복원도 **새로 만든 `kc-lab-edge` 안으로** 되돌린다. host의 `/etc`에 풀지 않는다. `{{STAMP}}`에는 백업 파일 이름의 시각값을 넣는다.
```bash label="[lab host] 백업을 새 edge guest로 되돌린다"
BACKUP="$HOME/letsencrypt-backup-{{STAMP}}.tgz"
scp "$BACKUP" kc-lab-edge:/tmp/letsencrypt-backup.tgz
ssh kc-lab-edge 'sudo tar xzf /tmp/letsencrypt-backup.tgz -C /etc'
ssh kc-lab-edge 'sudo certbot certificates && sudo nginx -t'
```
복원은 반대로 한 줄이다. `{{STAMP}}` 는 위 명령이 만든 파일 이름에 찍힌 시각을 그대로 옮겨 넣는다.
```bash label="[lab host] 묶어 둔 인증서를 되돌린다"
sudo tar xzf ~/letsencrypt-backup-{{STAMP}}.tgz -C /etc
```
마지막 줄이 둘 다 성공해야 복원이 끝난 것이다. 인증서 파일이 돌아왔는지와 nginx가 그 경로를 실제로 읽을 수 있는지를 따로 확인한다.
## 구성 값
@@ -263,11 +267,13 @@ seed-kc-lab-{1,2,edge}.iso Capacity=370.00 KiB Allocation=372.00 KiB
| 무엇 | 어떻게 되나 | 왜 |
|---|---|---|
| `base.qcow2` (335MB) | 남는다 | 다음 오버레이의 바닥 |
| 패키지 (libvirt · qemu · nginx · certbot · kubectl) | 남는다 | 재설치가 무의미 |
| host 패키지 (`libvirt` · `qemu` · `kubectl`, 쓰이지 않는 host `certbot`) | 남는다 | guest 삭제와 무관한 host 설치 상태 |
| edge guest의 `nginx` · `certbot` | 사라진다 | `kc-lab-edge` 디스크와 함께 삭제된다 |
| `~/workspace/cloud/kc-lab-{1,2}.yaml` | 남는다 | 키와 비밀번호가 들어 있다 |
| `~/.ssh/config` 의 `kc-lab-*` 항목 | 남는다 | 재구축해도 IP 가 같다 |
| libvirt `default` 네트워크 정의 | 남는다 | 예약만 지웠다 |
| `/etc/letsencrypt/` | 남긴다 (정책) | 재발급이 되는지를 아직 모른다 |
| edge guest의 `/etc/letsencrypt/` | 사라진다 | guest 삭제 전에 host의 `$HOME/letsencrypt-backup-<stamp>.tgz`로 백업한다 |
| host에 복사한 인증서 백업 tar | 남긴다 (정책) | DNS-01 등록은 확인됐지만 credential 유효성과 `certbot renew --dry-run` 성공은 아직 미확인 |
| 게스트 디스크 · 시드 ISO | 사라진다 | `--remove-all-storage` |
| DHCP 예약 | 사라진다 | `net-update delete` |
| k3s · Keycloak · 모든 워크로드 | 사라진다 | 게스트와 함께 |
@@ -9,13 +9,16 @@ project: virtualization
status: 게시 전
studio: "https://hyeonworks.com/studio/documents/975a6d61-4e34-4034-a0d2-01fea3b498a3/edit"
pinnedVersions:
- name: nginx (엣지 게스트)
- name: nginx (엣지 게스트, observed; apt 설치 버전 미고정)
version: 1.22.1
- name: nginx (물리 호스트)
- name: nginx (물리 호스트, observed; 이 Setup이 설치·버전을 고정하지 않음)
version: 1.30.4
- name: Debian GNU/Linux
version: 12 (bookworm)
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
evidence:
- ../../../final/evidence/raw/lab-state-before-rebuild/58-authenticator-and-token.txt
- ../../../final/evidence/raw/lab-state-before-rebuild/60-doc04-step5-path-defect.txt
source:
- final/document.md#190-단계-04-let's-encrypt-와-인증서-갱신
- final/document.md#184-이-부의-출처와-범위
@@ -81,7 +84,7 @@ source:
| 패키지 | `certbot` · `python3-certbot-dns-cloudflare` |
| 자격증명 | `/etc/letsencrypt/cloudflare.ini``600`, root 만 읽기 |
| 발급 대상 | `-d hyeonworks.com -d '*.hyeonworks.com'` |
| lineage 디렉터리 | `/etc/letsencrypt/live/hyeonworks.com/`첫 번째 `-d` 에서 따온 라벨 |
| lineage 디렉터리 | `/etc/letsencrypt/live/auth.hyeonworks.com/`2026-09-17 현재 실험대에서 관측한 경로 |
| nginx 가 읽는 것 | `fullchain.pem` · `privkey.pem` |
| 유효기간 | 오늘 + 90일 |
| deploy 훅 | `/etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh` (`chmod +x`) |
@@ -98,7 +101,7 @@ source:
가이드는 이 선택을 우열로 적지 않는다. 공개 서버라면 HTTP-01 이 맞고, 토큰도 DNS 연동도 없어 관리할 것이 적다.
아래 절차는 DNS-01 로 받는 형태다. 다만 **지금 돌고 있는 이 실험대의 certbot 이 어느 쪽으로 등록되어 있는지는 아직 읽지 못했다**(unknown) — 호스트의 `sudo` 가 비밀번호를 요구해 `/etc/letsencrypt/renewal/*.conf` `authenticator` 를 비대화식으로 읽을 수 없었다. 이 실험대의 문서 두 개도 이 대목에서 서로 어긋나, 한쪽은 DNS-01 로 결론냈고 다른 한쪽은 같은 04 단계를 `certbot certonly --webroot` 로 적어 두었다. 어느 쪽이 실제로 등록되어 있는지는 그 한 줄을 읽어야 갈린다.
아래 절차는 DNS-01 로 받는 형태이고 **이 실험대의 현재 등록 방식도 DNS-01 로 확인됐다**(2026-09-17, observed). `sudo sh -c` 안에서 renewal 설정을 읽자 `authenticator = dns-cloudflare` `dns_cloudflare_credentials = /etc/letsencrypt/cloudflare.ini` 가 나왔다. 다만 같은 후속 원문에서 토큰 길이는 0자였고 Cloudflare 검증은 실패했다. 따라서 검증 방식은 확정됐지만 현재 자격증명이 재발급에 쓸 수 있는지는 별도 확인이 필요하다.
## 전제와 되돌리기
@@ -284,7 +287,7 @@ sudo certbot certificates
```
```bash label="[kc-lab-edge] ④ 인증서가 실제로 어떤 이름에 유효한지 본다"
sudo openssl x509 -noout -ext subjectAltName -in /etc/letsencrypt/live/hyeonworks.com/fullchain.pem
sudo openssl x509 -noout -ext subjectAltName -in /etc/letsencrypt/live/auth.hyeonworks.com/fullchain.pem
```
**예상 결과** — ① 의 마지막 줄이 `The dry run was successful.` 이고, ② 는 `Successfully received certificate.` 와 저장 경로를 찍는다. ③ 에서 볼 것은 네 줄이다.
@@ -293,8 +296,8 @@ sudo openssl x509 -noout -ext subjectAltName -in /etc/letsencrypt/live/hyeonwork
|---|---|
| `Domains:` | `hyeonworks.com *.hyeonworks.com` — 한 줄에 둘 다 |
| `Expiry Date:` | 오늘 + 90일, `VALID` |
| `Certificate Path:` | `/etc/letsencrypt/live/hyeonworks.com/fullchain.pem` |
| `Private Key Path:` | `/etc/letsencrypt/live/hyeonworks.com/privkey.pem` |
| `Certificate Path:` | `/etc/letsencrypt/live/auth.hyeonworks.com/fullchain.pem` — 2026-09-17 현재 관측 |
| `Private Key Path:` | `/etc/letsencrypt/live/auth.hyeonworks.com/privkey.pem` |
④ 는 `DNS:hyeonworks.com, DNS:*.hyeonworks.com` 두 개를 내놓고 `auth.hyeonworks.com` 은 두 번째에 걸린다.
@@ -304,8 +307,8 @@ sudo openssl x509 -noout -ext subjectAltName -in /etc/letsencrypt/live/hyeonwork
| 무엇 | 정해지는 방식 |
|---|---|
| 디렉터리 이름 `live/hyeonworks.com/` | certbot 이 이 묶음을 관리하려고 붙인 라벨. 첫 번째 `-d` 에서 따오고 서빙과 무관하다 |
| SAN 목록 | 브라우저가 보는 실제 유효 호스트명. `-d` 로 준 이름 전부 |
| 디렉터리 이름 `live/auth.hyeonworks.com/` | 2026-09-17 현재 실험대에서 실제로 존재한 lineage. `certbot certificates` 가 찍은 경로를 따른다 |
| SAN 목록 | 브라우저가 보는 실제 유효 호스트명. `-d` 로 준 이름 전부이며 lineage 이름과 별개 |
**문제가 생기면** — 최초 실행이면 계정 등록 대화가 먼저 뜬다. 이메일을 비우면 `Invalid email address: .` 로 되묻고, 약관은 `Y`, 뉴스레터는 발급과 무관하므로 `N` 이다. 프롬프트 없이 돌리려면 `-m <이메일> --agree-tos --no-eff-email` 을 붙인다. `--register-unsafely-without-email` 은 쓰지 않는다 — 갱신 실패를 알려 줄 통로가 사라지는데, 6번이 재는 것이 바로 그 갱신이다. DNS-01 은 TXT 레코드가 퍼질 때까지 기다리느라 수십 초 걸리므로 중간에 끊지 않는다.
@@ -334,8 +337,8 @@ server {
listen 443 ssl http2 default_server;
server_name _;
ssl_certificate /etc/letsencrypt/live/hyeonworks.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/hyeonworks.com/privkey.pem;
ssl_certificate /etc/letsencrypt/live/auth.hyeonworks.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/auth.hyeonworks.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
@@ -369,7 +372,7 @@ sudo nginx -t && sudo systemctl reload nginx
`ssl_certificate` 에 적는 것은 `cert.pem` 이 아니라 `fullchain.pem` 이다. 서버 인증서만 보내면 중간 인증서가 빠져 체인이 끊기는데, 브라우저는 대개 캐시나 AIA 로 보완해서 정상으로 보이고 캐시가 없는 클라이언트에서만 깨지므로 발견이 늦다. 4번 ③ 에서 certbot 이 찍어 준 경로와 한 글자도 다르면 안 된다.
**★ 위 ② 의 인증서 경로가 틀렸다**(2026-09-17, observed). `live/hyeonworks.com/` 이라고 적혀 있는데 certbot 이 만드는 디렉터리는 **`live/auth.hyeonworks.com/`** 이다. 이름을 여럿 담은 인증서라도 디렉터리 이름은 `-d` 로 처음 준 이름 하나를 쓴다. 그대로 치면 3번에서 nginx 가 안 뜬다.
**★ 원본 가이드 04 의 5단계에는 known-wrong 경로가 있었다**(2026-09-17, observed). 가이드는 `live/hyeonworks.com/` 을 적었지만 실제 `live/` 아래에는 `auth.hyeonworks.com` 이 있었고, 그 잘못된 경로로 `nginx -t` 를 돌리면 아래처럼 실패했다. 위 canonical 절차는 이 관측을 반영해 처음부터 `live/auth.hyeonworks.com/` 을 쓴다.
```text label="문서 그대로 쳤을 때"
[emerg] cannot load certificate "/etc/letsencrypt/live/hyeonworks.com/fullchain.pem":
@@ -388,7 +391,7 @@ auth.hyeonworks.com
Certificate Path: /etc/letsencrypt/live/auth.hyeonworks.com/fullchain.pem
```
**아래 「문제가 생기면」의 진단은 방향이 거꾸로다.** `live/auth.hyeonworks.com/` 이 없는 경로가 아니라 **그쪽이 맞는 경로**이고, `live/hyeonworks.com/` 이 디스크에 없다. 4번 ③ 이 찍어 준 경로를 그대로 옮기라는 원칙은 옳고, 위 ② 가 그 원칙을 스스로 어겼다.
현재 truth는 하나다 — **이 실험대의 lineage는 `live/auth.hyeonworks.com/`이고, `live/hyeonworks.com/`은 원본 가이드에 남았던 historical wrong path**다. 다른 실험대에서 이름을 추측하지 말고 `certbot certificates` 가 찍은 `Certificate Path`를 그대로 nginx에 옮긴다.
**문제가 생기면** — `cannot load certificate` 로 막히면 경로를 본다. 자기 실험대의 디렉터리 이름은 `sudo ls /etc/letsencrypt/live/` 가 답한다. 확인 ② 에서 본 판 번호가 1.25.1 미만인데 `http2` 를 지시어로 썼다면 여기서 `unknown directive "http2"` 가 나온다.