450 lines
20 KiB
Markdown
450 lines
20 KiB
Markdown
# 04 — TLS
|
||
|
||
## 이 단계가 끝나면
|
||
|
||
`https://` 가 열리고 체인이 완전하며, 갱신이 자동으로 **서빙까지** 닿는다.
|
||
|
||
## 전제
|
||
|
||
[03](../03-nginx/) 이 끝나 엣지 nginx 가 Traefik 으로 프록시한다.
|
||
|
||
## 어디서 치는가
|
||
|
||
**이 단계는 전부 `[kc-lab-edge]` 에서 친다.** 인증서·certbot·갱신 타이머·
|
||
deploy 훅이 전부 엣지 게스트에 산다. 물리 호스트에는 아무것도 두지 않는다 —
|
||
그래야 `virsh undefine kc-lab-edge` 한 줄로 이 계층을 통째로 되돌릴 수 있다.
|
||
|
||
## ★ 검증 방식을 먼저 정한다 — HTTP-01 이냐 DNS-01 이냐
|
||
|
||
같은 Let's Encrypt 인증서인데 **「이 도메인이 네 것이냐」를 증명하는 방법**만
|
||
다르다. 그리고 이 실험대에서는 **선택의 여지가 없다.**
|
||
|
||
| | HTTP-01 | DNS-01 |
|
||
|---|---|---|
|
||
| 검증 방향 | Let's Encrypt **→ 우리 서버** (인바운드) | certbot **→ DNS 공급자 API** (아웃바운드) |
|
||
| 공개 인터넷에서 보여야 하나 | **그렇다** | 아니다 |
|
||
| 와일드카드 | 불가 | 가능 |
|
||
| 필요한 것 | 80 포트 · 공개 A 레코드 | DNS 공급자 API 토큰 |
|
||
|
||
**이 실험대는 공개 인터넷을 쓰지 않는다.** 도메인 세 개는 tailnet 주소를
|
||
가리킨다.
|
||
|
||
```bash
|
||
dig +short auth.hyeonworks.com
|
||
```
|
||
```
|
||
100.83.212.4
|
||
```
|
||
|
||
`100.64.0.0/10` 은 CGNAT 용으로 예약된 대역이라 **공개 인터넷에서 라우팅
|
||
자체가 되지 않는다.** 방화벽을 여는 문제가 아니라 그 주소가 인터넷에 존재하지
|
||
않는다. Let's Encrypt 를 tailnet 에 초대할 방법도 없다. **그래서 HTTP-01 은
|
||
쓸 수 없고 DNS-01 을 쓴다.**
|
||
|
||
> **공개 서버라면 HTTP-01 이 맞다.** 토큰도 DNS 연동도 필요 없어서 관리할
|
||
> 것이 적다. DNS-01 이 더 좋은 방식이어서 고르는 것이 아니라, HTTP-01 이
|
||
> 못 쓰이는 환경이라 고르는 것이다. 개념은
|
||
> [`session-lab-concepts.md`](../../session-lab-concepts.md) 의
|
||
> 「DNS-01 은 언제 쓰는가」.
|
||
|
||
---
|
||
|
||
## 1. certbot 을 깐다
|
||
|
||
cloud-init 이 이미 깔았다면 건너뛴다 —
|
||
[`kc-lab.yaml.example`](../../../deploy/lab/cloud-init/kc-lab.yaml.example) 의
|
||
`packages` 에 들어 있다.
|
||
|
||
**하기** — `[kc-lab-edge]`
|
||
```bash
|
||
sudo apt install -y certbot python3-certbot-dns-cloudflare
|
||
```
|
||
|
||
**확인** — 쓸 수 있는 검증 방식이 무엇인가
|
||
```bash
|
||
certbot plugins 2>/dev/null | grep -E '^\*'
|
||
```
|
||
|
||
**실측**
|
||
```
|
||
* dns-cloudflare
|
||
* standalone
|
||
* webroot
|
||
```
|
||
|
||
**어디를 봐야 하는가** — `dns-cloudflare` 한 줄이 있는가. 없으면 플러그인
|
||
패키지가 안 깔린 것이고, `--dns-cloudflare` 를 줘도 `unrecognized arguments`
|
||
로 끝난다.
|
||
|
||
## 2. 인증서를 받는다
|
||
|
||
DNS-01 이면 **와일드카드를 받을 수 있다.** 이 실험대는 처음에 이름 셋을
|
||
따로 받았고, 그 비용이 B-7 에서 청구됐다 — oauth2-proxy 를 올릴 네 번째
|
||
이름이 없어 Grafana 의 `app2` 를 빌려야 했다.
|
||
|
||
**하기** — `[kc-lab-edge]`. 토큰은 **존 하나 + DNS:Edit** 으로 좁힌다.
|
||
계정 전역 API Key 를 쓰지 않는다.
|
||
|
||
```bash
|
||
sudo install -m 600 /dev/null /etc/letsencrypt/cloudflare.ini
|
||
sudo tee /etc/letsencrypt/cloudflare.ini >/dev/null <<'EOF'
|
||
dns_cloudflare_api_token = <Cloudflare API 토큰>
|
||
EOF
|
||
|
||
sudo certbot certonly --dns-cloudflare \
|
||
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
|
||
-d hyeonworks.com -d '*.hyeonworks.com' --dry-run
|
||
```
|
||
|
||
**`--dry-run` 을 먼저 붙인다.** Let's Encrypt 는 같은 이름 조합에 대해
|
||
**주당 중복 인증서 5장** 제한이 있고, `--dry-run` 은 그 한도를 쓰지 않는다.
|
||
통과하면 `--dry-run` 만 떼고 다시 친다.
|
||
|
||
> **DNS-01 은 느리다.** TXT 레코드가 퍼질 때까지 기다려야 해서 발급이 수십
|
||
> 초 걸린다. certbot 이 기본 대기 시간을 두고 있으니 중간에 끊지 않는다.
|
||
|
||
**확인** — 인증서가 실제로 생겼고 이름이 다 들어갔는가
|
||
```bash
|
||
sudo certbot certificates
|
||
```
|
||
|
||
**어디를 봐야 하는가** — 네 줄이다. `Domains:` 에 **이름 세 개가 한 줄에**
|
||
나열되어 있는가(줄이 두 개로 갈라져 있으면 인증서가 두 장 만들어진 것이다),
|
||
`Expiry Date:` 가 오늘로부터 90일 뒤이고 `VALID` 인가, `Certificate Path:` 가
|
||
`/etc/letsencrypt/live/auth.hyeonworks.com/fullchain.pem` 인가, 그리고
|
||
`Private Key Path:` 가 그 옆의 `privkey.pem` 인가. **경로 두 개가 03 의
|
||
nginx 설정에 적은 것과 한 글자도 다르면 안 된다.**
|
||
|
||
**이 결과가 의미하는 것** — 여기 나온 경로가 곧 nginx 가 읽을 파일이다.
|
||
발급은 성공했는데 `No certificates found` 가 나오면 `sudo` 없이 쳐서 다른
|
||
사용자의 설정 디렉터리를 본 것이다. 발급 자체가 실패했다면 검증 단계에서
|
||
막힌 것이므로 DNS 와 80 포트부터 본다(아래 「막히면」).
|
||
|
||
> **이 실험대는 와일드카드를 쓰지 않았고, 그 비용이 나중에 청구됐다.**
|
||
> B-7 에서 oauth2-proxy 를 올릴 네 번째 이름이 없어 Grafana 의 `app2` 를
|
||
> 빌려야 했다. 와일드카드는 DNS-01 검증이 필요하고 그건 DNS 공급자 API 를
|
||
> 붙여야 한다 — 그 절충을 안 한 결과다.
|
||
|
||
## 3. nginx 가 `fullchain` 을 보게 한다
|
||
|
||
[03](../03-nginx/) 의 설정에 이미 있다.
|
||
|
||
```
|
||
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 로 보완해서 **정상으로 보이고**,
|
||
캐시가 없는 클라이언트에서만 깨진다. 그래서 발견이 늦다.
|
||
|
||
**하기**
|
||
```bash
|
||
sudo nginx -t && sudo systemctl reload nginx
|
||
```
|
||
|
||
## 4. 확인 — 열리는가, 체인이 완전한가
|
||
|
||
**확인 ①** 열리나 — **처음 한 번은 협상 과정을 읽는다**
|
||
```bash
|
||
curl -v https://auth.hyeonworks.com/realms/master -o /dev/null
|
||
```
|
||
|
||
**형태** (이 실험대에서 캡처해 두지 않았다 — 읽어야 할 줄만)
|
||
```
|
||
* SSL connection using TLSv1.3 / ...
|
||
* subject: CN=auth.hyeonworks.com
|
||
* issuer: C=US; O=Let's Encrypt; CN=...
|
||
* SSL certificate verify ok.
|
||
> GET /realms/master HTTP/1.1
|
||
< HTTP/1.1 200 OK
|
||
```
|
||
|
||
**어디를 봐야 하는가** — `*` 로 시작하는 줄 넷이다. 어떤 TLS 판으로
|
||
협상했는가, `subject` 의 CN 이 지금 친 이름과 같은가, `issuer` 가 Let's
|
||
Encrypt 인가, 그리고 **`SSL certificate verify ok.`** 가 있는가. 그 아래
|
||
`<` 로 시작하는 첫 줄이 응답 상태다. `-o /dev/null` 은 본문만 버리는 것이라
|
||
이 줄들은 그대로 남는다.
|
||
|
||
**이 결과가 의미하는 것** — 이 네 줄이 다 나오면 인증서가 붙었고 체인이
|
||
클라이언트 기준으로 검증됐다. TLS 에서 막힐 때 봐야 할 것이 전부 여기
|
||
있으므로, **인증서를 처음 붙인 직후에는 코드 한 칸이 아니라 이 화면을
|
||
본다.** `verify` 줄 대신 `unable to get local issuer certificate` 가 나오면
|
||
중간 인증서가 빠진 것이고, 그 원인은 3번의 `cert.pem`/`fullchain.pem`
|
||
이다 — 확인 ②로 간다.
|
||
|
||
같은 것을 반복해서 재거나 03·05 의 값과 나란히 비교할 때만 값만 뽑는
|
||
형태로 줄인다.
|
||
|
||
```bash
|
||
curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master
|
||
```
|
||
```
|
||
200
|
||
```
|
||
|
||
**확인 ②** 체인 단계와 검증
|
||
```bash
|
||
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)
|
||
```
|
||
|
||
**어디를 봐야 하는가** — 왼쪽의 **번호(0·1·2·3)가 몇까지 가는가**, 그리고
|
||
각 단계의 `i:`(발급자)가 **바로 다음 단계의 `s:`(주체)와 같은가**. 0번이
|
||
우리 서버 인증서이고, 위 실측에서 0의 `i:` 가 `CN = YE2` 인데 1의 `s:` 가
|
||
같은 `CN = YE2` 다 — 사슬이 이어져 있다는 뜻이다. 마지막이
|
||
`Verify return code: 0 (ok)`.
|
||
|
||
**이 결과가 의미하는 것** — **단계가 1개면 `cert.pem` 을 쓴 것이다.**
|
||
서버가 자기 인증서만 보내고 중간 인증서를 안 보낸 상태다. 이때 브라우저는
|
||
대개 캐시나 AIA 로 보완해서 **정상으로 보이므로**, 이 명령이 유일하게
|
||
믿을 수 있는 판정이다. 고치는 곳은 03 의 `ssl_certificate` 한 줄이고,
|
||
고친 뒤 `nginx -t && systemctl reload nginx` 하고 여기서 다시 잰다.
|
||
`Verify return code` 가 0 이 아니면 숫자마다 뜻이 다르다 —
|
||
`10` 은 만료, `20` 은 발급자를 못 찾음, `21` 은 첫 인증서를 검증 못 함.
|
||
|
||
**확인 ③** 이름 세 개가 한 인증서인가
|
||
```bash
|
||
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
|
||
echo | openssl s_client -connect auth.hyeonworks.com:443 -servername auth.hyeonworks.com 2>/dev/null \
|
||
| openssl x509 -noout -ext subjectAltName
|
||
```
|
||
|
||
## 5. ★ 갱신이 서빙까지 닿게 한다 — 여기가 이 단계의 핵심이다
|
||
|
||
**타이머가 도는 것만으로는 부족하다.**
|
||
|
||
**확인** — 타이머
|
||
```bash
|
||
systemctl list-timers certbot-renew.timer
|
||
```
|
||
|
||
**어디를 봐야 하는가** — 네 칸이다. `NEXT`(다음 실행 시각)와 `LEFT`(남은
|
||
시간)가 채워져 있는가, `LAST`/`PASSED` 가 하루 안쪽인가, `UNIT` 옆의
|
||
`ACTIVATES` 가 `certbot-renew.service` 를 가리키는가. **표가 통째로 비어
|
||
나오면 타이머가 없는 것이다** — 이름이 배포판마다 다르니
|
||
`systemctl list-timers --all | grep -i certbot` 로 찾는다.
|
||
|
||
**이 결과가 의미하는 것** — 여기까지가 「갱신이 돌기는 하는가」의 답이고,
|
||
대부분의 문서가 여기서 끝난다. **그런데 이것이 `active` 여도 갱신된 인증서가
|
||
서빙되지는 않는다.** nginx 는 인증서를
|
||
기동 시점에 읽어 메모리에 들고 있고, certbot 은 `live/` 심볼릭 링크만 갈아
|
||
끼운다. **경로는 그대로이고 내용만 바뀌므로 nginx 는 모른다.**
|
||
|
||
배포판 기본 유닛에는 reload 를 부르는 것이 없다.
|
||
|
||
```bash
|
||
systemctl cat certbot-renew.service
|
||
```
|
||
```
|
||
[Service]
|
||
Type=oneshot
|
||
ExecStart=/usr/bin/certbot -q renew
|
||
PrivateTmp=true
|
||
```
|
||
|
||
**어디를 봐야 하는가** — `ExecStart=` 한 줄과, 그 아래에 `ExecStartPost=` 가
|
||
**있는지 없는지**. 그리고 `ExecStart` 의 인자에 `--deploy-hook` 이 붙어
|
||
있는지. 여기 없는 것을 보는 것이 이 명령의 목적이다.
|
||
|
||
**이 결과가 의미하는 것** — `ExecStartPost` 도 `--deploy-hook` 도 없다.
|
||
즉 이 유닛은 **인증서를 새로 받는 데까지만** 책임지고, 받은 것을 누가
|
||
읽게 만드는 일은 아무도 하지 않는다. 배포판이 이렇게 준다는 것이 요점이다 —
|
||
「기본값이니 괜찮겠지」가 바로 이 결함의 서식지다. 여기 뭔가 적혀 있는
|
||
배포판이라면 아래 훅은 필요 없고, 대신 그 명령이 nginx 를 reload 하는지만
|
||
확인하면 된다.
|
||
|
||
**하기** — 훅 하나를 넣는다
|
||
```bash
|
||
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/lab/edge/reload-nginx.sh`](../../../deploy/lab/edge/reload-nginx.sh).
|
||
여기 손으로 치지 말고 그걸 밀어 넣는 편이 낫다.
|
||
|
||
```bash
|
||
cat deploy/lab/edge/reload-nginx.sh \
|
||
| ssh kc-lab-edge 'sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh >/dev/null'
|
||
ssh kc-lab-edge 'sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh'
|
||
```
|
||
|
||
> **이 훅은 한동안 저장소에 없었다.** 호스트에만 있어서, 호스트를 초기화하면
|
||
> **아무 오류 없이 사라지고** D-4 가 측정한 상태(갱신 성공 · 서빙 38분 25초
|
||
> 지연 · 타이머는 `SUCCESS`)로 되돌아갔다. 저장소에 두는 이유가 이것이다.
|
||
|
||
`deploy/` 에 넣는다. `post/` 는 갱신이 없어도 매번 돌아 하루 두 번 워커를
|
||
갈아치운다. `deploy/` 는 **실제로 갱신됐을 때만** 실행된다.
|
||
|
||
**확인** — 실제로 도는지
|
||
|
||
이 확인은 **상태를 바꾼다.** `--force-renewal` 은 인증서를 실제로 새로
|
||
받으므로 발급 한도(주당 중복 5장)를 깎는다. 먼저 `--dry-run` 으로 훅이
|
||
호출되는 것까지만 보고, 진짜 판정이 필요할 때만 강제 갱신을 한 번 쓴다.
|
||
|
||
```bash
|
||
sudo certbot renew --dry-run
|
||
```
|
||
|
||
**어디를 봐야 하는가** — 출력 끝의 `Running deploy-hook command` 줄과
|
||
`simulated renewals` 요약. 훅 줄이 아예 안 나오면 파일이 `deploy/` 가 아닌
|
||
곳에 있거나 실행 권한이 없는 것이다(`ls -l` 로 `x` 를 본다).
|
||
|
||
**이 결과가 의미하는 것** — dry-run 은 훅이 **호출되는지**까지만 말해 준다.
|
||
호출된 훅이 nginx 를 정말 갈아 끼웠는지는 dry-run 으로 알 수 없다 —
|
||
그래서 아래를 한 번 한다.
|
||
|
||
```bash
|
||
# 강제 갱신 전에 워커 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)과 둘째 열 묶음(lstart, 프로세스
|
||
시작 시각)**. 앞뒤로 놓고 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 하는 감시를 걸면 성공한 훅을 실패로 오독한다.**
|
||
|
||
**실측** — 이 실험대에서 잰 차이
|
||
|
||
| | 훅 없음 | 훅 있음 |
|
||
|---|---|---|
|
||
| 갱신 → 서빙 | **2305초 (38분 25초)** | **1~2초** |
|
||
| 무엇이 reload 했나 | 사람이 직접 | certbot deploy 훅 |
|
||
| 아무도 안 했다면 | 다음 nginx 재시작까지 = 사실상 무기한 | — |
|
||
|
||
**88일 동안 이 결함이 보이지 않는다.** 타이머는 정상이고 매번 `SUCCESS` 로
|
||
끝나며, 만료 30일 전까지는 갱신 자체를 하지 않아 발현할 기회가 없다.
|
||
발현하는 날의 증상은 **인증서 만료**이고, 그날에도 로그에는 `SUCCESS` 라고
|
||
적혀 있다.
|
||
|
||
원문: [D-4](../../experiment-d4-certificate-renewal.md) ·
|
||
[D-4a](../../experiment-d4a-deploy-hook.md) ·
|
||
증거 [`evidence/d4-certificate-renewal/`](../../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 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 '^\*'` |
|
||
| DNS-01 이 오래 걸림 | TXT 전파 대기 | **정상이다.** 끊지 않는다 |
|
||
| 재구축 뒤 인증서가 없음 | 발급하지 말고 **백업을 되돌린다** | 한도를 아끼는 길이다 |
|
||
|
||
---
|
||
|
||
## 근거를 재려면 (선택)
|
||
|
||
평소에는 필요 없다. **문서에 남길 근거가 필요할 때만** 이렇게까지 한다.
|
||
|
||
갱신 중 가용성을 재려면 **주입 전에 대조군부터** 잡는다. 평시 오류율을 모르면
|
||
갱신 중에 나온 실패 한 건을 해석할 수 없다.
|
||
|
||
```bash
|
||
# 대조군 — 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번을 재서 코드별로 세는 것이 목적이고,
|
||
헤더는 볼 일이 없다. 앞의 확인 ①과 형태가 다른 이유가 이것이다.
|
||
|
||
**어디를 봐야 하는가** — `uniq -c` 가 내놓는 **줄이 몇 개인가**. 한 줄이면
|
||
900번이 전부 같은 코드였다는 뜻이고, 그 줄의 왼쪽 수가 900 인지 본다.
|
||
두 줄 이상이면 그 자체가 평시 오류가 있다는 뜻이다. 응답 시간이 궁금하면
|
||
둘째 열을 따로 본다.
|
||
|
||
```bash
|
||
awk '{print $2}' /tmp/control.txt | sort -n | tail -1 # 최악값
|
||
```
|
||
|
||
**이 결과가 의미하는 것** — 이 실험대의 대조군은 **900건 전부 200, 오류 0**
|
||
이었다. 그래서 갱신 중 비200 이 한 번이라도 나오면 갱신 탓으로 귀속할 수
|
||
있었다. 대조군에 이미 오류가 섞여 있다면 주입 중의 오류 한 건은 아무것도
|
||
증명하지 못하므로, **대조군이 깨끗해질 때까지는 주입을 하지 않는다.**
|
||
|
||
**그리고 두 기계의 시각을 나란히 놓기 전에 시계부터 잰다.**
|
||
|
||
```bash
|
||
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:'
|
||
timedatectl show -p NTP -p NTPSynchronized
|
||
```
|
||
|
||
**어디를 봐야 하는가** — 세 수 `A`·`B`·`C` 를 눈으로 빼서 **초 단위 차이가
|
||
몇인가**. `A` 와 `C` 는 같은 기계에서 SSH 왕복 직전·직후에 찍은 것이라, 그
|
||
가운데가 「저쪽 시각을 잰 순간의 이쪽 시각」이다. 그다음 `Date:` 헤더의
|
||
시각이 둘 중 어느 쪽에 가까운가. 마지막으로 `NTPSynchronized=yes` 인가.
|
||
|
||
**이 결과가 의미하는 것** — 차이가 수초 이내면 두 기계의 로그를 그대로
|
||
나란히 놓아도 된다. 크면 **먼저 어느 쪽이 틀렸는지 가른 다음** 보정한다.
|
||
이 실험대는 test-server 가 NTP 미동기로 106초 빨랐고, 보정하지 않은 첫
|
||
계산은 훅이 인증서 발급보다 104초 먼저 실행된 것이 되어 물리적으로
|
||
불가능했다 — **음수 지연이 나오면 계산이 아니라 시계를 의심한다.**
|