기록 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.5 KiB
kind, slug, title, topic, topicName, project, status, source, sourceRevision, evidence
| kind | slug | title | topic | topicName | project | status | source | sourceRevision | evidence | |||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| REFERENCE | never-subtract-values-from-two-clocks | 두 시계에서 온 값을 빼지 않는다 | when-the-measurement-lies | 주입이 걸렸는지 무엇으로 아는가 | keycloak-session-store | 게시 전 |
|
cdac9b8178391311d8eca1ebc6cac15bb62d79af |
|
두 시계에서 온 값을 빼지 않는다
서로 다른 기계에서 온 타임스탬프는 빼기 전에 두 시계가 같은지 확인한다. D-4a 에서 test-server 는 dev 머신보다 106초 빨랐다. 보정하지 않고 계산한 D-4 의 공백은 106초 짧았고(2199 → 2305초), 1~2초를 재는 D-4a 에서는 그냥 뺀 값이 107초로 나와 참값보다 약 106초 어긋났다.
관계
- 실패 76 건이 서버 탓이 아니었다 — 대조군이 오보를 막았다 같은 D-4 의 값이다. 그 실험이 적어 둔 공백 2199초가 이 보정으로 2305초가 됐다.
- 산문으로 적힌 측정 장치를 실행 가능하게 고쳤더니 한 건이 깨졌다 측정값이 아니라 재는 쪽이 틀린 다른 경우다. 거기서는 적어 둔 명령이 실행되지 않았고, 여기서는 실행된 명령이 남긴 시각이 어긋나 있었다.
목적
보정하지 않고 뺀 값이 자릿수만 어긋난다고 읽는 것을 막는다.
D-4a 에서 갱신부터 서빙까지 1~2초를 재려다 걸렸다. test-server 는 NTP 동기가 꺼져 있고 dev 머신보다 106초 빠르다. 그 사실을 적지 않고 뺀 D-4 의 공백은 2199초였는데 보정하면 2305초, 38분 25초다. 틀린 값이 106초 짧게 나왔다.
같은 왜곡이 D-4a 에서는 결과를 통째로 바꾼다. 그냥 빼면 훅이 발급보다 107초 뒤로 보이는데 참값은 1~2초라 약 106초가 어긋나고, 보정을 반대쪽에 걸면 음수가 나와 훅이 갱신보다 먼저 돈 것이 된다. 자릿수만이 아니라 두 사건의 선후까지 뒤집힌다.
규칙
1. 빼기 전에 두 값이 같은 시계에서 왔는지 확인한다
D-4 의 2199초는 archive/cert2.pem 의 mtime 과 일련번호 관측 시각을 그대로 뺀 값이다. 앞은 test-server 시계이고 뒤는 dev 시계다. 양쪽 다 UTC 로 적혀 있어 뺄 때는 같은 자에서 읽은 것처럼 보인다. 갈리는 단위는 시간대가 아니라 기계다.
차를 셸이 대신 빼게 하지 않는다. 두 값을 각각 찍어 놓고 눈으로 빼면 어느 값이 어느 기계에서 왔는지가 출력에 남지만, 명령 안에서 빼 버리면 결과 한 줄만 남고 그 정보가 사라진다.
D-4 의 가이드에는 이 편에만 있는 시각 표기 규약이 하나 있다. 시각마다 어느 시계인지를 붙여 08:58:52 (dev) 와 17:22:13 KST (ts) 로 적고, 보정한 값은 08:20:27 (실제) 로 적는다. 두 기계의 시계를 섞어 빼는 바람에 숫자를 한 번 틀린 뒤에 붙은 규약이다.
2. 어느 시계가 밀렸는지는 제3의 기준으로 가른다
dev 머신은 Google 및 Let's Encrypt ACME 응답과 0초 차였고, test-server 는 Google 기준으로 -105초였다. 두 기계만 맞대면 차이만 나오고 어느 쪽이 맞는지는 안 나온다. test-server 의 timedatectl 은 NTP=no · NTPSynchronized=no 였다.
3. 왜곡값은 여러 번 재서 흔들리는지 본다
ssh 왕복으로 잰 왜곡은 3회 모두 +106.1초였다. 흔들리면 네트워크 지연이 섞인 것이고 안정적이면 왜곡이다. 이 실험대가 쓴 106초는 이 106.1초를 반올림한 값이다. Google 비교 쪽의 -105초와는 1초 넘게 갈렸다 — 둘 다 test-server 가 앞섰다는 것은 같지만, 보정에 넣은 것은 흔들림을 확인한 ssh 왕복 쪽이다.
4. 왜곡은 주입하기 전에 재 둔다
D-4 와 D-4a 의 재현 절차는 인증서를 강제 갱신하기 전에 시계부터 잰다. 주입이 끝난 뒤에는 그때 그 시계가 얼마나 어긋나 있었는지를 되짚을 방법이 없기 때문이다. D-4 는 시계를 재지 않고 2199초를 적었고, 나중에 D-4a 가 시계를 재면서 그 값이 2305초로 정정됐다.
강제 갱신은 진짜 인증서를 발급해 되돌릴 수 없으므로 이 실험 전체에서 한 번만 쓴다. 그 한 번을 헛되이 쓰지 않으려고 주입 전에 잴 것을 여덟 칸으로 적어 두었고, 시계는 그중 일곱 번째다.
관측도 한 기계에서만 한다. 이 실험은 전부 dev 머신에서 관측했고, 호스트에서만 알 수 있는 파일 mtime 과 훅 로그만 보정해서 썼다. 밖에서 본 것이 이 실험의 답이고, dev 가 이 실험대에서 유일하게 정확한 시계였기 때문이다.
5. 보정한 결과는 독립된 시계로 교차검증한다
새 인증서의 SCT 는 CT 로그가 자기 시계로 서명한 시각이라 test-server 것도 dev 머신 것도 아니다. 106초를 뺀 타임라인에서 훅의 nginx -t 는 발급 1초 뒤에 놓였다. 보정이 틀린 방향이었으면 훅이 발급보다 앞에 왔을 것이다.
다만 훅 시각은 초 단위로만 남은 로그 값이고 SCT 에는 밀리초가 붙어 있다. 이 실험대가 적은 「정확히 1초 앞」은 그 반올림을 거친 표현이라 차가 1.000초라는 뜻은 아니다.
인증서에 적힌 notBefore 는 발급 시각을 재는 기준으로 쓸 수 없다. Let's Encrypt 가 notBefore 를 정확히 한 시간 백데이트하므로 그대로 발급 시각으로 읽으면 한 시간을 잃고, 한 시간을 더한 값도 발급 시각이 아니다. 이 실험대의 두 인증서에서 SCT 는 그 값보다 약 89초 앞섰다.
6. 같은 왜곡이라도 다른 실험에서 잰 값을 섞어 쓰지 않는다
B-4 는 타임스탬프 둘의 차로 약 107초를 어림했고, D-4a 는 ACME 응답을 제3의 기준으로 두고 106초를 다시 쟀다. 둘 다 test-server 가 앞선 양이다. 보정에 쓸 값은 D-4a 의 106초이고, 107초는 B-4 의 12회를 읽기 위한 어림이다.
7. 음수 지연이 나오면 계산이 아니라 시계를 의심한다
D-4 와 D-4a 의 「막히면」 표는 공백이나 지연이 음수로 나오는 것을 시계 재는 절차로 돌아갈 신호로 적어 두었다. 선후가 뒤집힌 결과는 자릿수만 어긋난 값과 달리 바로 걸린다.
적용 조건
- 서로 다른 기계에서 온 타임스탬프의 차이를 잴 때
- 로그 시각과 외부 서비스가 찍은 시각을 견줄 때. D-4a 가 견준 것은 훅 로그와 CT 로그의 SCT 다
- NTP 동기를 확인하지 않은 호스트가 측정에 끼어 있을 때. test-server 가 그랬다
- 앞선 실험이 적어 둔 구간 값을 그대로 인용할 때. D-4 의 2199초가 D-4a 때문에 정정됐다
예외
- 같은 기계의 단조 시계로만 잰 구간에는 걸리지 않는다
- 재는 구간이 왜곡보다 훨씬 길면 값은 틀려도 결론은 남는다. D-4 에서는 오차가 106초여서 결론이 안 바뀌었고, 1~2초를 재는 D-4a 에서는 결과가 완전히 뒤집혔다
- 근거는 D-4·D-4a 한 쌍이다. B-4 에서도 같은 두 기계가 약 107초 어긋나 클레임 반영 시각이 변경 시각보다 앞서 보였는데, 그쪽은 어림으로 재고 넘어갔다. 이 실험대 밖의 측정으로 넓혀 확인한 적은 없다
예시
- test-server : NTP=no · NTPSynchronized=no · dev 머신보다 106초 빠름
- dev 머신 → Google : 0초 차. dev 머신 → Let's Encrypt ACME : 0초 차
- ssh 왕복 왜곡 3회 : +106.1 / +106.1 / +106.1초. 안정적
- test-server → Google : -105초. 같은 왜곡을 다른 방법으로 잰 값이다
- 보정식 : 실제 시각 = test-server 시계 − 106초. dev 시계는 보정하지 않는다
- D-4 의 공백 : 보정 전 2199초, 보정 후 2305초 = 38분 25초
- 보정하지 않은 D-4a : 그냥 빼면 107초, 참값 1~2초보다 약 106초 어긋난다. 보정을 반대쪽에 걸면 음수가 된다
- 보정한 D-4a 타임라인 : 발급 다음 초에 훅 nginx -t 와 새 워커 37252 기동, 그다음 초에 훅 nginx -s reload. 발급에서 서빙까지 1~2초
- SCT 는 밀리초까지 찍히고 훅 로그는 초 단위다. 「정확히 1초 앞」은 그 차이를 반올림한 표현이다
- D-4a 의 새 인증서 : notBefore 는
Sep 4 11:29:18, 한 시간을 더하면12:29:18, SCT 는12:27:49.05다. 약 88.9초 앞선다 - B-4 의 약 107초 : 타임스탬프 둘의 차로 어림한 별개 값이다