test-server 를 비우고 다시 세운 뒤 Setup 기록 35편(virtualization 9 ·
keycloak-session-store 26)을 문서에 적힌 명령 그대로 쳤다. 어긋난 자리를
기록과 SSOT 양쪽에 실측과 함께 넣었다.
막히던 것
- 04 의 인증서 경로가 live/hyeonworks.com 이라 nginx 가 [emerg] 로 안 떴다.
실제 계보는 live/auth.hyeonworks.com 이고 「문제가 생기면」은 진단이 거꾸로였다
- 인증서가 와일드카드가 아니다. SAN 이 auth·app1·app2 셋뿐이라 그 밖의 이름은
TLS 에서 끊기고 curl 이 exit 60 · %{http_code} 000 을 낸다. SSOT 안에서
두 문단이 서로 어긋나 있었다
- A-7 14번 ①이 kc-lab-1 에서 여섯 줄 다 실패하는데 마지막 date 만 「차단」을 찍는다
검사가 실패할 수 없던 자리
- B-1 의 세션 키 고르기는 앞 단계가 $KEY 를 채워 둬서 루프가 한 건도 못 맞혀도
통과한다. KEY= 로 비우고 키마다 1/0 을 찍게 바꿨다
- k3s-agent 유닛의 sed -i 는 패턴에 $HOME 이 들어 있어 아무 줄도 안 바꾼 채 성공한다
certbot
- renew --dry-run 의 종료 코드는 성공도 0, 실패도 0, 다른 사유의 실패는 1 이다.
본문의 renew failure(s) 로만 판정할 수 있다
- --dry-run 은 staging 서버를 쓰는데 renewal/*.conf 의 account= 는 운영 계정을
가리킨다. 실패한 dry-run 이 staging 계정을 하나 더 만들어 다음 실행이 계속 멎는다
- 훅을 755 로 놓고 시뮬레이션이 성공해도 Running deploy-hook command 는 안 나온다.
certbot 2.1.0 에는 --run-deploy-hooks 도 없다
- 강제 갱신은 실제로 쳤고 서빙까지 닿았다. serial 06F3E0EF…1373 → 065547…3DF1,
notAfter Dec 3 → Dec 16, nginx worker 2629 4712 → 4745 4754
독자가 칠 수 있는 형태로
- 안 되는 형태가 번호 붙은 단계에 앉아 있던 8곳을 뒤집고, 되는 형태를 ①로 올렸다
- 랩 안에서 공개 이름을 치는 curl 65줄에 --resolve 를 붙였다. 붙인 형태를 실제로
쳐서 문서가 적은 값과 같은지 확인했다
- 힙독·sed -i·echo >>·&&·|| 를 편집기 + 파일 리스팅 + 분할 형태로 바꿨다
- 닫는 코드펜스가 빠져 뒤 200여 줄의 블록 종류가 뒤집혀 있던 곳을 포함해 3곳을 고쳤다
관문: check_body PASS · check_prose error 0 · check_evidence 두 프로젝트 문제 없음 ·
verify-tech-log-tree error 0 · verify-project-layout error 0 · 코드펜스 전수 0건
남은 것: B-0 주입은 keycloak-pattern 저장소의 소스를 고치고 이미지를 다시 구워야
해서 안 했다(unknown).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
638 lines
39 KiB
Markdown
638 lines
39 KiB
Markdown
---
|
|
id: 975a6d61-4e34-4034-a0d2-01fea3b498a3
|
|
kind: SETUP
|
|
slug: wildcard-certificate-with-dns-01-and-a-deploy-hook
|
|
title: DNS-01 으로 와일드카드 인증서를 받고 갱신이 서빙까지 닿게 한다
|
|
topic: lab-environment-build
|
|
topicName: 실험대 환경 구성
|
|
project: virtualization
|
|
status: 게시 전
|
|
studio: "https://hyeonworks.com/studio/documents/975a6d61-4e34-4034-a0d2-01fea3b498a3/edit"
|
|
pinnedVersions:
|
|
- name: nginx (엣지 게스트)
|
|
version: 1.22.1
|
|
- name: nginx (물리 호스트)
|
|
version: 1.30.4
|
|
- name: Debian GNU/Linux
|
|
version: 12 (bookworm)
|
|
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
|
|
source:
|
|
- final/document.md#190-단계-04-let's-encrypt-와-인증서-갱신
|
|
- final/document.md#184-이-부의-출처와-범위
|
|
---
|
|
|
|
# DNS-01 으로 와일드카드 인증서를 받고 갱신이 서빙까지 닿게 한다
|
|
|
|
엣지 게스트에 certbot 과 Cloudflare 플러그인을 깔고 DNS-01 로 와일드카드 인증서를 받아 nginx 에 443 을 얹는 절차다. 갱신이 서빙하는 인증서까지 닿게 하는 deploy 훅도 넣는다. 물리 호스트에는 아무것도 두지 않는다.
|
|
|
|
## 관계
|
|
|
|
- **인증서는 DNS-01 로 받는다 — 실험대 주소가 공개 인터넷에 없어 HTTP-01 이 성립하지 않는다**
|
|
왜 이 방식이어야 했는지와 토큰이 평문으로 놓이는 비용을 그 결정이 갖고, 여기는 그것을 실행하는 명령만 갖는다.
|
|
- **갱신은 매번 SUCCESS 였고 옛 인증서가 계속 나갔다 — 2305초와 1~2초**
|
|
훅이 없을 때 갱신과 서빙이 얼마나 벌어지는지를 그 기록이 쟀다. 여기서 훅을 넣는 다섯째 단계가 그 측정에서 나왔다.
|
|
- **엣지 게스트에 nginx 를 세우고 호스트 DNAT 으로 밖에서 들어오는 길을 연다**
|
|
이 절차가 고쳐 쓰는 nginx 파일과 80 서버 블록을 그 단계가 먼저 세운다.
|
|
- **도구가 낸 출력은 대상의 상태가 아니다 — 그 명령이 무엇을 세는지부터 가른다**
|
|
갱신 로그의 `SUCCESS` 도 훅 로그의 `error output` 도 서버가 지금 무엇을 내보내는지를 말하지 않는다. 그래서 판정을 워커 PID 와 체인 단계 수로 한다.
|
|
- **가까운 층부터 한 층씩 건너뛰며 확인한다 — 층마다 성공 신호가 다르다**
|
|
확인 명령을 엣지 안에서 치면 층의 답이 아니라 친 곳의 답이 돌아온다. 어느 기계에서 치는지가 이 단계의 판정을 가른다.
|
|
- **Keycloak 2노드와 PostgreSQL 을 k3s 에 올리고 클러스터가 묶였는지 확인한다**
|
|
다음 단계다. 여기서 잰 `404 tls=0` 이 거기서 `200` 으로 바뀌는 것이 그 단계가 만든 변화다.
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
## 읽기 전에 — 어디서 치는가
|
|
|
|
이 단계도 셸이 갈린다.
|
|
|
|
| 번호 | 무엇 | 어디서 |
|
|
|---|---|---|
|
|
| 1 | certbot 설치 | `[kc-lab-edge]` |
|
|
| 2 | Cloudflare 토큰 발급 | 브라우저 |
|
|
| 3 | 토큰 파일 · 토큰 검증 | `[kc-lab-edge]` |
|
|
| 4 | 시험 발급 · 실제 발급 · 확인 | `[kc-lab-edge]` |
|
|
| 5 | nginx 에 443 을 얹는다 | `[kc-lab-edge]` |
|
|
| 6 | 갱신이 서빙까지 닿게 한다 | `[kc-lab-edge]` |
|
|
| 판정 | 열리는가 · 체인이 완전한가 | **tailnet 에 붙은 다른 머신** — 여기만 엣지가 아니다 |
|
|
|
|
**판정만 엣지에서 치지 않는 까닭**을 가이드가 한 줄로 적었다 — 엣지에는 Tailscale 이 없어 자기 자신을 밖에서 들어오는 경로로 부를 수 없다. 엣지 안에서 치면 이렇게 막힌다.
|
|
|
|
```text
|
|
* connect to 100.83.212.4 port 443 failed: Connection refused
|
|
```
|
|
|
|
`auth.hyeonworks.com` 은 호스트의 tailnet 주소로 풀리는데 엣지 게스트에는 Tailscale 이 없다. 엣지에서 나간 패킷은 호스트의 `virbr0` 으로 들어가고 호스트의 DNAT 규칙은 `iifname "tailscale0"` 만 매칭하므로 그 패킷은 규칙에 안 걸리고, 호스트 443 에 리스너가 없어 거절된다. 설정이 틀린 것이 아니라 친 곳이 틀렸다. 가이드는 그 기계를 「tailnet 에 붙은 다른 머신」이라고만 부르고 다섯 셸 이름 중 어느 것인지 짚지 않았다(unknown).
|
|
|
|
편집기로 여는 파일은 셋이다 — Cloudflare 자격증명, 443 블록을 넣은 nginx 설정, deploy 훅 스크립트. 설치와 발급과 갱신은 certbot 명령을 그대로 쓴다. 비밀은 값을 화면에 찍지 않는다. 토큰이 제대로 들어갔는지는 파일 권한과 크기로 보고, 값이 맞는지는 Cloudflare 에 물어 응답의 상태 문자열로 가른다.
|
|
|
|
## 이 단계가 세우는 것
|
|
|
|
가이드 04 의 「이 단계가 끝나면」은 한 줄이다.
|
|
|
|
> `https://` 가 열리고 체인이 완전하며, 갱신이 자동으로 **서빙까지** 닿는다.
|
|
|
|
세 마디가 각각 다른 확인을 요구한다. 「열린다」는 `curl -v`, 「체인이 완전하다」는 `openssl s_client`, 「서빙까지 닿는다」는 워커 PID 로 판정한다. 앞의 둘까지만 보고 끝내는 문서가 많고, 이 실험대가 재 둔 결함은 정확히 세 번째에 있다.
|
|
|
|
| 무엇 | 값 |
|
|
|---|---|
|
|
| 패키지 | `certbot` · `python3-certbot-dns-cloudflare` |
|
|
| 자격증명 | `/etc/letsencrypt/cloudflare.ini` — `600`, root 만 읽기 |
|
|
| 발급 대상 | `-d hyeonworks.com -d '*.hyeonworks.com'` |
|
|
| lineage 디렉터리 | `/etc/letsencrypt/live/hyeonworks.com/` — 첫 번째 `-d` 에서 따온 라벨 |
|
|
| nginx 가 읽는 것 | `fullchain.pem` · `privkey.pem` |
|
|
| 유효기간 | 오늘 + 90일 |
|
|
| deploy 훅 | `/etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh` (`chmod +x`) |
|
|
| 타이머 | `certbot-renew.timer` → `certbot-renew.service` |
|
|
|
|
**제약이 검증 방식을 정했다**(observed). 같은 Let's Encrypt 인증서인데 「이 도메인이 네 것이냐」를 증명하는 방법만 다르고, 이 실험대에는 선택의 여지가 없다.
|
|
|
|
| 무엇 | HTTP-01 | DNS-01 |
|
|
|---|---|---|
|
|
| 검증 방향 | Let's Encrypt 가 우리 서버로 (인바운드) | certbot 이 DNS 공급자 API 로 (아웃바운드) |
|
|
| 공개 인터넷에서 보여야 하나 | 그렇다 | 아니다 |
|
|
| 와일드카드 | 불가 | 가능 |
|
|
| 필요한 것 | 80 포트 · 공개 A 레코드 | DNS 공급자 API 토큰 |
|
|
|
|
가이드는 이 선택을 우열로 적지 않는다. 공개 서버라면 HTTP-01 이 맞고, 토큰도 DNS 연동도 없어 관리할 것이 적다.
|
|
|
|
아래 절차는 DNS-01 로 받는 형태다. 다만 **지금 돌고 있는 이 실험대의 certbot 이 어느 쪽으로 등록되어 있는지는 아직 읽지 못했다**(unknown) — 호스트의 `sudo` 가 비밀번호를 요구해 `/etc/letsencrypt/renewal/*.conf` 의 `authenticator` 를 비대화식으로 읽을 수 없었다. 이 실험대의 문서 두 개도 이 대목에서 서로 어긋나, 한쪽은 DNS-01 로 결론냈고 다른 한쪽은 같은 04 단계를 `certbot certonly --webroot` 로 적어 두었다. 어느 쪽이 실제로 등록되어 있는지는 그 한 줄을 읽어야 갈린다.
|
|
|
|
## 전제와 되돌리기
|
|
|
|
전제는 한 줄이다 — 앞 단계가 끝나 엣지 nginx 가 Traefik 으로 프록시한다.
|
|
|
|
**되돌리는 절차는 원본 가이드 04 에 없다**(unknown). 되돌리기에 가장 가깝게 적힌 문장은 배치의 까닭 한 줄뿐이다.
|
|
|
|
> 인증서·certbot·갱신 타이머·deploy 훅이 전부 엣지 게스트에 산다. 물리 호스트에는 아무것도 두지 않는다 —
|
|
> 그래야 `virsh undefine kc-lab-edge` 한 줄로 이 계층을 통째로 되돌릴 수 있다.
|
|
|
|
이 문장은 「게스트를 통째로 지우면 같이 없어진다」이고 걷어내는 절차가 아니다. 순서도, 사전 조건도, 지운 뒤 무엇을 확인하는지도 없고 이 실험대가 그렇게 지워 본 기록도 없다. 이 단계가 엣지에 남기는 것은 넷이다.
|
|
|
|
| 남는 것 | 어디에 |
|
|
|---|---|
|
|
| 패키지 둘 | 게스트 패키지 데이터베이스 |
|
|
| `cloudflare.ini` · 인증서 묶음 · deploy 훅 | `/etc/letsencrypt/` 아래 |
|
|
| `certbot-renew.timer` 활성화 | systemd |
|
|
| 443 블록 | `/etc/nginx/sites-available/keycloak-lab` |
|
|
|
|
**Cloudflare 쪽 토큰은 엣지를 지워도 계정에 남는다**(external). 토큰은 그쪽 서버가 들고 있고 엣지에 있던 것은 사본이다. 가이드는 그 폐기를 적지 않았다(unknown).
|
|
|
|
## 세우기 전에 먼저 본다
|
|
|
|
두 확인은 아무것도 바꾸지 않는다. 하나는 검증 방식을 정하고, 하나는 5번에서 쓸 설정 문법을 가른다.
|
|
|
|
여기부터 6번까지가 엣지 게스트 셸이다. lab host 에서 들어간다.
|
|
|
|
```bash label="[lab host] 엣지 게스트에 들어간다"
|
|
ssh kc-lab-edge
|
|
```
|
|
|
|
### 확인 ① 이 도메인이 무엇으로 풀리는가
|
|
|
|
```bash label="[kc-lab-edge] 도메인이 어느 주소로 풀리는지 본다"
|
|
getent hosts auth.hyeonworks.com
|
|
```
|
|
|
|
**실측**(observed)
|
|
|
|
```text
|
|
100.83.212.4 auth.hyeonworks.com
|
|
```
|
|
|
|
**여기에 `dig` 를 바로 쓰지 않는다.** 엣지 게스트에는 `dig` 가 없다 — 2026-09-17 에 cloud-init 으로 갓 만든 `kc-lab-edge` 에서 쳐서 확인했다(observed).
|
|
|
|
```text
|
|
dig: command not found
|
|
```
|
|
|
|
cloud-init 이 까는 것은 `curl` 과 `nftables` 뿐이라 `dnsutils` 가 안 들어간다. `getent hosts` 는 `libc` 가 주는 것이라 어디서나 있고 주소와 이름을 한 줄로 낸다. `dig` 의 형태가 꼭 필요하면 먼저 깐다 — 둘 다 같은 `100.83.212.4` 를 냈다(observed).
|
|
|
|
```bash label="[kc-lab-edge] dig 를 쓰려면 먼저 깐다"
|
|
sudo apt install -y dnsutils && dig +short auth.hyeonworks.com
|
|
```
|
|
|
|
**어디를 봐야 하는가** — 주소 한 줄. 그 주소가 공개 인터넷 대역인가, `100.64.0.0/10` 안인가.
|
|
|
|
**이 결과가 의미하는 것** — `100.64.0.0/10` 은 CGNAT 용으로 예약된 대역이라 공개 인터넷에서 라우팅 자체가 되지 않는다(external). 방화벽을 여는 문제가 아니라 그 주소가 인터넷에 존재하지 않고, Let's Encrypt 를 tailnet 에 초대할 방법도 없다. 그래서 HTTP-01 은 쓸 수 없고 DNS-01 을 쓴다. 여기서 공개 주소가 나오는 환경이라면 위 표의 왼쪽을 고르는 편이 낫다.
|
|
|
|
### 확인 ② nginx 판 번호는 몇인가
|
|
|
|
```bash label="[kc-lab-edge] 이 게스트의 nginx 판 번호"
|
|
sudo nginx -v
|
|
```
|
|
|
|
**`sudo` 가 붙는 까닭은 앞 단계와 같다.** 실행 파일이 `/usr/sbin/nginx` 인데 데비안 일반 사용자 `PATH` 에 그 디렉터리가 없다. `sudo` 없이 치면 `nginx: command not found` 가 나오고, 안 깔린 것처럼 보이지만 아니다(2026-09-17, observed).
|
|
|
|
**실측**(observed) — 같은 설정인데 배포판에서 갈린다.
|
|
|
|
```text
|
|
엣지 (Debian 12): nginx version: nginx/1.22.1
|
|
물리 호스트 (Arch): nginx version: nginx/1.30.4
|
|
```
|
|
|
|
**어디를 봐야 하는가** — `1.25.1` 이 경계다. 지금 셸의 번호가 그 위인가 아래인가.
|
|
|
|
**이 결과가 의미하는 것** — 1.25.1 미만이면 `http2 on;` 지시어가 없다. 5번의 설정처럼 `listen` 의 파라미터로 쓰면 1.22 와 1.30 양쪽에서 다 돈다. 지시어 형태로 쓰면 이렇게 막힌다.
|
|
|
|
```text
|
|
[emerg] 1287#1287: unknown directive "http2" in /etc/nginx/sites-enabled/keycloak-lab:14
|
|
nginx: configuration file /etc/nginx/nginx.conf test failed
|
|
```
|
|
|
|
## 실행 절차
|
|
|
|
### 1. certbot 과 Cloudflare 플러그인을 깐다
|
|
|
|
**목적** — DNS-01 검증을 할 수 있는 상태로 만든다.
|
|
|
|
```bash label="[kc-lab-edge] ① 두 패키지를 깐다"
|
|
sudo apt install -y certbot python3-certbot-dns-cloudflare
|
|
```
|
|
|
|
```bash label="[kc-lab-edge] ② 쓸 수 있는 검증 방식이 무엇인지 본다"
|
|
certbot plugins 2>/dev/null | grep -E '^\*'
|
|
```
|
|
|
|
**예상 결과**(observed)
|
|
|
|
```text
|
|
* dns-cloudflare
|
|
* standalone
|
|
* webroot
|
|
```
|
|
|
|
`^\*` 로 거른 것은 certbot 이 쓸 수 있다고 표시한 플러그인 앞에 별표를 붙이기 때문이다.
|
|
|
|
**왜 필요한가** — `dns-cloudflare` 가 이 목록에 없으면 4번의 발급이 `unrecognized arguments: --dns-cloudflare` 로 끝난다. 플러그인은 `certbot` 본체와 별개 패키지라 한쪽만 깔려 있어도 `certbot` 명령 자체는 돈다.
|
|
|
|
**문제가 생기면** — 목록에 없으면 ① 의 뒤엣것이 깔렸는지 본다. cloud-init 이 이미 깔았다면 이 단계를 건너뛴다.
|
|
|
|
### 2. Cloudflare 토큰을 발급받는다
|
|
|
|
**목적** — certbot 이 인증용 TXT 레코드를 직접 만들었다 지운다. 그래서 DNS 쓰기 권한이 필요하다.
|
|
|
|
브라우저에서 다섯 단계다.
|
|
|
|
1. `https://dash.cloudflare.com/profile/api-tokens` 에서 **Create Token**
|
|
2. **`Edit zone DNS`** 템플릿을 고르고 **Use template**
|
|
3. **Permissions** 는 `Zone` · `DNS` · `Edit` — 템플릿이 채워 준 그대로
|
|
4. **Zone Resources** 는 `Include` · `Specific zone` · **`hyeonworks.com`**
|
|
5. **Continue to summary** 다음 **Create Token**
|
|
|
|
**예상 결과** — 토큰 값이 화면에 한 번 나온다. 창을 닫으면 복구가 없으므로 바로 3번으로 넘어간다.
|
|
|
|
**왜 필요한가** — `All zones` 로 두면 계정의 모든 도메인에 대한 DNS 수정 권한이 엣지 게스트 안 평문 파일에 놓인다. Global API Key 는 더 나쁘다 — 계정 전체 만능 키라 폐기하면 그 키를 쓰던 다른 것들이 전부 같이 죽는다. 권한을 어디까지 좁히고 무엇을 감수했는지는 이 절차를 부른 결정이 갖고 있다.
|
|
|
|
**문제가 생기면** — 값을 놓쳤으면 다시 발급받는다. 이 문서에도 터미널에도 값을 적지 않는다.
|
|
|
|
### 3. 자격증명 파일을 만들고 권한부터 좁힌다
|
|
|
|
**목적** — certbot 이 읽을 자격증명을 root 만 읽을 수 있는 파일에 둔다.
|
|
|
|
```bash label="[kc-lab-edge] ① 빈 파일을 600 으로 먼저 만든다"
|
|
sudo install -m 600 /dev/null /etc/letsencrypt/cloudflare.ini
|
|
```
|
|
|
|
```bash label="[kc-lab-edge] ② 그다음에 토큰을 쓴다"
|
|
sudo nano /etc/letsencrypt/cloudflare.ini
|
|
```
|
|
|
|
③ 아래의 `{{CLOUDFLARE_API_TOKEN}}` 을 2번에서 화면에 한 번 나온 토큰 값으로 바꿔 쓴다. 발급받을 때마다 값이 달라서 여기 적을 수 없다. 저장은 `Ctrl+O` 다음 `Enter`, 나가기는 `Ctrl+X`.
|
|
|
|
```ini label="③ cloudflare.ini 에 쓸 내용"
|
|
# file: /etc/letsencrypt/cloudflare.ini
|
|
dns_cloudflare_api_token = {{CLOUDFLARE_API_TOKEN}}
|
|
```
|
|
|
|
```bash label="[kc-lab-edge] ④ 권한과 크기만 본다"
|
|
ls -l /etc/letsencrypt/cloudflare.ini
|
|
sudo wc -c /etc/letsencrypt/cloudflare.ini
|
|
```
|
|
|
|
```bash label="[kc-lab-edge] ⑤ 토큰이 살아 있고 권한 범위가 맞는지 묻는다"
|
|
CF=$(sudo awk -F'= *' '/api_token/{print $2}' /etc/letsencrypt/cloudflare.ini)
|
|
curl -s https://api.cloudflare.com/client/v4/user/tokens/verify -H "Authorization: Bearer $CF"
|
|
```
|
|
|
|
**예상 결과** — ④ 는 `-rw-------` 이고 바이트 수가 0 이 아니다. `sudo` 없이 `wc` 를 치면 `Permission denied` 가 나는데 그것이 정상이다. ⑤ 의 응답에는 `"status":"active"` 와 `"success":true` 가 들어 있다. 값 자체는 셸 변수에만 담기고 화면에 찍히지 않는다.
|
|
|
|
**왜 필요한가** — 두 명령의 순서가 그 자체로 보안 조치다. `install -m 600 /dev/null` 은 빈 파일을 `600` 으로 만드는 명령이고, 토큰을 먼저 쓰고 나서 권한을 고치면 그사이에 파일이 열려 있다. ⑤ 를 여기서 해 두면 뒤에서 실패했을 때 DNS 문제인지 토큰 문제인지를 헷갈리지 않는다.
|
|
|
|
**문제가 생기면** — 권한이 `-rw-r--r--` 면 같은 게스트의 다른 사용자도 토큰을 읽는다. 바이트 수가 0 이면 편집기에서 저장을 안 했거나 다른 경로에 썼다. ⑤ 의 응답에 `"code":6003` 이 보이면 토큰 값이 틀렸거나 잘렸고, `"code":9109` 면 권한 범위가 모자라다.
|
|
|
|
### 4. dry-run 을 먼저 돌리고 그다음에 발급한다
|
|
|
|
**목적** — 와일드카드를 포함한 인증서를 받는다. 실패를 시험용 한도에서 먼저 만난다.
|
|
|
|
```bash label="[kc-lab-edge] ① 먼저 시험한다"
|
|
sudo certbot certonly --dns-cloudflare \
|
|
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
|
|
-d hyeonworks.com -d '*.hyeonworks.com' --dry-run
|
|
```
|
|
|
|
```bash label="[kc-lab-edge] ② 실제로 받는다"
|
|
sudo certbot certonly --dns-cloudflare \
|
|
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
|
|
-d hyeonworks.com -d '*.hyeonworks.com'
|
|
```
|
|
|
|
```bash label="[kc-lab-edge] ③ 무엇을 받았는지 본다"
|
|
sudo certbot certificates
|
|
```
|
|
|
|
```bash label="[kc-lab-edge] ④ 인증서가 실제로 어떤 이름에 유효한지 본다"
|
|
sudo openssl x509 -noout -ext subjectAltName -in /etc/letsencrypt/live/hyeonworks.com/fullchain.pem
|
|
```
|
|
|
|
**예상 결과** — ① 의 마지막 줄이 `The dry run was successful.` 이고, ② 는 `Successfully received certificate.` 와 저장 경로를 찍는다. ③ 에서 볼 것은 네 줄이다.
|
|
|
|
| 줄 | 값 |
|
|
|---|---|
|
|
| `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` |
|
|
|
|
④ 는 `DNS:hyeonworks.com, DNS:*.hyeonworks.com` 두 개를 내놓고 `auth.hyeonworks.com` 은 두 번째에 걸린다.
|
|
|
|
**왜 필요한가** — dry-run 을 먼저 도는 것은 Let's Encrypt 의 주당 중복 인증서 5장 한도를 dry-run 이 쓰지 않기 때문이다(external). dry-run 은 스테이징 서버에 대고 시험만 하므로 `/etc/letsencrypt/live/` 에는 아무것도 안 생기고, 그 직후 `certbot certificates` 가 `No certificates found` 를 내는 것이 정상이다. `-d` 를 둘 주는 것은 와일드카드가 한 단계만 덮기 때문이고, `a.b.hyeonworks.com` 도 apex 인 `hyeonworks.com` 자신도 `*.hyeonworks.com` 에 들어가지 않는다.
|
|
|
|
디렉터리 이름과 인증서가 덮는 이름은 별개다(observed). 여기서 가장 많이 헷갈린다.
|
|
|
|
| 무엇 | 정해지는 방식 |
|
|
|---|---|
|
|
| 디렉터리 이름 `live/hyeonworks.com/` | certbot 이 이 묶음을 관리하려고 붙인 라벨. 첫 번째 `-d` 에서 따오고 서빙과 무관하다 |
|
|
| SAN 목록 | 브라우저가 보는 실제 유효 호스트명. `-d` 로 준 이름 전부 |
|
|
|
|
**문제가 생기면** — 최초 실행이면 계정 등록 대화가 먼저 뜬다. 이메일을 비우면 `Invalid email address: .` 로 되묻고, 약관은 `Y`, 뉴스레터는 발급과 무관하므로 `N` 이다. 프롬프트 없이 돌리려면 `-m <이메일> --agree-tos --no-eff-email` 을 붙인다. `--register-unsafely-without-email` 은 쓰지 않는다 — 갱신 실패를 알려 줄 통로가 사라지는데, 6번이 재는 것이 바로 그 갱신이다. DNS-01 은 TXT 레코드가 퍼질 때까지 기다리느라 수십 초 걸리므로 중간에 끊지 않는다.
|
|
|
|
### 5. nginx 에 443 을 얹는다
|
|
|
|
**목적** — 앞 단계에서는 80 만 세웠다. 인증서가 생겼으니 443 블록을 더하고 80 은 리다이렉트로 바꾼다.
|
|
|
|
```bash label="[kc-lab-edge] ① 앞 단계에서 쓴 그 파일을 연다"
|
|
sudo nano /etc/nginx/sites-available/keycloak-lab
|
|
```
|
|
|
|
```nginx label="② keycloak-lab 을 이 내용으로 바꾼다"
|
|
# file: /etc/nginx/sites-available/keycloak-lab
|
|
upstream k3s_traefik {
|
|
server 192.168.122.11:80;
|
|
server 192.168.122.12:80;
|
|
}
|
|
|
|
server {
|
|
listen 80 default_server;
|
|
server_name _;
|
|
return 301 https://$host$request_uri;
|
|
}
|
|
|
|
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_protocols TLSv1.2 TLSv1.3;
|
|
|
|
location / {
|
|
proxy_pass http://k3s_traefik;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Forwarded-Host $host;
|
|
proxy_set_header X-Forwarded-Proto https;
|
|
proxy_set_header X-Forwarded-Port 443;
|
|
proxy_set_header X-Forwarded-For $remote_addr;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
}
|
|
}
|
|
```
|
|
|
|
```bash label="[kc-lab-edge] ③ 앞 단계에서 쓴 그 두 줄로 다시 읽힌다"
|
|
sudo nginx -t && sudo systemctl reload nginx
|
|
```
|
|
|
|
**예상 결과** — `syntax is ok` 와 `test is successful` 두 마디가 나오고 reload 가 돈다. 앞에 붙는 `types_hash` 경고는 통과를 막지 않는다.
|
|
|
|
**왜 필요한가** — 앞 단계의 파일에서 바뀐 곳이 셋이다.
|
|
|
|
| 줄 | 앞 단계에서는 | 지금 |
|
|
|---|---|---|
|
|
| 80 블록 | `location / { proxy_pass … }` | 리다이렉트만 |
|
|
| 443 블록 | 없었다 | 인증서와 함께 새로 |
|
|
| `X-Forwarded-Proto` | `http` | `https` |
|
|
|
|
`ssl_certificate` 에 적는 것은 `cert.pem` 이 아니라 `fullchain.pem` 이다. 서버 인증서만 보내면 중간 인증서가 빠져 체인이 끊기는데, 브라우저는 대개 캐시나 AIA 로 보완해서 정상으로 보이고 캐시가 없는 클라이언트에서만 깨지므로 발견이 늦다. 4번 ③ 에서 certbot 이 찍어 준 경로와 한 글자도 다르면 안 된다.
|
|
|
|
**★ 위 ② 의 인증서 경로가 틀렸다**(2026-09-17, observed). `live/hyeonworks.com/` 이라고 적혀 있는데 certbot 이 만드는 디렉터리는 **`live/auth.hyeonworks.com/`** 이다. 이름을 여럿 담은 인증서라도 디렉터리 이름은 `-d` 로 처음 준 이름 하나를 쓴다. 그대로 치면 3번에서 nginx 가 안 뜬다.
|
|
|
|
```text label="문서 그대로 쳤을 때"
|
|
[emerg] cannot load certificate "/etc/letsencrypt/live/hyeonworks.com/fullchain.pem":
|
|
BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory
|
|
nginx: configuration file /etc/nginx/nginx.conf test failed
|
|
```
|
|
|
|
```bash label="[kc-lab-edge] 실제 디렉터리 이름을 보고 쓴다"
|
|
sudo ls /etc/letsencrypt/live/
|
|
sudo certbot certificates | grep 'Certificate Path'
|
|
```
|
|
|
|
```text label="그 출력"
|
|
README
|
|
auth.hyeonworks.com
|
|
Certificate Path: /etc/letsencrypt/live/auth.hyeonworks.com/fullchain.pem
|
|
```
|
|
|
|
**아래 「문제가 생기면」의 진단은 방향이 거꾸로다.** `live/auth.hyeonworks.com/` 이 없는 경로가 아니라 **그쪽이 맞는 경로**이고, `live/hyeonworks.com/` 이 디스크에 없다. 4번 ③ 이 찍어 준 경로를 그대로 옮기라는 원칙은 옳고, 위 ② 가 그 원칙을 스스로 어겼다.
|
|
|
|
**문제가 생기면** — `cannot load certificate` 로 막히면 경로를 본다. 자기 실험대의 디렉터리 이름은 `sudo ls /etc/letsencrypt/live/` 가 답한다. 확인 ② 에서 본 판 번호가 1.25.1 미만인데 `http2` 를 지시어로 썼다면 여기서 `unknown directive "http2"` 가 나온다.
|
|
|
|
### 6. 갱신이 서빙까지 닿게 한다
|
|
|
|
**목적** — 타이머가 도는 것만으로는 부족하다. 갱신된 인증서를 누가 읽게 만드는 일까지 세운다. 여기가 이 단계의 알맹이다.
|
|
|
|
```bash label="[kc-lab-edge] ① 타이머가 있는지 본다"
|
|
systemctl list-timers certbot-renew.timer
|
|
```
|
|
|
|
```bash label="[kc-lab-edge] ② 표가 비어 나오면 이름이 다른지 찾는다"
|
|
systemctl list-timers --all | grep -i certbot
|
|
```
|
|
|
|
```bash label="[kc-lab-edge] ③ 배포판 기본 유닛이 reload 를 부르는지 본다"
|
|
systemctl cat certbot-renew.service
|
|
```
|
|
|
|
```text
|
|
[Service]
|
|
Type=oneshot
|
|
ExecStart=/usr/bin/certbot -q renew
|
|
PrivateTmp=true
|
|
```
|
|
|
|
```bash label="[kc-lab-edge] ④ 훅 스크립트를 연다"
|
|
sudo nano /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
|
|
```
|
|
|
|
```sh label="⑤ reload-nginx.sh 에 쓸 내용"
|
|
# file: /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
|
|
#!/bin/sh
|
|
nginx -t && nginx -s reload
|
|
```
|
|
|
|
```bash label="[kc-lab-edge] ⑥ 실행 권한을 준다"
|
|
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
|
|
```
|
|
|
|
```bash label="[kc-lab-edge] ⑦ 실행 파일이 됐는지 본다"
|
|
ls -l /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
|
|
```
|
|
|
|
**예상 결과** — ① 은 `NEXT` 와 `LEFT` 가 채워져 있고 `ACTIVATES` 가 `certbot-renew.service` 를 가리킨다. ③ 에는 `ExecStartPost` 도 `--deploy-hook` 도 없고, 여기 없는 것을 보는 것이 그 명령의 목적이다. ⑦ 의 권한 문자열에는 `x` 가 세 번 보인다.
|
|
|
|
**왜 필요한가** — 타이머가 `active` 여도 갱신된 인증서가 서빙되지는 않는다. nginx 는 인증서를 기동 시점에 읽어 메모리에 들고 있고 certbot 은 `live/` 심볼릭 링크만 갈아 끼우므로, 설정에 적힌 경로는 그대로이고 그 경로가 가리키는 파일만 바뀌어 nginx 에는 다시 읽을 계기가 생기지 않는다. 배포판 기본 유닛은 인증서를 새로 받는 데까지만 책임지고 받은 것을 누가 읽게 만드는 일은 아무도 하지 않는다. 훅이 그 둘을 잇는다. 훅을 `post/` 가 아니라 `deploy/` 에 넣는 것은 `post/` 가 갱신이 없어도 매번 돌아 하루 두 번 워커를 갈아치우기 때문이고, `deploy/` 는 실제로 갱신됐을 때만 실행된다. 실행 권한이 없으면 certbot 이 이 훅을 조용히 건너뛴다.
|
|
|
|
**문제가 생기면** — ③ 에 무엇인가 적혀 있는 배포판이라면 이 훅은 필요 없고, 대신 그 명령이 nginx 를 reload 하는지만 확인한다. 갱신은 됐는데 옛 인증서가 나가면 ⑦ 부터 다시 본다.
|
|
|
|
## 구성 값
|
|
|
|
이 훅이 있고 없고의 차이를 이 실험대가 쟀다(observed).
|
|
|
|
| 무엇 | 훅 없음 | 훅 있음 |
|
|
|---|---|---|
|
|
| 갱신에서 서빙까지 | 2305초 (38분 25초) | 1~2초 |
|
|
| 무엇이 reload 했나 | 사람이 직접 | certbot deploy 훅 |
|
|
| 아무도 안 했다면 | 다음 nginx 재시작까지 = 사실상 무기한 | — |
|
|
|
|
88일 동안 이 결함이 보이지 않는다. 타이머는 정상이고 매번 `SUCCESS` 로 끝나며, 만료 30일 전까지는 갱신 자체를 하지 않아 발현할 기회가 없다. 발현하는 날의 증상은 인증서 만료이고, 그날에도 로그에는 `SUCCESS` 라고 적혀 있다.
|
|
|
|
reload 자체는 무중단이었다(observed). 새 연결 8856건 전부 200 이었고 p95 는 205.7ms 대 204.3ms 로 변화가 없었으며, 845KB 를 받는 중이던 요청이 전송 12초째에 reload 를 맞고도 845361바이트를 온전히 받았다. 옛 워커가 그 요청을 끝까지 책임진다.
|
|
|
|
## 끝났는지 판정한다
|
|
|
|
세 확인은 tailnet 에 붙은 다른 머신에서 친다. 1번부터 6번까지는 전부 엣지에서 쳤지만 판정은 밖에서 들어와야 의미가 있다.
|
|
|
|
### 확인 ① 열리나 — 처음 한 번은 협상 과정을 읽는다
|
|
|
|
**무엇을 확인하는가** — TLS 가 붙었는지. 응답 코드가 아니다.
|
|
|
|
```bash label="[워크스테이션] 협상 과정을 그대로 읽는다"
|
|
curl -v https://auth.hyeonworks.com/ -o /dev/null
|
|
```
|
|
|
|
경로는 `/` 다. Keycloak 은 다음 단계에서 올리므로 아직 Ingress 가 없고 `404` 가 정상이다. `/realms/master` 같은 Keycloak 경로를 여기서 쓰면 TLS 가 안 된 건지 Keycloak 이 없는 건지가 섞인다.
|
|
|
|
읽어야 할 줄만 옮긴다. 전체 출력은 이 실험대에서 캡처해 두지 않았다(unknown).
|
|
|
|
```text
|
|
* SSL connection using TLSv1.3 / ...
|
|
* subject: CN=hyeonworks.com
|
|
* issuer: C=US; O=Let's Encrypt; CN=...
|
|
* SSL certificate verify ok.
|
|
< HTTP/1.1 404 Not Found
|
|
```
|
|
|
|
**어디를 봐야 하는가** — 별표로 시작하는 줄 넷이다. 어떤 TLS 판으로 협상했는가, `subject` 의 CN 이 무엇인가, `issuer` 가 Let's Encrypt 인가, 그리고 `SSL certificate verify ok.` 가 있는가. 그 아래 `<` 로 시작하는 첫 줄이 응답 상태다. `subject` 가 `hyeonworks.com` 인 것이 맞다 — 와일드카드 인증서라 CN 은 apex 이름이고 `auth.hyeonworks.com` 은 SAN 의 `*.hyeonworks.com` 에 걸린다.
|
|
|
|
**★ 그런데 이 실험대에서는 CN 이 `auth.hyeonworks.com` 이다**(2026-09-17, observed). 가이드대로 와일드카드를 받으면 위 두 문단처럼 보인다. 이 실험대는 와일드카드를 안 받았고(§「가이드는 여기에 값을 하나 더 적어 두었다」), 계보도 `auth.hyeonworks.com` 하나만 있다.
|
|
|
|
```bash label="[lab host] 엣지가 내놓는 인증서가 어느 이름을 덮는가"
|
|
echo | openssl s_client -connect 192.168.122.10:443 -servername auth.hyeonworks.com 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName -dates
|
|
```
|
|
|
|
```text label="이 실험대의 값"
|
|
subject=CN=auth.hyeonworks.com
|
|
X509v3 Subject Alternative Name:
|
|
DNS:app1.hyeonworks.com, DNS:app2.hyeonworks.com, DNS:auth.hyeonworks.com
|
|
notBefore=Sep 4 11:29:18 2026 GMT
|
|
notAfter=Dec 3 11:29:17 2026 GMT
|
|
```
|
|
|
|
**CN 은 합격 조건이 아니다.** 와일드카드를 받았으면 `hyeonworks.com`, 이름을 나열해 받았으면 첫 번째 `-d` 의 이름이 CN 이 된다. 보는 것은 `SSL certificate verify ok.` 한 줄과, 지금 친 이름이 SAN 에 있는가다.
|
|
|
|
**이름 셋 밖은 TLS 단계에서 끝난다.** `curl` 은 종료 코드 `60` 을 내고 `%{http_code}` 는 `000` 이 되는데, 화면에서는 「서버가 죽었다」와 같아 보인다. A-4 의 Grafana 탐침이 그렇게 쓸모없어졌다.
|
|
|
|
**이 결과가 의미하는 것** — 네 줄이 다 나오면 인증서가 붙었고 체인이 클라이언트 기준으로 검증됐다. 인증서를 처음 붙인 직후에는 코드 한 칸이 아니라 이 화면을 본다. `verify` 줄 대신 `unable to get local issuer certificate` 가 나오면 중간 인증서가 빠진 것이고, 원인은 5번의 인증서 파일 이름이라 확인 ② 로 간다.
|
|
|
|
같은 것을 반복해서 재거나 앞뒤 단계의 값과 나란히 비교할 때만 값만 뽑는 형태로 줄인다.
|
|
|
|
```bash label="[워크스테이션] 코드와 검증 결과만 뽑는다"
|
|
curl -s -o /dev/null -w '%{http_code} tls=%{ssl_verify_result}\n' https://auth.hyeonworks.com/
|
|
```
|
|
|
|
**실측**(observed) — 2026-09-11, tailnet 클라이언트에서
|
|
|
|
```text
|
|
404 tls=0
|
|
```
|
|
|
|
`tls=0` 이 인증서 검증 통과다. 앞의 `404` 는 다음 단계 이후에 `200` 으로 바뀐다.
|
|
|
|
### 확인 ② 체인 단계와 검증
|
|
|
|
**무엇을 확인하는가** — 서버가 중간 인증서까지 보내는지.
|
|
|
|
```bash label="[워크스테이션] 체인 단계를 센다"
|
|
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'
|
|
```
|
|
|
|
**실측**(observed) — 이름을 따로 받던 시절의 기록이라 0번 `s:` 가 `auth.hyeonworks.com` 이다. 와일드카드로 받으면 `CN = hyeonworks.com` 이 되고, 봐야 할 구조는 똑같다.
|
|
|
|
```text
|
|
0 s:CN = auth.hyeonworks.com
|
|
i:C = US, O = Let's Encrypt, CN = YE2
|
|
1 s:C = US, O = Let's Encrypt, CN = YE2
|
|
i:C = US, O = ISRG, CN = Root YE
|
|
2 s:C = US, O = ISRG, CN = Root YE
|
|
i:C = US, O = Internet Security Research Group, CN = ISRG Root X2
|
|
3 s:C = US, O = Internet Security Research Group, CN = ISRG Root X2
|
|
i:C = US, O = Internet Security Research Group, CN = ISRG Root X1
|
|
Verify return code: 0 (ok)
|
|
```
|
|
|
|
**어디를 봐야 하는가** — 왼쪽의 번호가 몇까지 가는가, 그리고 각 단계의 발급자 줄이 다음 단계의 주체 줄과 같은가. 마지막이 `Verify return code: 0 (ok)` 인가.
|
|
|
|
**이 결과가 의미하는 것** — 단계가 1개면 `cert.pem` 을 쓴 것이다. 서버가 자기 인증서만 보내고 중간 인증서를 안 보낸 상태인데, 이때 브라우저는 대개 캐시나 AIA 로 보완해서 정상으로 보이므로 이 명령이 유일하게 믿을 수 있는 판정이 된다. 고치는 곳은 5번의 `ssl_certificate` 한 줄이고, 고친 뒤 문법 검사와 reload 를 하고 여기서 다시 잰다. `Verify return code` 가 0 이 아니면 숫자마다 뜻이 다르다 — `10` 은 만료, `20` 은 발급자를 못 찾음, `21` 은 첫 인증서를 검증 못 함.
|
|
|
|
### 확인 ③ 이름 세 개가 한 인증서인가
|
|
|
|
**무엇을 확인하는가** — 이름마다 다른 인증서가 붙어 있지는 않은지.
|
|
|
|
```bash label="[워크스테이션] 세 이름의 일련번호를 나란히 뽑는다"
|
|
for H in auth app1 app2; do
|
|
echo | openssl s_client -connect $H.hyeonworks.com:443 -servername $H.hyeonworks.com 2>/dev/null \
|
|
| openssl x509 -noout -serial
|
|
done
|
|
```
|
|
|
|
**어디를 봐야 하는가** — 찍히는 세 줄의 일련번호가 서로 같은가. 값 자체에는 뜻이 없고 셋이 일치하는지만 본다.
|
|
|
|
**이 결과가 의미하는 것** — 셋이 같으면 SAN 하나에 이름 셋이 들어 있는 인증서 한 장이고, 갱신도 한 번에 끝난다. 다르면 인증서가 여러 장이라 갱신 훅도 장마다 따로 돌고, 한 장만 갱신됐을 때 나머지 이름이 만료되는 상황이 생긴다. 이름별로 무엇이 실려 있는지 보려면 SAN 을 직접 편다.
|
|
|
|
```bash label="[워크스테이션] 지금 서빙되는 인증서의 SAN 을 편다"
|
|
echo | openssl s_client -connect auth.hyeonworks.com:443 -servername auth.hyeonworks.com 2>/dev/null \
|
|
| openssl x509 -noout -ext subjectAltName
|
|
```
|
|
|
|
### 확인 ④ 갱신이 서빙까지 닿는가 — 워커 PID 로 본다
|
|
|
|
**무엇을 확인하는가** — 훅이 호출되는지, 그리고 호출된 훅이 nginx 를 정말 갈아 끼웠는지.
|
|
|
|
```bash label="[kc-lab-edge] 훅이 불리는지 먼저 본다. 상태를 바꾸지 않는다"
|
|
sudo certbot renew --dry-run
|
|
```
|
|
|
|
출력 끝의 `Running deploy-hook command` 줄과 `simulated renewals` 요약을 본다. 훅 줄이 아예 안 나오면 파일이 `deploy/` 가 아닌 곳에 있거나 실행 권한이 없다. dry-run 은 훅이 호출되는지까지만 말해 준다.
|
|
|
|
```bash label="[kc-lab-edge] 강제 갱신 전후로 워커를 비교한다. 이 확인은 상태를 바꾼다"
|
|
# 강제 갱신 전에 워커 PID 를 적어 둔다
|
|
ps -eo pid,lstart,args | grep 'nginx: worker' | grep -v grep
|
|
|
|
sudo certbot renew --force-renewal
|
|
|
|
# 워커 PID 가 바뀌었으면 reload 된 것이다
|
|
ps -eo pid,lstart,args | grep 'nginx: worker' | grep -v grep
|
|
```
|
|
|
|
`--force-renewal` 은 인증서를 실제로 새로 받으므로 발급 한도를 깎는다. 진짜 판정이 필요할 때만 한 번 쓴다.
|
|
|
|
**어디를 봐야 하는가** — 두 출력의 첫 열과 둘째 열 묶음이다. 앞뒤로 놓고 PID 집합이 통째로 바뀌었는지만 본다. `lstart` 를 같이 뽑는 것은 PID 가 우연히 재사용됐을 때를 가르기 위해서다. 마스터 프로세스는 그대로이고 워커만 갈리는 쪽이 정상이다.
|
|
|
|
**이 결과가 의미하는 것** — PID 가 바뀌었으면 훅이 돌아 nginx 가 새 인증서를 읽었다. 안 바뀌었으면 인증서는 갱신됐는데 서빙되는 것은 옛것이다. 판정은 로그 문구가 아니라 워커 PID 로 한다 — certbot 이 `Hook 'deploy-hook' ran with error output` 이라고 찍는데 실패가 아니다. nginx 의 `types_hash` 경고가 stderr 로 나갔을 뿐이고 내용은 `test is successful` 과 `signal process started` 다. 로그에서 `error` 를 grep 하는 감시를 걸면 성공한 훅을 실패로 오독한다.
|
|
|
|
## 통과 조건을 한 번에 다시 본다
|
|
|
|
| 무엇 | 명령 | 통과 |
|
|
|---|---|---|
|
|
| 열리는가 | `curl -v https://auth.hyeonworks.com/ -o /dev/null` | `SSL certificate verify ok.` |
|
|
| 검증값 | `curl -s -o /dev/null -w '%{http_code} tls=%{ssl_verify_result}\n' …` | `404 tls=0` |
|
|
| 체인 | `openssl s_client … \| grep …` | 번호가 3까지 · `Verify return code: 0 (ok)` |
|
|
| 이름 셋 | `for H in auth app1 app2; …` | 일련번호 세 줄이 같다 |
|
|
| 서빙까지 | 강제 갱신 앞뒤의 `ps … 'nginx: worker'` | 워커 PID 집합이 바뀐다 |
|
|
|
|
## 막히면
|
|
|
|
| 증상 | 원인 | 확인 |
|
|
|---|---|---|
|
|
| certbot 검증 실패 | DNS 가 이 호스트를 안 가리킴 · 80 이 막힘 | `getent hosts auth.hyeonworks.com` · 밖에서 `curl -I http://auth.hyeonworks.com` |
|
|
| 체인 단계가 1개 | `cert.pem` 을 씀 | 확인 ② |
|
|
| 갱신은 됐는데 옛 인증서가 나감 | deploy 훅 없음 | 워커 PID · `sudo ls /etc/letsencrypt/renewal-hooks/deploy/` |
|
|
| 훅이 실패한 것처럼 보임 | stderr 경고를 error 로 표시 | 문구 말고 워커 PID |
|
|
| 발급 한도 | 주당 중복 인증서 5장 | `--dry-run` 으로 먼저 시험 |
|
|
| `unrecognized arguments: --dns-cloudflare` | 플러그인 미설치 | `certbot plugins \| grep '^\*'` |
|
|
| 토큰 응답이 `"code":6003` · `"code":9109` | 토큰 값이 잘렸거나 권한 범위가 좁다 | Cloudflare 에서 토큰을 다시 발급한다 |
|
|
| DNS-01 이 오래 걸림 | TXT 전파 대기 | 정상이다. 끊지 않는다 |
|
|
| 재구축 뒤 인증서가 없음 | 발급하지 말고 백업을 되돌린다 | 한도를 아끼는 길이다 |
|
|
| `cannot load certificate` | 설정에 lineage 디렉터리 이름을 잘못 적었다 | `sudo certbot certificates` 의 `Certificate Path:` 와 대조 |
|
|
| 엣지에서 친 `curl` 이 `Connection refused` | 친 곳이 틀렸다 | tailnet 에 붙은 다른 머신에서 다시 친다 |
|
|
|
|
## 무엇이 관측이고 무엇이 아닌가
|
|
|
|
- (observed) 이름 풀이가 낸 `100.83.212.4`, `certbot plugins` 세 줄, `nginx -v` 두 줄, 체인 4단계와 `Verify return code: 0 (ok)`, `404 tls=0`, 배포판 기본 유닛에 훅이 없다는 것, 갱신에서 서빙까지 2305초 대 1~2초, reload 무중단 측정값.
|
|
- (observed) 2026-09-17 에 새로 만든 엣지에서 잰 것 — `dig` 가 없다는 것, `getent hosts` 와 `dnsutils` 설치 뒤의 `dig` 가 같은 주소를 낸다는 것, `nginx -v` 가 `sudo` 없이는 `command not found` 라는 것, `certbot plugins` 세 줄이 문서에 적힌 것과 같다는 것.
|
|
- (observed) **이 실험대의 certbot 은 DNS-01 로 등록되어 있다.** 위에서 「아직 읽지 못했다」고 적은 그 한 줄을 2026-09-17 에 읽었다 — `authenticator = dns-cloudflare` 이고 `dns_cloudflare_credentials = /etc/letsencrypt/cloudflare.ini` 이며 `server` 는 Let's Encrypt 운영 디렉터리다. 두 문서가 갈렸던 대목이 DNS-01 쪽으로 닫혔다.
|
|
|
|
```bash label="[lab host] 등록된 인증 방식을 읽는다"
|
|
sudo sh -c 'grep -H "authenticator\|dns_cloudflare_credentials\|server =" /etc/letsencrypt/renewal/*.conf'
|
|
```
|
|
|
|
**`sudo grep ... /etc/letsencrypt/renewal/*.conf` 로 치면 안 된다**(observed). 글로브를 일반 사용자 셸이 먼저 펼치는데 그 디렉터리를 못 읽어서 `zsh: no matches found` 로 끝나고 `grep` 은 시작도 안 한다. `sudo sh -c '...'` 로 sudo 안에서 펼쳐야 한다.
|
|
|
|
- (unknown) 2번부터 4번까지(토큰 발급 · 시험 발급 · 실제 발급)는 2026-09-17 에 다시 밟지 않았다. 5번의 443 블록은 이미 있던 인증서를 엣지로 옮겨서 밟았고, 그때 위 ② 의 경로 결함이 드러났다.
|
|
- (external) `100.64.0.0/10` 이 CGNAT 예약 대역이라는 것과 Let's Encrypt 의 발급 한도와 유효기간 90일은 규격이고 이 실험대가 잰 값이 아니다.
|
|
- (external) Cloudflare 계정에 남는 토큰은 엣지를 지운다고 없어지지 않는다. 가이드는 폐기를 적지 않았다.
|
|
- (inferred) 2305초 측정은 훅이 물리 호스트에만 있던 시절의 기록이라, 엣지 게스트에서 다시 잰 값이 아니다. 지금 배치에서 같은 수가 나오는지는 재지 않았다.
|
|
- (unknown) 체인 실측은 이름을 따로 받던 시절 것이고, 와일드카드로 받은 지금의 출력은 싣지 않았다.
|
|
- (unknown) `curl -v` 의 전체 출력, `certbot certificates` 의 실제 화면, 타이머 목록의 출력, 확인 ③ 의 일련번호 세 줄은 가이드가 봐야 할 줄만 적었고 값이 남아 있지 않다.
|
|
- (unknown) 원본 가이드 04 에 되돌리는 절차가 없다. 패키지와 자격증명과 인증서와 훅과 타이머와 443 블록을 걷어내 본 적이 없다.
|
|
- (unknown) 이 단계에는 「이 실험대는 이렇게 했다」와 「따라 하는 사람은」 두 갈래가 없다. 04 는 설정 파일 셋을 전부 편집기로 쓴다.
|
|
|
|
<!-- body:end -->
|