The experiment documents record what was found. These record what to type to
reproduce it, in folders per stage.
Two kinds of command are kept apart. 하기/확인 is what somebody actually types
at a terminal — short, one at a time. 근거를 재려면 is the long measuring form
this lab used to put evidence in a document, marked as not needed day to day.
The same split applies to curl: -I to look once, -w '%{http_code}' only when
comparing across repetitions.
No placeholders. Where a value is needed the command that produces it is
given, and secrets are checked by length rather than printed:
TOKEN=$(ssh kc-lab-1 'sudo cat /var/lib/rancher/k3s/server/node-token')
echo "${#TOKEN} 자"
Stage 05 verifies resources in layers, because a Secret existing and a pod
having received it are different facts: keys, then length, then the value
inside the container, then which env var came from which Secret. Same for
workloads — Deployment to ReplicaSet to Pod, with the seven ReplicaSets this
cluster actually carries as the worked example.
Two commands were wrong and re-running them caught it. kubectl get endpoints
prints a deprecation warning on v1.33+, so the guide uses describe svc and
EndpointSlice. And the Keycloak image has no curl, so reading metrics from
inside the container fails with exit 127 — the guide asks Prometheus instead,
or runs a throwaway curl pod.
Read-only checks were executed against the running lab and their output is
quoted verbatim. Creating commands could not be re-run without destroying the
lab, so they are the ones used at build time; the README says which is which.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7.8 KiB
04 — TLS
이 단계가 끝나면
https:// 가 열리고 체인이 완전하며, 갱신이 자동으로 서빙까지 닿는다.
전제
03 이 끝나 nginx 가 Traefik 으로 프록시한다. 그리고 공개 DNS 에 이름 세 개가 이 호스트를 가리키고 있어야 한다 — Let's Encrypt 가 HTTP-01 로 검증하러 오기 때문이다.
1. certbot 을 깐다
하기
sudo pacman -S certbot certbot-nginx # Arch
sudo apt install certbot python3-certbot-nginx # Debian/Ubuntu
2. 인증서를 받는다
이름 세 개를 한 인증서에 넣는다.
하기
sudo certbot certonly --webroot -w /var/www/html \
-d auth.hyeonworks.com -d app1.hyeonworks.com -d app2.hyeonworks.com
확인
sudo certbot certificates
이 실험대는 와일드카드를 쓰지 않았고, 그 비용이 나중에 청구됐다. B-7 에서 oauth2-proxy 를 올릴 네 번째 이름이 없어 Grafana 의
app2를 빌려야 했다. 와일드카드는 DNS-01 검증이 필요하고 그건 DNS 공급자 API 를 붙여야 한다 — 그 절충을 안 한 결과다.
3. nginx 가 fullchain 을 보게 한다
03 의 설정에 이미 있다.
ssl_certificate /etc/letsencrypt/live/auth.hyeonworks.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/auth.hyeonworks.com/privkey.pem;
cert.pem 이 아니라 fullchain.pem. 서버 인증서만 보내면 중간 인증서가
빠져 체인이 끊긴다. 브라우저는 대개 캐시나 AIA 로 보완해서 정상으로 보이고,
캐시가 없는 클라이언트에서만 깨진다. 그래서 발견이 늦다.
하기
sudo nginx -t && sudo systemctl reload nginx
4. 확인 — 열리는가, 체인이 완전한가
확인 ① 열리나
curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master
200
확인 ② 체인 단계와 검증
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'
실측
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)
단계가 1개면 cert.pem 을 쓴 것이다.
확인 ③ 이름 세 개가 한 인증서인가
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 하나에 들어 있는 것이다.
5. ★ 갱신이 서빙까지 닿게 한다 — 여기가 이 단계의 핵심이다
타이머가 도는 것만으로는 부족하다.
확인 — 타이머
systemctl list-timers certbot-renew.timer
이것이 active 여도 갱신된 인증서가 서빙되지는 않는다. nginx 는 인증서를
기동 시점에 읽어 메모리에 들고 있고, certbot 은 live/ 심볼릭 링크만 갈아
끼운다. 경로는 그대로이고 내용만 바뀌므로 nginx 는 모른다.
배포판 기본 유닛에는 reload 를 부르는 것이 없다.
systemctl cat certbot-renew.service
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot -q renew
PrivateTmp=true
ExecStartPost 도 --deploy-hook 도 없다.
하기 — 훅 하나를 넣는다
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh > /dev/null <<'EOF'
#!/bin/sh
nginx -t && nginx -s reload
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
deploy/ 에 넣는다. post/ 는 갱신이 없어도 매번 돌아 하루 두 번 워커를
갈아치운다. deploy/ 는 실제로 갱신됐을 때만 실행된다.
확인 — 실제로 도는지
# 강제 갱신 전에 워커 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
판정은 로그 문구가 아니라 워커 PID 로 한다. certbot 이
Hook 'deploy-hook' ran with error output 이라고 찍는데 실패가 아니다 —
nginx 의 types_hash 경고가 stderr 로 나갔을 뿐이고 내용은
test is successful · signal process started 다.
로그에서
error를 grep 하는 감시를 걸면 성공한 훅을 실패로 오독한다.
실측 — 이 실험대에서 잰 차이
| 훅 없음 | 훅 있음 | |
|---|---|---|
| 갱신 → 서빙 | 2305초 (38분 25초) | 1~2초 |
| 무엇이 reload 했나 | 사람이 직접 | certbot deploy 훅 |
| 아무도 안 했다면 | 다음 nginx 재시작까지 = 사실상 무기한 | — |
88일 동안 이 결함이 보이지 않는다. 타이머는 정상이고 매번 SUCCESS 로
끝나며, 만료 30일 전까지는 갱신 자체를 하지 않아 발현할 기회가 없다.
발현하는 날의 증상은 인증서 만료이고, 그날에도 로그에는 SUCCESS 라고
적혀 있다.
원문: D-4 ·
D-4a ·
증거 evidence/d4-certificate-renewal/
6. reload 는 무중단인가 — 쟀다
궁금할 것이므로 결과만 적는다. 무중단이다.
새 연결 8856건 전부 200, p95 는 205.7ms 대 204.3ms 로 변화 없음. 그리고 845KB 를 20k/s 로 받는 중이던 요청이 전송 12초째에 reload 를 맞고도 845361바이트를 온전히 받았다(연결수 1). 옛 워커가 그 요청을 끝까지 책임진다.
막히면
| 증상 | 원인 | 확인 |
|---|---|---|
| certbot 검증 실패 | DNS 가 이 호스트를 안 가리킴 · 80 이 막힘 | dig +short <name> · 밖에서 curl http://<name> |
| 체인 단계가 1개 | cert.pem 을 씀 |
위 확인 ② |
| 갱신은 됐는데 옛 인증서가 나감 | deploy 훅 없음 | 워커 PID · sudo ls /etc/letsencrypt/renewal-hooks/deploy/ |
| 훅이 실패한 것처럼 보임 | stderr 경고를 error 로 표시 | 문구 말고 워커 PID |
| 발급 한도 | 주당 중복 인증서 5장 | --dry-run 으로 먼저 시험 |
근거를 재려면 (선택)
평소에는 필요 없다. 문서에 남길 근거가 필요할 때만 이렇게까지 한다.
갱신 중 가용성을 재려면 주입 전에 대조군부터 잡는다. 평시 오류율을 모르면 갱신 중에 나온 실패 한 건을 해석할 수 없다.
# 대조군 — 0.2초 × 900회 = 180초
i=0; while [ $i -lt 900 ]; do
curl -s -o /dev/null -w '%{http_code} %{time_total}\n' --max-time 5 \
https://auth.hyeonworks.com/realms/master
i=$((i+1)); sleep 0.2
done > /tmp/control.txt
awk '{print $1}' /tmp/control.txt | sort | uniq -c
이 실험대의 대조군은 900건 전부 200, 오류 0 이었다. 그래서 갱신 중 비200 이 한 번이라도 나오면 갱신 탓으로 귀속할 수 있었다.
그리고 시각을 비교할 때 시계를 확인한다. 이 실험대는 test-server 가 NTP 미동기로 106초 빨랐고, 보정하지 않은 첫 계산은 훅이 인증서 발급보다 104초 먼저 실행된 것이 되어 물리적으로 불가능했다.
A=$(date -u +%s.%N); B=$(ssh test-server 'date -u +%s.%N'); C=$(date -u +%s.%N)
# 왜곡 ≈ B − (A+C)/2 , 어느 쪽이 맞는지는 외부 기준으로 가른다
curl -sI https://www.google.com | grep -i '^date:'