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>
4073 lines
271 KiB
JSON
4073 lines
271 KiB
JSON
{
|
||
"schemaVersion": 4,
|
||
"project": "keycloak-session-store",
|
||
"ssot": "final/document.md",
|
||
"sourceRepository": {
|
||
"path": "/home/donghyeon/workspace/keycloak-pattern",
|
||
"revision": "9465582b5d1630eb4ae7c4e078021486919bf6b6",
|
||
"verified": "반입할 때 적어 둔 source/.source-revision 이 이 커밋이고 저장소에 실재한다 — 「chore: 실행 환경 구성 문서 추가 및 수정」, 2026-09-10. **전에 이 칸은 cdac9b81 이었고 그것은 틀렸다** (2026-09-17 대조) — 그 커밋은 2026-09-04 이고 반입본 306개 가운데 docs/guides/** 28개가 거기에 아예 없다. 가이드는 그 엿새 뒤 6f6ab86 에서 들어왔다. **그런데 반입한 바이트는 이 커밋과도 같지 않다** — 9465582b 와 같은 것은 276개이고 29개가 다르다. 같은 306개를 저장소의 **작업 트리**와 견주면 297개가 같다. HEAD 에서 200 커밋을 거슬러 전수 대조했을 때 가장 가까운 6f6ab86 도 28개가 어긋났다. **맞는 커밋은 없다** — 반입은 커밋이 아니라 **그 시점의 작업 트리**(미커밋 수정이 있던 상태)에서 떠 온 것이다. 지금도 저장소는 그 파일들을 M 으로 낸다. 작업 트리와 남은 차이 8개 가운데 5개가 그 M 목록에 있고(반입 뒤 저장소가 더 고쳤다), deploy/lab/host/nginx-keycloak-lab.conf 는 저장소에서 deploy/lab/edge/ 로 옮겨져 반입본에만 남았다. **이 커밋은 「반입 시점의 HEAD」라는 뜻이지 「반입한 바이트가 이것이다」가 아니다**"
|
||
},
|
||
"ssotSha256": "fdb69c6e6e80bb74ee1e066523a799be7c8a0320f81f3d3799a12909762d6387",
|
||
"sourceRevision": "keycloak-session-lab@2026-09",
|
||
"generatedAt": "2026-09-17",
|
||
"candidateScope": {
|
||
"document": "final/document.md",
|
||
"sections": [
|
||
"코드보다 먼저 드러난 문제",
|
||
"문제를 어렵게 만든 제약",
|
||
"검토한 선택지와 막힌 지점",
|
||
"선택의 이유와 지킨 경계",
|
||
"선택이 코드와 흐름에 반영되는 방식",
|
||
"결정이 지켜지는지 확인하는 방법",
|
||
"얻은 것, 잃은 것, 적용하지 않을 때",
|
||
"결국 지키려던 것은 무엇이었나",
|
||
"2026-09-11 추가 측정 — 워크로드 종류가 클러스터에 미치는 영향",
|
||
"재현 가이드 26편과, 그것을 따라가다 드러난 결함",
|
||
"실험대가 쓴 개념 — 조사한 것",
|
||
"A층 재현 절차 — 열 편을 직접 치는 순서",
|
||
"B층 재현 절차 — 아홉 편을 직접 치는 순서",
|
||
"C층 재현 절차 — 두 편을 직접 치는 순서",
|
||
"D층 재현 절차 — 다섯 편을 직접 치는 순서"
|
||
],
|
||
"excluded": [
|
||
"자료",
|
||
"이 기록에 아직 없는 것"
|
||
],
|
||
"note": "처음부터 한 편으로 쓴 글이라 제2부가 없다. 맨 뒤 두 절은 증거 목록과 그림 제작 메모라 후보 자리가 아니다. 2026-09-11 에 SSOT 로 들어온 h2 둘(추가 측정 · 재현 가이드 감사)을 범위에 더했다 — 둘 다 실험을 돌리고 잰 검증 기록이라 후보 자리다. 「실험대가 쓴 개념 — 조사한 것」은 이번 분해의 범위가 아니라 sections 에도 excluded 에도 넣지 않았다. 2026-09-12 에 Studio 의 여섯 번째 종류 SETUP(화면 이름 「환경 구성」)을 화면에서 확인해 스킬과 스크립트에 넣었고, 그 다음 날 A층 열 편의 절차가 SSOT 로 들어와 h2 하나를 범위에 더했다 — `source/docs/guides/experiments/` 의 재현 가이드 26편은 발견이 아니라 「직접 쳐서 다시 만드는 절차」인데 담을 종류가 없어 SSOT 에도 계약에도 없었다. 이 h2 에서 나오는 후보는 종류가 전부 SETUP 이고, 같은 실험의 발견은 이미 A층 Case 들이 담고 있으므로 겹치는 것은 MERGE_INTO 가 아니라 relations 로 잇는다. B·C·D 층 16편은 아직 SSOT 에 없어 이번 범위가 아니다. 그 다음 B층 아홉 편의 절차가 SSOT 로 들어와 h2 하나를 더 범위에 더했다 — A층과 같은 뼈대(기준선 → 주입 → 주입 검증 → 관찰 → 복구)이지만 건드리는 것이 클러스터·네트워크·DB 가 아니라 애플리케이션 소스와 매니페스트이고, 되돌리기가 `git checkout` 뒤 재빌드와 두 노드 재import 까지 간다. 이 h2 에서 나오는 후보는 종류가 전부 SETUP 이고, 같은 실험의 발견은 이미 B층 Case 여섯이 담고 있으므로 겹치는 것은 MERGE_INTO 가 아니라 relations 로 잇는다. C·D 층 일곱 편은 아직 SSOT 에 없어 이번 범위가 아니다. 그 다음 C·D 층 일곱 편의 절차가 SSOT 로 들어와 h2 둘을 더 범위에 더했다 — 뼈대는 앞의 두 층과 같은 「기준선 → 주입 → 주입 검증 → 관찰 → 복구」 이지만 C층이 건드리는 것은 두 앱의 세션이고 D층이 건드리는 것은 운영 절차 자체(백업·판올림·비밀·인증서 갱신)다. 이 h2 둘에서 나오는 후보도 종류가 전부 SETUP 이고, 같은 실험의 발견은 이미 C·D층 Case 다섯이 담고 있으므로 겹치는 것은 MERGE_INTO 가 아니라 relations 로 잇는다. 층마다 주제 하나로 둔 것도 A·B 와 같다 — C 는 두 편뿐이지만 독자 질문이 D 와 다르다. 이로써 `source/docs/guides/experiments/` 의 재현 가이드 26편이 전부 SSOT 에 들어왔다. ── 「실험대가 쓴 개념 — 조사한 것」(1,474행·여덟 층)을 범위에 더했다. 그전까지 이 h2 는 sections 에도 excluded 에도 없어서 범위 검사가 양쪽 어디로도 걸지 못했고, 그래서 어느 항목도 처분을 받은 적이 없다. 범위로 넣은 까닭은 이것이 제2부(모듈 분석 전문)나 제3부(분석 재료)가 아니라 이 실험대에서 직접 읽은 값이기 때문이다 — conntrack 표 311/131072, 유닛 다섯의 Type 과 KillMode, nginx cgroup 의 PID 와 카운터, SCT 타임스탬프 둘, 게스트 두 대의 RSS 와 available 이 전부 이 실험대에서 뽑은 것이라고 「이 조사가 선 근거」가 적는다. 근거로 빼는 쪽도 성립한다 — 이 절은 계약이 선 뒤에 쓰였고 스스로 「각 주제가 서 있는 바닥」이라고 적는다. 그래도 범위로 둔 것은, 빼 두면 이 절에 나중에 붙는 것도 영영 판정되지 않아 지금 고치는 상태가 그대로 다시 생기기 때문이다. 대신 선별 결과를 층마다 대장에 적었다 — 후보 22건이고 PROMOTE 는 0건이다. 여덟 층은 이미 쓰인 기록들이 필요한 만큼씩 흡수해 간 바닥이라, 지금 독립 기록으로 올리면 같은 문장이 두 곳에 생긴다. 0층의 아래쪽(KVM ioctl·VMX·virtio 규격·vhost-user·VFIO)은 SSOT 가 스스로 「이 실험대에서 잰 것이 아니라 문서에서 옮긴 것」이라고 적고 virtualization 프로젝트가 그것을 정의로 담고 있어, 저쪽이 정의이고 이쪽이 측정이라는 앞 단계의 판정 그대로 KEEP_IN_SSOT 로 둔다. excludedAnchorPattern 도 이번에 처음 적었다 — 없으면 「범위 밖에서만 나온 글감」 검사가 통째로 꺼진 채였다. 지금 excluded 인 두 절만 가리키고, 이 패턴으로 걸리는 기존 글감은 없다.",
|
||
"excludedAnchorPattern": "#(자료|이-기록에-아직-없는-것)(-|$)"
|
||
},
|
||
"note": "이 프로젝트의 글감 전부다. 분해 계약이자 색인이고, 이 파일이 정본이다. 노드의 칸은 사람이 적고 file·publication·status 는 기록 파일에서 읽어 채운다 — python3 scripts/build-tech-log-tree.py keycloak-session-store",
|
||
"contract": {
|
||
"decomposition": [
|
||
"글감을 찾는 입력은 final/document.md 하나다. 거기에 없는 근거는 먼저 SSOT 에 넣는다.",
|
||
"후보 전부는 candidates 에 처분과 함께 남고 PROMOTE 만 topics 로 올라간다.",
|
||
"없애고 관련 Case 나 Concept 의 한 절로 넣어도 이해·결정·재사용성이 그대로라면 독립 기록으로 만들지 않는다.",
|
||
"Topic 은 독자 질문 하나다. 그 물음에 답하지 않는 글감은 다른 Topic 으로 옮긴다.",
|
||
"Concept 은 Case·Decision·Question 을 먼저 고른 뒤 그것을 이해하는 데 필요한 것만 거꾸로 더한다."
|
||
],
|
||
"readinessValues": [
|
||
"READY",
|
||
"OPEN",
|
||
"NEEDS_EVIDENCE",
|
||
"NEEDS_DECISION",
|
||
"BLOCKED"
|
||
],
|
||
"dispositionValues": {
|
||
"PROMOTE": "독립 Tech Log 로 쓴다",
|
||
"MERGE_INTO": "다른 기록의 한 절로 흡수한다",
|
||
"KEEP_IN_SSOT": "분석에는 남기고 독립 기록으로 만들지 않는다 — 정상적인 성공 결과다",
|
||
"NEEDS_EVIDENCE": "주장에 아직 검증이 없다",
|
||
"NEEDS_DECISION": "방향이 그럴듯하지만 프로젝트가 정하지 않았다",
|
||
"BLOCKED": "원본이 불완전하거나 서로 어긋난다"
|
||
}
|
||
},
|
||
"topics": {
|
||
"session-custody-across-nodes": {
|
||
"title": "Keycloak 두 노드가 같은 세션을 읽는 경로",
|
||
"readerQuestion": "두 노드가 같은 세션을 쓰는 것은 복제 때문인가?",
|
||
"kinds": {
|
||
"case": [
|
||
{
|
||
"title": "클러스터는 형성됐는데 세션을 나르는 것은 데이터베이스였다",
|
||
"slug": "session-sharing-is-the-database-not-replication",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#코드보다-먼저-드러난-문제-전제가-무너졌다",
|
||
"final/document.md#선택의-이유와-지킨-경계-a1"
|
||
],
|
||
"code": [
|
||
"JGROUPS_PING",
|
||
"OFFLINE_USER_SESSION",
|
||
"sessions 캐시"
|
||
],
|
||
"classification": "로그가 가리키는 원인과 실제 경로가 다르다는 것을 노드 B 의 SQL 문장 로깅으로 확정했고, 그 확정이 A-1 의 빗나간 예측까지 설명하며 닫힌다",
|
||
"missing-verification": "Infinispan 복제를 명시적으로 켠 구성에서는 재지 않았다. 이 결론은 persistent-user-sessions 가 켜진 26.7.0 기본값에 한정된다",
|
||
"relations": [
|
||
"concept:persistent-vs-volatile-user-sessions",
|
||
"case:cache-temperature-decides-the-outcome",
|
||
"reference:state-the-version-and-the-setting-with-the-result"
|
||
],
|
||
"kind": "case",
|
||
"ssot-assets": [
|
||
"session-sharing-path",
|
||
"a1-transport-vs-discovery"
|
||
],
|
||
"ssot-evidence": [
|
||
"raw/a1-jgroups-transport-block__09-cross-node-under-partition.txt",
|
||
"raw/a1-jgroups-transport-block__10-logout-not-propagated.txt"
|
||
],
|
||
"publication": "초안",
|
||
"file": "session-custody-across-nodes/case/case-session-sharing-is-the-database-not-replication.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [
|
||
"session-sharing-path",
|
||
"a1-transport-vs-discovery"
|
||
],
|
||
"assetFiles": [
|
||
"session-sharing-path",
|
||
"a1-transport-vs-discovery"
|
||
],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/a1-jgroups-transport-block__09-cross-node-under-partition.txt",
|
||
"../../../final/evidence/raw/a1-jgroups-transport-block__10-logout-not-propagated.txt"
|
||
]
|
||
},
|
||
{
|
||
"title": "같은 설정이 캐시 온도만으로 세 가지 답을 냈다",
|
||
"slug": "cache-temperature-decides-the-outcome",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#선택의-이유와-지킨-경계-a7-a7a"
|
||
],
|
||
"code": [
|
||
"CLIENT_SCOPE_CLIENT",
|
||
"REVOKED_TOKEN",
|
||
"DEFAULT_SCOPE"
|
||
],
|
||
"classification": "가설(REVOKED_TOKEN)을 문장 로깅으로 반증하고 실제 조회를 확정한 뒤, 캐시 온도 셋을 모두 재현해 표 하나로 닫히지 않는다는 결론까지 갔다",
|
||
"missing-verification": "캐시가 식는 시간을 재지 않았다. 냉·중간·온 세 상태를 만들어 확인했을 뿐 그 사이의 전이는 관측하지 않았다",
|
||
"relations": [
|
||
"concept:persistent-vs-volatile-user-sessions",
|
||
"reference:state-the-version-and-the-setting-with-the-result"
|
||
],
|
||
"kind": "case",
|
||
"ssot-assets": [
|
||
"cache-temperature-outcomes"
|
||
],
|
||
"ssot-evidence": [
|
||
"raw/a7a-volatile-cause__01-cause-determined.txt",
|
||
"raw/a7-volatile-comparison__05-a2-rerun-db-loss.txt"
|
||
],
|
||
"publication": "초안",
|
||
"file": "session-custody-across-nodes/case/case-cache-temperature-decides-the-outcome.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [
|
||
"cache-temperature-outcomes"
|
||
],
|
||
"assetFiles": [
|
||
"cache-temperature-outcomes"
|
||
],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/a7a-volatile-cause__01-cause-determined.txt",
|
||
"../../../final/evidence/raw/a7-volatile-comparison__05-a2-rerun-db-loss.txt"
|
||
]
|
||
},
|
||
{
|
||
"title": "롤링 재시작은 세션을 남기고 캐시만 지웠다",
|
||
"slug": "rolling-restart-keeps-sessions-drops-cache",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#선택의-이유와-지킨-경계-a8"
|
||
],
|
||
"code": [
|
||
"persistent-user-sessions"
|
||
],
|
||
"classification": "재시작 전후의 DB 세션 수와 캐시 수를 따로 세어 무엇이 남고 무엇이 사라지는지 갈랐고, 그 설정을 켜는 이유로 닫힌다",
|
||
"missing-verification": "표본이 적어 무중단 주장을 처음에 과장했다가 고쳤다. 재시작 중 진행 중이던 요청은 재지 않았다",
|
||
"relations": [
|
||
"concept:persistent-vs-volatile-user-sessions",
|
||
"case:session-sharing-is-the-database-not-replication"
|
||
],
|
||
"kind": "case",
|
||
"ssot-assets": [
|
||
"a8-cache-vs-session"
|
||
],
|
||
"ssot-evidence": [
|
||
"raw/a8-rolling-restart__01-restart-availability.txt",
|
||
"raw/a8-rolling-restart__02-session-survival.txt"
|
||
],
|
||
"publication": "초안",
|
||
"file": "session-custody-across-nodes/case/case-rolling-restart-keeps-sessions-drops-cache.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [
|
||
"a8-cache-vs-session"
|
||
],
|
||
"assetFiles": [
|
||
"a8-cache-vs-session"
|
||
],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/a8-rolling-restart__01-restart-availability.txt",
|
||
"../../../final/evidence/raw/a8-rolling-restart__02-session-survival.txt"
|
||
]
|
||
}
|
||
],
|
||
"concept": [
|
||
{
|
||
"title": "persistent-user-sessions 가 세션의 거처를 정한다",
|
||
"slug": "persistent-vs-volatile-user-sessions",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#코드보다-먼저-드러난-문제-버전-조건"
|
||
],
|
||
"basis-version": "Keycloak 26.7.0 · persistent-user-sessions 기본 활성 · 24 이전과 대조",
|
||
"classification": "세션이 DB 에 있는지 메모리에 있는지를 이 설정이 정하고, 그것을 모르면 A 층 결과 셋이 왜 뒤집히는지 읽을 수 없다",
|
||
"relations": [
|
||
"case:session-sharing-is-the-database-not-replication",
|
||
"case:rolling-restart-keeps-sessions-drops-cache",
|
||
"reference:state-the-version-and-the-setting-with-the-result"
|
||
],
|
||
"kind": "concept",
|
||
"ssot-assets": [
|
||
"version-conditional-results"
|
||
],
|
||
"publication": "초안",
|
||
"file": "session-custody-across-nodes/concept/concept-persistent-vs-volatile-user-sessions.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [
|
||
"version-conditional-results"
|
||
],
|
||
"assetFiles": [
|
||
"version-conditional-results"
|
||
],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/session-replication__01-cross-node-session.txt",
|
||
"../../../final/evidence/raw/a7-volatile-comparison__01-switch-to-volatile.txt",
|
||
"../../../final/evidence/raw/a1-jgroups-transport-block__09-cross-node-under-partition.txt",
|
||
"../../../final/evidence/raw/a7-volatile-comparison__04-a1-rerun-partition.txt",
|
||
"../../../final/evidence/raw/a8-rolling-restart__02-session-survival.txt",
|
||
"../../../final/evidence/raw/a7-volatile-comparison__03-a8-rerun-restart.txt",
|
||
"../../../final/evidence/raw/a2-database-loss__03-four-paths.txt",
|
||
"../../../final/evidence/raw/a7-volatile-comparison__05-a2-rerun-db-loss.txt"
|
||
]
|
||
}
|
||
],
|
||
"reference": [
|
||
{
|
||
"title": "버전과 설정을 결과와 함께 적는다",
|
||
"slug": "state-the-version-and-the-setting-with-the-result",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#코드보다-먼저-드러난-문제-버전-조건",
|
||
"final/document.md#얻은-것-잃은-것-적용하지-않을-때-적용되지-않는-조건"
|
||
],
|
||
"classification": "같은 제품의 같은 실험이 설정 하나로 뒤집히는 것을 세 건(A-1·A-2·A-8)에서 확인했다. 본 것은 Keycloak 한 제품의 `persistent-user-sessions` 하나뿐이고, 다른 제품의 기본값 변경은 이 프로젝트가 재지 않았다 — 규칙은 「버전과 설정을 결과와 함께 적어라」이지 「다른 제품도 이렇게 뒤집힌다」가 아니다",
|
||
"scope": "기본값이 메이저 버전 사이에 바뀐 제품을 측정해 결과를 적는 자리. 이 프로젝트의 근거는 Keycloak 26 사례 하나다",
|
||
"exceptions": "측정 대상이 그 설정에 걸리지 않는 경로라면 조건을 달지 않아도 된다. 무엇이 걸리는지를 먼저 확인한다",
|
||
"relations": [
|
||
"concept:persistent-vs-volatile-user-sessions",
|
||
"case:cache-temperature-decides-the-outcome"
|
||
],
|
||
"kind": "reference",
|
||
"publication": "초안",
|
||
"file": "session-custody-across-nodes/reference/reference-state-the-version-and-the-setting-with-the-result.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/a7-volatile-comparison__01-switch-to-volatile.txt",
|
||
"../../../final/evidence/raw/a1-jgroups-transport-block__09-cross-node-under-partition.txt",
|
||
"../../../final/evidence/raw/a7-volatile-comparison__04-a1-rerun-partition.txt",
|
||
"../../../final/evidence/raw/a8-rolling-restart__02-session-survival.txt",
|
||
"../../../final/evidence/raw/a7-volatile-comparison__03-a8-rerun-restart.txt",
|
||
"../../../final/evidence/raw/a2-database-loss__03-four-paths.txt",
|
||
"../../../final/evidence/raw/a7-volatile-comparison__05-a2-rerun-db-loss.txt"
|
||
]
|
||
}
|
||
],
|
||
"question": [],
|
||
"decision": [],
|
||
"setup": [
|
||
{
|
||
"title": "세션을 공유하는 것이 Infinispan 인지 PostgreSQL 인지 손으로 가른다",
|
||
"kind": "setup",
|
||
"slug": "reproduce-a0-session-sharing-path",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-0",
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서"
|
||
],
|
||
"pinned-versions": [
|
||
"Keycloak = 26.7.0",
|
||
"curlimages/curl = 8.11.1"
|
||
],
|
||
"classification": "**열 편의 첫 편이고 뒤의 아홉이 이 절차의 결과 위에 선다** — A-2 와 A-3 과 A-8 이 전제에 「A-0 을 먼저 한다」를 적는다. 세션 테이블을 비우고 StatefulSet 을 재시작해 출발값을 0 으로 만든 뒤 상주 탐침 파드에서 시험 넷을 차례로 친다 — 교차 노드 사용 · 캐시 계수기 델타 · 엔트리 소유 · PostgreSQL 문장 로깅으로 SQL 포획. **읽는 사람이 그대로 치는 명령이라 Setup 이다**. `kubectl`·`psql`·`curl` 이 40분어치 이어지고 Case 의 평문 한 칸에 담으면 복사가 안 된다. **순서가 결과를 바꾸는 곳이 셋**(observed) — DB 만 지우고 파드를 재시작하지 않으면 캐시 합계 19 와 DB 총계 15 가 어긋나고, 재시작 뒤 파드 IP 를 다시 잡지 않으면 아무 데도 안 닿는 것을 「복제 실패」로 읽으며, refresh token 은 회전하므로 **반대편 노드에 먼저** 써야 시험군이 남는다. **탐침 선택이 곧 측정 설계다** — 첫 판본은 `userinfo` 로 쟀고 `403` 을 복제 실패로 읽을 뻔했는데 발급 노드도 `403` 이었고 원인은 `openid` scope 가 없는 것이었다. **판정은** 반대편 캐시가 전부 `+0` 인 것, 엔트리가 대각선으로 `7 + 5 = 12` 로 DB 총계와 맞는 것, `keycloak-1` 이 날린 SQL 여덟 줄이다. **유효 범위** — 캐시 설정은 파일에서 읽을 수 없고(`cache-ispn.xml` 에는 `<transport/>` 뿐이다) 이 판정은 동작을 측정해서 얻은 것이다. Prometheus 출력을 자르는 `tr`·`grep` 줄과 JWT 에서 sid 를 뽑는 `sed` 줄은 가이드가 미검증으로 표시했다(unknown).",
|
||
"relations": [
|
||
"case:session-sharing-is-the-database-not-replication",
|
||
"concept:persistent-vs-volatile-user-sessions",
|
||
"reference:verify-the-injection-landed-separately-from-the-result",
|
||
"setup:reproduce-a1-jgroups-transport-block",
|
||
"setup:reproduce-a3-database-crash"
|
||
],
|
||
"publication": "게시됨",
|
||
"file": "session-custody-across-nodes/setup/setup-reproduce-a0-session-sharing-path.md",
|
||
"status": "게시 전",
|
||
"studioId": "4f56ed58-fc82-4ed1-ac87-c356b30c34f7",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": []
|
||
},
|
||
{
|
||
"title": "7800 을 막고 디스커버리와 트랜스포트를 갈라 끊는다",
|
||
"kind": "setup",
|
||
"slug": "reproduce-a1-jgroups-transport-block",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-1"
|
||
],
|
||
"pinned-versions": [
|
||
"Keycloak = 26.7.0",
|
||
"curlimages/curl = 8.11.1"
|
||
],
|
||
"classification": "NetworkPolicy 로 7800 만 빼고 8080·9000 을 허용해 **디스커버리는 살리고 트랜스포트만** 끊는 절차다. **주입을 넣은 것과 걸린 것이 여기서 처음 갈린다** — 정책을 걸어도 25분 내내 `vendor_cluster_size` 가 2 였고, `/proc/net/tcp6` 의 `01`(ESTABLISHED)과 `conntrack -L` 이 까닭을 댄다. **순서가 결과를 바꾸는 곳이 둘**(observed) — 9000 을 안 열면 readiness 가 실패해 kubelet 이 파드를 죽여 엉뚱한 이유로 클러스터가 깨지고, `conntrack -D` 는 `-L` 출력의 src·dst·sport·dport 를 **그대로** 옮겨야 지워진다(`--dport 7800` 만 주면 0건이다). **판정은** `coord = t` 가 두 줄인 것 하나로 끝난다 — 로그를 두 번 긁는 것보다 짧다. **이 절차가 확정하지 못한 것** — conntrack 삭제만으로 분단이 만들어지는지. 해설이 「3분 뒤 분단」이라고 썼다가 정정했고 실제 하락은 파드 재시작 4초 뒤였다. 그래서 분단을 확실히 만드는 단계는 정책이 걸린 채 파드를 지우는 것이다. `ssh kc-lab-2` 로 들어가 `conntrack` 을 따로 치는 두 단계 형태는 이 실험대에서 치지 않았다(unknown).",
|
||
"relations": [
|
||
"case:session-sharing-is-the-database-not-replication",
|
||
"case:nine-injections-that-silently-did-nothing",
|
||
"concept:readiness-hides-the-broken-node",
|
||
"case:commands-written-as-prose-do-not-run",
|
||
"setup:reproduce-a0-session-sharing-path",
|
||
"setup:reproduce-a5-asymmetric-partition"
|
||
],
|
||
"publication": "게시됨",
|
||
"file": "session-custody-across-nodes/setup/setup-reproduce-a1-jgroups-transport-block.md",
|
||
"status": "게시 전",
|
||
"studioId": "95e29500-b535-4246-86db-d569ed814904",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": []
|
||
},
|
||
{
|
||
"title": "persistent-user-sessions 를 끄고 A층 결론 넷을 다시 잰다",
|
||
"kind": "setup",
|
||
"slug": "reproduce-a7-volatile-comparison",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-7"
|
||
],
|
||
"pinned-versions": [
|
||
"Keycloak = 26.7.0",
|
||
"curlimages/curl = 8.11.1",
|
||
"persistent-user-sessions = v1"
|
||
],
|
||
"classification": "A-0·A-1·A-2·A-8 을 **옛 기본값 위에서 그대로 다시 치는** 절차다. 그 넷을 먼저 해 두지 않으면 「뒤집혔다」가 보이지 않는다. **끌 수 있는지를 먼저 확인한다** — `kc.sh build --help-all` 의 기능 목록에 `persistent-user-sessions[:v1]` 이 있어야 하고, 없으면 그 버전에서는 이 절차를 할 수 없으며 그 자체가 답이다. **전환 전에 세션 테이블을 비운다** — volatile 은 옛 행을 지우지 않으므로 빼먹으면 「전환이 안 됐다」로 잘못 읽는다. **빌드 옵션이라 재빌드가 일어나 롤아웃이 오래 걸린다** — `--timeout=500s` 를 준다. **args 문자열만으로는 부족하다** — 로그인 5회 뒤 `offline_user_session` 이 `(0 rows)` 인 것이 전환의 유일한 확실한 증거다. **캐시 엔트리 수로는 두 모드를 구별할 수 없다** — persistent 와 volatile 이 똑같이 `5 / 0` 을 낸다. **원복을 잊으면 이후 실험이 전부 오염된다** — args 와 매니페스트를 둘 다 되돌리고 로그인 뒤 행이 생기는지로 확인한다. **여기서 잰 `① 500 · ② 200` 을 그대로 표로 옮기면 안 된다** — 조건부이고 `setup:reproduce-a7a-volatile-cause` 가 그 셋을 가른다.",
|
||
"relations": [
|
||
"case:cache-temperature-decides-the-outcome",
|
||
"concept:persistent-vs-volatile-user-sessions",
|
||
"reference:state-the-version-and-the-setting-with-the-result",
|
||
"setup:reproduce-a7a-volatile-cause",
|
||
"setup:reproduce-a8-rolling-restart",
|
||
"setup:reproduce-a1-jgroups-transport-block"
|
||
],
|
||
"publication": "게시됨",
|
||
"file": "session-custody-across-nodes/setup/setup-reproduce-a7-volatile-comparison.md",
|
||
"status": "게시 전",
|
||
"studioId": "b8d7db33-afdc-4e49-9eab-ed4edbbe398e",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": []
|
||
},
|
||
{
|
||
"title": "문장 로깅으로 그 500 을 낸 SQL 을 확정하고 캐시 온도 셋을 재현한다",
|
||
"kind": "setup",
|
||
"slug": "reproduce-a7a-volatile-cause",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-7a"
|
||
],
|
||
"pinned-versions": [
|
||
"Keycloak = 26.7.0",
|
||
"curlimages/curl = 8.11.1",
|
||
"persistent-user-sessions = v1"
|
||
],
|
||
"classification": "**A-7 과 나눠 둔 까닭은 주입과 복구가 따로 서기 때문이다** — 여기는 주입이 셋(문장 로깅 · volatile 전환 · DB 정지)이고 복구도 셋이며, 계기가 `iptables` 가 아니라 **표식과 문장 로그**다. 표식 → 요청 → 표식 을 손으로 넣고 `awk '/A/,/B/'` 로 구간을 잘라 본다. **표식을 셸 함수로 감싸지 않는다** — 원 실행의 `m()` 은 출력을 `/dev/null` 로 버려 표식이 로그에 들어갔는지 확인하지 않고 넘어갔다(observed). **`grep -v JGROUPS_PING` 으로 거르기 전에 소음을 한 번 본다** — 5초마다 폴링한다. **캐시를 식히는 방법은 `rollout restart` 하나뿐이고**, 재현 셋 사이마다 그것을 건너뛰면 세 상태가 하나로 뭉개져 전부 `200/200` 이 된다. 재현 A 는 재시작과 DB 정지 **사이에 아무 요청도 보내지 않고**, B 는 **로그인만 한 번** 하고, C 는 refresh 를 3회 미리 돌린다. **문장 로깅을 끄지 않으면 다음 실험의 측정값이 바뀐다** — A-3 은 초당 14건으로 로그인을 도는데 로그가 폭주해 크래시 타이밍이 달라진다. **이 편의 시각은 UTC 다.** **재지 않은 것** — `CLIENT_SCOPE_CLIENT` 결과의 캐시가 얼마나 오래 더운지.",
|
||
"relations": [
|
||
"case:cache-temperature-decides-the-outcome",
|
||
"concept:persistent-vs-volatile-user-sessions",
|
||
"setup:reproduce-a7-volatile-comparison",
|
||
"reference:verify-the-injection-landed-separately-from-the-result"
|
||
],
|
||
"publication": "게시됨",
|
||
"file": "session-custody-across-nodes/setup/setup-reproduce-a7a-volatile-cause.md",
|
||
"status": "게시 전",
|
||
"studioId": "21dce25a-a165-47dc-bb40-2ed9f6f9efea",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": []
|
||
},
|
||
{
|
||
"title": "롤링 재시작을 걸고 재시작 전 토큰이 통하는지 본다",
|
||
"kind": "setup",
|
||
"slug": "reproduce-a8-rolling-restart",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-8"
|
||
],
|
||
"pinned-versions": [
|
||
"Keycloak = 26.7.0",
|
||
"curlimages/curl = 8.11.1"
|
||
],
|
||
"classification": "**파괴적이지 않아서 더 조심하는 절차다** — 재는 것이 「깨졌나」가 아니라 「안 깨졌나」라 측정을 잘못하면 안 깨진 것처럼 보이기 쉽고, 원래 실행이 실제로 그랬다. **함정은 토큰을 파드 안에 담는 단계에 있다** — 첫 재현 절차에 `/tmp/rt` 를 만드는 줄이 없어 빈 문자열이 `refresh_token=` 으로 전송됐는데 `400` 이 아니라 통과한 것처럼 보였고 아무 에러도 안 났다. `wc -c < /tmp/rt` 한 번이 이 시험 전체를 지킨다. **탐침은 StatefulSet 밖에 두고 재시작 내내 유지한다** — 다시 띄우면 `/tmp/rt` 와 `/tmp/sid` 가 같이 사라지므로 새 파드 IP 는 명령줄로 넘긴다. **대조군 refresh 뒤에 `/tmp/rt` 를 다시 채운다** — 토큰이 회전했으므로 안 채우면 뒤의 `400` 이 재시작 탓인지 재사용 탓인지 갈리지 않는다. **재시작 여부는 `RESTARTS` 가 아니라 `AGE` 로 본다** — `rollout restart` 는 파드를 교체하므로 카운터는 0 에서 시작하고, 두 파드의 나이가 `44s`/`66s` 로 다른 것이 한 번에 하나씩 내렸다는 증거다. **가용성 루프를 재시작보다 먼저 띄운다.** **말할 수 있는 문장** — 「무중단이었다」가 아니라 「5초 해상도에서 끊김이 관측되지 않았다」이고 표본은 9개다.",
|
||
"relations": [
|
||
"case:rolling-restart-keeps-sessions-drops-cache",
|
||
"concept:persistent-vs-volatile-user-sessions",
|
||
"case:commands-written-as-prose-do-not-run",
|
||
"setup:reproduce-a7-volatile-comparison",
|
||
"setup:reproduce-a0-session-sharing-path"
|
||
],
|
||
"publication": "게시됨",
|
||
"file": "session-custody-across-nodes/setup/setup-reproduce-a8-rolling-restart.md",
|
||
"status": "게시 전",
|
||
"studioId": "0b64d23a-f82a-44b4-ad54-e079578977c4",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": []
|
||
}
|
||
]
|
||
},
|
||
"topic": "session-custody-across-nodes"
|
||
},
|
||
"losing-a-node-or-the-store": {
|
||
"title": "PostgreSQL 을 내리고 노드 전원을 뽑았을 때",
|
||
"readerQuestion": "저장소나 노드가 죽으면 실제로 무엇을 잃는가?",
|
||
"kinds": {
|
||
"case": [
|
||
{
|
||
"title": "200 과 토큰을 받은 로그인 네 건이 데이터베이스에 없었다",
|
||
"slug": "four-logins-that-returned-200-and-vanished",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#선택의-이유와-지킨-경계-a2-a3"
|
||
],
|
||
"code": [
|
||
"SET LOCAL synchronous_commit TO OFF",
|
||
"wal_writer_delay"
|
||
],
|
||
"classification": "153 건 중 149 건만 남은 것을 세어 확인하고 원인을 커밋 설정에서 확정했으며, 의도된 설계의 대가를 숫자로 닫았다",
|
||
"missing-verification": "synchronous_commit 을 켠 대조군을 돌리지 않았다. 유실이 0 이 되는지와 그때의 지연 비용은 재지 않았다",
|
||
"relations": [
|
||
"case:two-ways-to-lose-a-node",
|
||
"concept:readiness-hides-the-broken-node",
|
||
"reference:most-of-an-outage-is-noticing"
|
||
],
|
||
"kind": "case",
|
||
"ssot-assets": [
|
||
"a3-commit-to-disk-gap"
|
||
],
|
||
"ssot-evidence": [
|
||
"raw/a3-database-crash__02-design-check.txt",
|
||
"raw/a3-database-crash__03-loss-measurement.txt",
|
||
"raw/a3-database-crash__07-loss-result.txt",
|
||
"raw/a3-database-crash__08-wal-settings.txt"
|
||
],
|
||
"publication": "초안",
|
||
"file": "losing-a-node-or-the-store/case/case-four-logins-that-returned-200-and-vanished.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [
|
||
"a3-commit-to-disk-gap"
|
||
],
|
||
"assetFiles": [
|
||
"a3-commit-to-disk-gap"
|
||
],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/a3-database-crash__02-design-check.txt",
|
||
"../../../final/evidence/raw/a3-database-crash__03-loss-measurement.txt",
|
||
"../../../final/evidence/raw/a3-database-crash__07-loss-result.txt",
|
||
"../../../final/evidence/raw/a3-database-crash__08-wal-settings.txt"
|
||
]
|
||
},
|
||
{
|
||
"title": "노드를 잃는 두 가지 — 저장소가 같이 죽는 것과 들어갈 길이 없는 것",
|
||
"slug": "two-ways-to-lose-a-node",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#선택의-이유와-지킨-경계-a4"
|
||
],
|
||
"code": [
|
||
"node-monitor-grace-period",
|
||
"tolerationSeconds",
|
||
"StatefulSet"
|
||
],
|
||
"classification": "같은 복구 시간을 내는 두 장애의 원인이 다르다는 것을 갈랐다. 축출까지의 시간은 **두 값을 더한 340초가 계산이지 측정이 아니고**, 실측한 축출은 240~270초다. 두 폴링이 같은 `+0` 을 쓰는지 이 실험이 적어 두지 않아 **모순되는지 아닌지를 이 실험은 말할 수 없다** — 그 판정 불가도 이 기록이 적는 것이다",
|
||
"missing-verification": "저장소를 두 노드에 나눠 배치한 구성에서는 재지 않았다. 4a 의 결과는 이 실험대의 배치에 걸려 있다",
|
||
"relations": [
|
||
"case:four-logins-that-returned-200-and-vanished",
|
||
"concept:readiness-hides-the-broken-node",
|
||
"reference:most-of-an-outage-is-noticing"
|
||
],
|
||
"kind": "case",
|
||
"ssot-assets": [
|
||
"a4-two-node-losses",
|
||
"lab-topology"
|
||
],
|
||
"ssot-evidence": [
|
||
"raw/a4-node-loss__02-worker-node-killed.txt",
|
||
"raw/a4-node-loss__03-state-during-loss.txt",
|
||
"raw/a4-node-loss__04-eviction-timing.txt"
|
||
],
|
||
"publication": "초안",
|
||
"file": "losing-a-node-or-the-store/case/case-two-ways-to-lose-a-node.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [
|
||
"lab-topology",
|
||
"a4-two-node-losses"
|
||
],
|
||
"assetFiles": [
|
||
"lab-topology",
|
||
"a4-two-node-losses"
|
||
],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/a4-node-loss__02-worker-node-killed.txt",
|
||
"../../../final/evidence/raw/a4-node-loss__03-state-during-loss.txt",
|
||
"../../../final/evidence/raw/a4-node-loss__04-eviction-timing.txt"
|
||
]
|
||
},
|
||
{
|
||
"title": "200 밀리초를 넣었더니 응답이 22.2 초가 됐다",
|
||
"slug": "200ms-of-delay-became-22-seconds",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#선택의-이유와-지킨-경계-a6"
|
||
],
|
||
"code": [
|
||
"agroal 커넥션 풀",
|
||
"readiness probe"
|
||
],
|
||
"classification": "28 배가 어디서 왔는지를 왕복 누적과 풀 경합 두 단계로 분해했고, 느림이 장애로 승격되는 지점까지 관측해 닫았다",
|
||
"missing-verification": "지연 값을 200ms 한 점에서만 쟀다. 어느 지연부터 readiness 가 실패하는지는 재지 않았다",
|
||
"relations": [
|
||
"concept:readiness-hides-the-broken-node",
|
||
"case:two-ways-to-lose-a-node"
|
||
],
|
||
"kind": "case",
|
||
"ssot-assets": [
|
||
"a6-latency-multiplication"
|
||
],
|
||
"ssot-evidence": [
|
||
"raw/a6-latency-injection__02-delay-injected.txt",
|
||
"raw/a6-latency-injection__04-pool-under-load.txt"
|
||
],
|
||
"publication": "초안",
|
||
"file": "losing-a-node-or-the-store/case/case-200ms-of-delay-became-22-seconds.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [
|
||
"a6-latency-multiplication"
|
||
],
|
||
"assetFiles": [
|
||
"a6-latency-multiplication"
|
||
],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/a6-latency-injection__02-delay-injected.txt",
|
||
"../../../final/evidence/raw/a6-latency-injection__04-pool-under-load.txt"
|
||
]
|
||
},
|
||
{
|
||
"title": "StatefulSet 과 Deployment 가 같은 답을 냈고, 유령 행은 남은 코디네이터가 지웠다",
|
||
"slug": "ghost-rows-are-cleaned-by-the-surviving-coordinator",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#2026-09-11-추가-측정-워크로드-종류가-클러스터에-미치는-영향-무엇을-쟀나",
|
||
"final/document.md#2026-09-11-추가-측정-워크로드-종류가-클러스터에-미치는-영향-관측",
|
||
"final/document.md#2026-09-11-추가-측정-워크로드-종류가-클러스터에-미치는-영향-결론"
|
||
],
|
||
"code": [
|
||
"JGROUPS_PING",
|
||
"kubectl delete pod",
|
||
"--grace-period=0",
|
||
"maxSurge: 0",
|
||
"maxUnavailable: 1",
|
||
"ISPN100001",
|
||
"keycloak-0-60375",
|
||
"keycloak-85469cb4d-cfzkt-24175"
|
||
],
|
||
"classification": "워크로드 종류 둘과 종료 방식 둘, 네 조합에서 JGROUPS_PING 을 매번 조회해 유령 행이 남지 않는 것을 확정했고, 코디네이터 로그의 뷰 변경 시각과 행 소멸 시각이 같은 초에 찍히는 것으로 정리 주체까지 닫았다. experiment-plan.md 에 미해결로 남아 있던 항목이 「자동」으로 닫힌다. 덤으로 StatefulSet 의 근거가 클러스터 동작이 아니라 로그와 표를 접두사로 대조하기 위한 성질 둘로 좁혀졌다",
|
||
"missing-verification": "정상 종료와 SIGKILL 만 쟀다. 노드 상실(A-4 형태)에서 코디네이터 자신이 죽는 경우는 재지 않았고, 그때는 정리 주체가 사라지므로 결과가 다를 수 있다. Deployment 도 성립한다는 것은 이 측정에서 나온 추론이고 프로젝트가 워크로드 종류를 바꾸기로 정한 기록은 없다",
|
||
"relations": [
|
||
"case:two-ways-to-lose-a-node",
|
||
"case:rolling-restart-keeps-sessions-drops-cache",
|
||
"case:session-sharing-is-the-database-not-replication"
|
||
],
|
||
"kind": "case",
|
||
"publication": "초안",
|
||
"file": "losing-a-node-or-the-store/case/case-ghost-rows-are-cleaned-by-the-surviving-coordinator.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [
|
||
"ghost-row-cleanup-order"
|
||
],
|
||
"assetFiles": [
|
||
"ghost-row-cleanup-order"
|
||
],
|
||
"evidenceFiles": []
|
||
}
|
||
],
|
||
"concept": [
|
||
{
|
||
"title": "readiness 가 깨진 노드를 시야에서 먼저 치운다",
|
||
"slug": "readiness-hides-the-broken-node",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#검토한-선택지와-막힌-지점-관측을-어디에"
|
||
],
|
||
"basis-version": "k3s · Kubernetes readiness probe · Keycloak 26.7.0 /health",
|
||
"classification": "밖에서 재면 장애가 보이지 않는 이유가 이 동작이고, 이것을 모르면 A 층 결과 대부분을 아무 일도 없었던 것으로 읽는다",
|
||
"relations": [
|
||
"case:200ms-of-delay-became-22-seconds",
|
||
"case:two-ways-to-lose-a-node",
|
||
"concept:the-up-metric-cannot-see-alive-but-useless"
|
||
],
|
||
"kind": "concept",
|
||
"ssot-assets": [
|
||
"observation-points"
|
||
],
|
||
"publication": "초안",
|
||
"file": "losing-a-node-or-the-store/concept/concept-readiness-hides-the-broken-node.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [
|
||
"observation-points"
|
||
],
|
||
"assetFiles": [
|
||
"observation-points"
|
||
],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/a1-jgroups-transport-block__11-service-impact.txt",
|
||
"../../../final/evidence/raw/a2-database-loss__05-recovery.txt",
|
||
"../../../final/evidence/raw/a4-node-loss__03-state-during-loss.txt",
|
||
"../../../final/evidence/raw/a6-latency-injection__04-pool-under-load.txt",
|
||
"../../../final/evidence/raw/a6-latency-injection__05-recovery.txt"
|
||
]
|
||
}
|
||
],
|
||
"reference": [
|
||
{
|
||
"title": "장애 시간의 대부분은 알아채는 데 걸린다",
|
||
"slug": "most-of-an-outage-is-noticing",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#선택의-이유와-지킨-경계-a4"
|
||
],
|
||
"classification": "복구 절차가 아니라 감지 지연이 장애 시간을 정한다는 것을 A-4 에서 봤다. **타이머 두 개의 합으로 확인한 것이 아니다** — `tolerationSeconds` 300 은 읽었지만 `node-monitor-grace-period` 40 은 조회하지 않았고(evidence 0건, 쿠버네티스 기본값을 인용했다), **두 값을 더한 340초는 계산이지 측정이 아니다.** 실측한 축출은 240~270초이고, 두 폴링이 같은 `+0` 을 쓰는지 적어 두지 않아 **모순되는지 아닌지를 이 실험은 말할 수 없다.** 기다리는 시간을 잡는 데는 그 계산으로 충분하지만 결과로 적을 때는 잰 쪽을 적는다. 다른 오케스트레이터는 이 프로젝트가 보지 않았다",
|
||
"scope": "노드 상실을 오케스트레이터가 감지해 대체를 만드는 구성. 이 프로젝트가 본 것은 k3s 한 벌이다",
|
||
"exceptions": "진입 경로 자체를 잃은 장애는 감지가 빨라도 복구되지 않는다. 그때는 이중화 지점이 문제다",
|
||
"relations": [
|
||
"case:two-ways-to-lose-a-node",
|
||
"case:four-logins-that-returned-200-and-vanished"
|
||
],
|
||
"kind": "reference",
|
||
"publication": "초안",
|
||
"file": "losing-a-node-or-the-store/reference/reference-most-of-an-outage-is-noticing.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/a4-node-loss__02-worker-node-killed.txt",
|
||
"../../../final/evidence/raw/a4-node-loss__03-state-during-loss.txt",
|
||
"../../../final/evidence/raw/a4-node-loss__04-eviction-timing.txt",
|
||
"../../../final/evidence/raw/a4-node-loss__05-recovery.txt",
|
||
"../../../final/evidence/raw/a4-node-loss__08-control-plane-recovery.txt"
|
||
]
|
||
}
|
||
],
|
||
"question": [],
|
||
"decision": [],
|
||
"setup": [
|
||
{
|
||
"title": "PostgreSQL 을 정상 종료시키고 네 경로를 잰다",
|
||
"kind": "setup",
|
||
"slug": "reproduce-a2-database-loss",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-2"
|
||
],
|
||
"pinned-versions": [
|
||
"Keycloak = 26.7.0",
|
||
"curlimages/curl = 8.11.1"
|
||
],
|
||
"classification": "`scale deployment/postgres --replicas=0` 으로 DB 를 **정상 종료**시키고 네 경로를 같은 명령으로 주입 전후에 잰다 — 캐시를 가진 노드의 refresh · 없는 노드의 refresh · 새 로그인 · 이미 발급된 토큰의 관리 API. **정문이 실제로 `503` 이 되므로 정지 구간을 1분 남짓으로 짧게 잡는다.** **계측 도구가 A-1 에서 바뀐다** — `--rm` 임시 파드는 토큰을 단계 사이로 못 넘기므로 `sleep 7200` 짜리 상주 탐침을 띄우고 `exec` 로 이어간다. **순서가 결과를 바꾸는 곳이 둘**(observed) — access token 수명이 60초라 「토큰 발급 → 정지 → 시험」을 그 안에 끝내야 `401` 이 만료인지 DB 탓인지 갈리고, `-o /dev/null` 을 빼면 본문과 상태코드가 섞여 `HTTP 000000{...}401` 이 나온다(원래 실행이 그렇게 했고 그 측정은 버렸다). **판정은** Ready 파드 0개 · `ready : []` · 정문 `503` 이고, 헬스 네 항목 중 `database connections` 만 DOWN 이다. **복구 중에 Keycloak 을 재시작하지 않는다** — 그러면 「사람 개입이 필요한가」가 사라진다. 실제로 `restarts=0` 인 채 DB Ready 이후 약 15초에 돌아왔다.",
|
||
"relations": [
|
||
"case:four-logins-that-returned-200-and-vanished",
|
||
"concept:the-up-metric-cannot-see-alive-but-useless",
|
||
"concept:readiness-hides-the-broken-node",
|
||
"setup:reproduce-a0-session-sharing-path",
|
||
"setup:reproduce-a3-database-crash"
|
||
],
|
||
"publication": "게시됨",
|
||
"file": "losing-a-node-or-the-store/setup/setup-reproduce-a2-database-loss.md",
|
||
"status": "게시 전",
|
||
"studioId": "bf169fef-f900-4247-88f8-427742ae3fe9",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": []
|
||
},
|
||
{
|
||
"title": "PostgreSQL 을 진짜로 크래시시키고 잃은 로그인을 센다",
|
||
"kind": "setup",
|
||
"slug": "reproduce-a3-database-crash",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-3"
|
||
],
|
||
"pinned-versions": [
|
||
"Keycloak = 26.7.0",
|
||
"curlimages/curl = 8.11.1"
|
||
],
|
||
"classification": "**절차의 절반이 「죽이는 데 실패하는 두 가지 방법」이다.** `--grace-period=0 --force` 와 컨테이너 안 `kill -9 1` 을 순서대로 밟아 보고 세 번째로 백엔드 프로세스를 죽인다. 건너뛰고 세 번째만 하면 왜 그것이 유일한 방법인지 모른다. **주입 성공 신호를 미리 정한다** — `database system was not properly shut down` 과 `redo starts`/`redo done` 이 없으면 결과를 해석하지 않는다. **줄의 존재가 아니라 시각을 본다** — `ready to accept connections` 는 아까 뜰 때 찍힌 줄일 수 있다. **손으로 칠 물건이 아닌 것이 하나 있다** — 400회 로그인 루프는 편집기로 `/tmp/a3-login-loop.sh` 를 써서 `cat >` 로 파드에 밀어 넣고 터미널 하나를 통째로 쓴다. 파드 안에서 `( ... ) &` 로 띄우면 `exec` 세션이 끝날 때 같이 죽어 0건을 모은다(observed). **차집합을 낼 때 `LC_ALL=C sort` 를 빼면 안 된다** — sid 가 base64url 이라 로케일이 다르면 멀쩡한 sid 가 없는 것으로 잡힌다. **가정한 값은 재기 전에 잰다** — `wal_writer_delay` 200ms 는 주입 전에 `pg_settings` 로 읽어 둔다. **말할 수 있는 범위** — 153/149/4 의 「4」가 아니라 「0 이 아니고 WAL 플러시 주기와 같은 자릿수」까지다.",
|
||
"relations": [
|
||
"case:four-logins-that-returned-200-and-vanished",
|
||
"case:nine-injections-that-silently-did-nothing",
|
||
"case:commands-written-as-prose-do-not-run",
|
||
"reference:verify-the-injection-landed-separately-from-the-result",
|
||
"setup:reproduce-a2-database-loss"
|
||
],
|
||
"publication": "게시됨",
|
||
"file": "losing-a-node-or-the-store/setup/setup-reproduce-a3-database-crash.md",
|
||
"status": "게시 전",
|
||
"studioId": "91ce17ad-758d-4a63-be5b-489786609557",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": []
|
||
},
|
||
{
|
||
"title": "기계 전원을 뽑고 쿠버네티스가 알아채는 시각을 잰다",
|
||
"kind": "setup",
|
||
"slug": "reproduce-a4-node-loss",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-4"
|
||
],
|
||
"pinned-versions": [
|
||
"Keycloak = 26.7.0"
|
||
],
|
||
"classification": "**명령을 치는 곳이 세 군데인 것이 이 절차의 내용이다** — 터미널 A 는 VM 호스트에서 `virsh`, B 는 `kc-lab-1` 에서 `kubectl`(4b 에서는 이 터미널이 죽는다), C 는 밖에서 `curl`. 워커를 죽이는 4a 와 k3s 서버를 죽이는 4b 로 나뉘고 **어느 노드를 죽이느냐가 전부**다. **`virsh shutdown` 을 쓰면 안 된다** — ACPI 종료라 쿠버네티스가 정상 이탈로 처리해 이 절차의 발견 둘이 통째로 안 나온다. `virsh destroy` 가 전원 차단이다. **믿을 수 있는 것은 하이퍼바이저뿐이다** — 쿠버네티스가 40초 동안 `Ready` 라고 말하는 것은 결과이지 검증이 아니다. **`--max-time 8` 을 모든 외부 확인에 준다** — 4b 에서 그것이 없으면 curl 이 몇 분씩 매달리고, 타임아웃이 곧 결과다(처음 40초의 `000`). **기다리는 시간이 절차의 일부다** — 40초 + `tolerationSeconds=300` 이라 축출까지 약 5분 40초이고 그것만 7분을 본다. **`delete pod --grace-period=0 --force` 는 치지 않는다** — 노드가 살아 있으면 같은 이름의 파드 둘이 생긴다. `virsh start` 가 빠르고 안전하다. **4a 확인표를 통과하기 전에 4b 로 넘어가지 않는다.** **이 절차가 답을 못 남긴 곳** — 처음 40초가 `000` 인 까닭을 nginx 로그로 확인하려던 절이 증거 파일에 제목만 있고 비어 있다.",
|
||
"relations": [
|
||
"case:two-ways-to-lose-a-node",
|
||
"reference:most-of-an-outage-is-noticing",
|
||
"concept:the-up-metric-cannot-see-alive-but-useless",
|
||
"concept:readiness-hides-the-broken-node"
|
||
],
|
||
"publication": "게시됨",
|
||
"file": "losing-a-node-or-the-store/setup/setup-reproduce-a4-node-loss.md",
|
||
"status": "게시 전",
|
||
"studioId": "d845adc8-be2c-4471-aa1d-7e4db864c471",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": []
|
||
},
|
||
{
|
||
"title": "한 방향만 끊어 보고 raw PREROUTING 까지 내려간다",
|
||
"kind": "setup",
|
||
"slug": "reproduce-a5-asymmetric-partition",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-5"
|
||
],
|
||
"pinned-versions": [
|
||
"Keycloak = 26.7.0",
|
||
"curlimages/curl = 8.11.1"
|
||
],
|
||
"classification": "주입이 네 번이고 앞의 둘은 **일부러 실패한다** — `filter FORWARD` 최상단은 kube-router 가 자기 체인을 재삽입하며 밀어내고, `raw PREROUTING` 으로 옮겨도 연결 방향을 잘못 짚으면 0 패킷이다. 셋 다 화면에는 「아무 일도 없었다」로 보이므로 겪어 보지 않으면 다음에도 속는다. **카운터가 유일한 판정 기준이다** — 규칙이 목록에 보이는 것은 검증이 아니고 `pkts` 가 0 이면 아무것도 측정하지 않은 것이다. **규칙을 넣기 전에 `conntrack -L | grep 7800` 으로 방향을 본다** — `dport=7800` 인 쪽이 서버이고, A-1 때와 방향이 반대였다. **57800 도 같이 막는다** — FD_SOCK2 는 `bind_port + 50000` 이라 7800 만 막으면 장애 감지가 살아 분단이 어중간해진다. **단방향으로는 안 갈라진다** — JGroups 가 열린 방향으로 다시 붙고 `suspected = 0` 이 그 증거다. 양방향으로 막아야 `coord = t` 가 둘이 된다. **로그 시각은 UTC 다** — KST 에서 9시간을 빼서 맞춰 보지 않으면 주입 전후를 정반대로 가른다. **일회용 `--rm -it` 파드는 붙는 경주가 되므로** 상주 탐침을 쓴다(observed — `container is in CONTAINER_EXITED state`).",
|
||
"relations": [
|
||
"case:two-ways-to-lose-a-node",
|
||
"case:nine-injections-that-silently-did-nothing",
|
||
"reference:verify-the-injection-landed-separately-from-the-result",
|
||
"concept:readiness-hides-the-broken-node",
|
||
"setup:reproduce-a1-jgroups-transport-block"
|
||
],
|
||
"publication": "게시됨",
|
||
"file": "losing-a-node-or-the-store/setup/setup-reproduce-a5-asymmetric-partition.md",
|
||
"status": "게시 전",
|
||
"studioId": "b90d719f-39fb-4bab-a263-0e32eedb2b36",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": []
|
||
},
|
||
{
|
||
"title": "flannel.1 에 200ms 를 넣고 커넥션 풀이 고갈되는 것을 본다",
|
||
"kind": "setup",
|
||
"slug": "reproduce-a6-latency-injection",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-6"
|
||
],
|
||
"pinned-versions": [
|
||
"Keycloak = 26.7.0",
|
||
"curlimages/curl = 8.11.1"
|
||
],
|
||
"classification": "**대조군이 같은 클러스터 안에 있는 설계라 배치를 먼저 확인해야 절차가 성립한다** — postgres 와 `keycloak-0` 이 같은 노드이고 `keycloak-1` 만 VXLAN 을 건너야 한다. 둘 다 같은 노드면 시험군이 없고 둘 다 다른 노드면 대조군이 없다. 주입은 세 번이고 앞의 둘은 **일부러 실패한다** — `eth0` 은 이 게스트에 없고(`enp1s0` 이다), `enp1s0` 에서는 VXLAN 캡슐화 때문에 파드 IP 가 헤더에 없다. **성공한 주입은 `flannel.1` 에 `prio` + `netem` + `u32 filter` 세 줄을 한 줄씩 치는 것이다** — `netem` 을 root 에 바로 붙이면 모든 트래픽이 느려진다. **`Sent 0 pkt` 의 뜻이 A-5 와 다르다** — 부하 전이면 정상이고 부하 후면 필터가 틀린 것이다. **부하는 상주 파드 안 파일로 모은다** — `kubectl run --rm -i` 로 동시 20건을 띄우면 stdout 이 새어 20줄 중 일부만 도착한다(observed). **시간 값은 `sort -g` 로 정렬한다** — 사전순이면 `10.5` 가 `3.4` 앞에 와 최대값을 잘못 읽는다. **커넥션 풀 지표는 부하 직후에 읽는다** — `awaiting_count`·`active_count` 는 순간값이라 끝나면 0 이다. **대조군도 변한다** — `70 ms` 에서 `41 ms` 로 41% 빨라졌고 자릿수로 판정한다.",
|
||
"relations": [
|
||
"case:200ms-of-delay-became-22-seconds",
|
||
"case:nine-injections-that-silently-did-nothing",
|
||
"case:commands-written-as-prose-do-not-run",
|
||
"reference:verify-the-injection-landed-separately-from-the-result"
|
||
],
|
||
"publication": "게시됨",
|
||
"file": "losing-a-node-or-the-store/setup/setup-reproduce-a6-latency-injection.md",
|
||
"status": "게시 전",
|
||
"studioId": "cf2e783c-424b-4167-aa27-3bd6b5f46ee2",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": []
|
||
}
|
||
]
|
||
},
|
||
"topic": "losing-a-node-or-the-store"
|
||
},
|
||
"where-application-state-lives": {
|
||
"title": "세션과 토큰을 Redis 와 PostgreSQL 에 나눠 두기",
|
||
"readerQuestion": "저장소를 옮겨도 안 고쳐지는 것은 무엇인가?",
|
||
"kinds": {
|
||
"case": [
|
||
{
|
||
"title": "세션만 Redis 로 옮기자 토큰이 따라오지 않았다",
|
||
"slug": "moving-the-session-left-the-tokens-behind",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-b0",
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-b1"
|
||
],
|
||
"code": [
|
||
"InMemoryOAuth2AuthorizedClientService",
|
||
"AuthenticatedPrincipalOAuth2AuthorizedClientRepository",
|
||
"SPRING_SESSION_STORE_TYPE"
|
||
],
|
||
"classification": "저장소를 붙이기 전에 자동구성이 무엇을 골랐는지 먼저 읽고, 그 조회 키 차이가 다음 실험의 결과를 만든다는 것까지 이어 닫았다",
|
||
"missing-verification": "Redis 를 붙인 상태에서 토큰 저장소만 따로 두는 조합은 B-2 에서 다시 쟀다. 여기서는 세션 쪽만 확인했다",
|
||
"relations": [
|
||
"concept:two-stores-two-lookup-keys",
|
||
"case:a-primary-key-without-the-session-id",
|
||
"decision:split-the-two-stores-and-design-each"
|
||
],
|
||
"kind": "case",
|
||
"ssot-evidence": [
|
||
"raw/b0-bff-redis-deploy__03-beans-analysis.txt",
|
||
"raw/b1-redis-session-store__03-redis-contents.txt"
|
||
],
|
||
"publication": "초안",
|
||
"file": "where-application-state-lives/case/case-moving-the-session-left-the-tokens-behind.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/b0-bff-redis-deploy__03-beans-analysis.txt",
|
||
"../../../final/evidence/raw/b1-redis-session-store__03-redis-contents.txt"
|
||
]
|
||
},
|
||
{
|
||
"title": "기본키에 세션 id 가 없어서 두 번째 로그인이 첫 토큰을 덮어썼다",
|
||
"slug": "a-primary-key-without-the-session-id",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-b2"
|
||
],
|
||
"code": [
|
||
"JdbcOAuth2AuthorizedClientService",
|
||
"PRIMARY KEY (client_registration_id, principal_name)"
|
||
],
|
||
"classification": "저장소를 나눠 옮긴 뒤 네 가지를 확인해 둘은 풀리고 둘은 남는 것을 갈랐고, 남은 둘의 원인이 저장소가 아니라 DDL 한 줄임을 확정했다",
|
||
"missing-verification": "세션 id 를 키에 넣은 스키마로 고쳐서 다시 재지 않았다. 덮어쓰기가 사라지는지는 확인하지 않았다",
|
||
"relations": [
|
||
"concept:two-stores-two-lookup-keys",
|
||
"case:moving-the-session-left-the-tokens-behind",
|
||
"reference:look-at-the-lookup-key-before-moving-the-store",
|
||
"decision:split-the-two-stores-and-design-each"
|
||
],
|
||
"kind": "case",
|
||
"ssot-assets": [
|
||
"b2-primary-key-overwrite"
|
||
],
|
||
"ssot-evidence": [
|
||
"raw/b2-multi-instance-session__02-schema.txt",
|
||
"raw/b2-multi-instance-session__04-overwrite-test.txt"
|
||
],
|
||
"publication": "초안",
|
||
"file": "where-application-state-lives/case/case-a-primary-key-without-the-session-id.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [
|
||
"b2-primary-key-overwrite"
|
||
],
|
||
"assetFiles": [
|
||
"b2-primary-key-overwrite"
|
||
],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/b2-multi-instance-session__02-schema.txt",
|
||
"../../../final/evidence/raw/b2-multi-instance-session__04-overwrite-test.txt"
|
||
]
|
||
},
|
||
{
|
||
"title": "회전 경쟁에서 이긴 요청의 토큰도 쓸 수 없었다",
|
||
"slug": "the-winner-of-the-rotation-race-also-loses",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-b3"
|
||
],
|
||
"code": [
|
||
"revokeRefreshToken=true",
|
||
"refreshTokenMaxReuse=0"
|
||
],
|
||
"classification": "동시 5 건을 실제로 경합시켜 「하나는 성공한다」가 아니라 전부 못 쓰게 되는 것을 관측했고, 원인을 client session 삭제로 확정했다",
|
||
"missing-verification": "재시도 설계를 붙여서 다시 재지 않았다. 어떤 재시도가 이 상황을 복구하는지는 확인하지 않았다",
|
||
"relations": [
|
||
"case:a-primary-key-without-the-session-id",
|
||
"decision:split-the-two-stores-and-design-each"
|
||
],
|
||
"kind": "case",
|
||
"ssot-assets": [
|
||
"b3-rotation-contention"
|
||
],
|
||
"ssot-evidence": [
|
||
"raw/b3-refresh-contention__01-concurrent-refresh.txt",
|
||
"raw/b3-refresh-contention__03-client-session-removed.txt"
|
||
],
|
||
"publication": "초안",
|
||
"file": "where-application-state-lives/case/case-the-winner-of-the-rotation-race-also-loses.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [
|
||
"b3-rotation-contention"
|
||
],
|
||
"assetFiles": [
|
||
"b3-rotation-contention"
|
||
],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/b3-refresh-contention__01-concurrent-refresh.txt",
|
||
"../../../final/evidence/raw/b3-refresh-contention__02-session-impact.txt",
|
||
"../../../final/evidence/raw/b3-refresh-contention__03-client-session-removed.txt",
|
||
"../../../final/evidence/raw/b3-refresh-contention__04-policy-comparison.txt"
|
||
]
|
||
},
|
||
{
|
||
"title": "볼륨 없는 영속화와 유예 없는 키 회전",
|
||
"slug": "persistence-without-a-volume-and-rotation-without-overlap",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-b5-b6"
|
||
],
|
||
"code": [
|
||
"appendonly yes",
|
||
"NimbusJwtDecoder",
|
||
"JWKS kid"
|
||
],
|
||
"classification": "설정만 보면 되어 있는 것으로 읽히는 두 자리를 각각 실제로 죽여 보고 회전시켜 확인했으며, 설정과 매체를 나눠 봐야 한다는 결론으로 닫힌다",
|
||
"missing-verification": "볼륨을 붙인 Redis 로 다시 재지 않았다. 영속화가 실제로 동작하는지는 확인하지 않았다",
|
||
"relations": [
|
||
"case:moving-the-session-left-the-tokens-behind",
|
||
"reference:look-at-the-lookup-key-before-moving-the-store"
|
||
],
|
||
"kind": "case",
|
||
"ssot-assets": [
|
||
"b5-b6-storage-and-keys"
|
||
],
|
||
"ssot-evidence": [
|
||
"raw/b5-redis-loss__04-persistence.txt",
|
||
"raw/b6-key-rotation__03-old-key-removed.txt"
|
||
],
|
||
"publication": "초안",
|
||
"file": "where-application-state-lives/case/case-persistence-without-a-volume-and-rotation-without-overlap.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [
|
||
"b5-b6-storage-and-keys"
|
||
],
|
||
"assetFiles": [
|
||
"b5-b6-storage-and-keys"
|
||
],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/b5-redis-loss__04-persistence.txt",
|
||
"../../../final/evidence/raw/b6-key-rotation__03-old-key-removed.txt"
|
||
]
|
||
}
|
||
],
|
||
"concept": [
|
||
{
|
||
"title": "세션과 인가된 클라이언트는 조회 키가 다르다",
|
||
"slug": "two-stores-two-lookup-keys",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-b0"
|
||
],
|
||
"basis-version": "Spring Boot 3 · Spring Security OAuth2 Client · Spring Session Redis",
|
||
"classification": "하나는 세션 id 로 다른 하나는 principal 이름으로 찾는다는 것을 알아야 저장소를 옮긴 결과를 읽을 수 있다",
|
||
"relations": [
|
||
"case:moving-the-session-left-the-tokens-behind",
|
||
"case:a-primary-key-without-the-session-id",
|
||
"decision:split-the-two-stores-and-design-each"
|
||
],
|
||
"kind": "concept",
|
||
"ssot-assets": [
|
||
"bff-store-lookup-keys"
|
||
],
|
||
"publication": "초안",
|
||
"file": "where-application-state-lives/concept/concept-two-stores-two-lookup-keys.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [
|
||
"bff-store-lookup-keys"
|
||
],
|
||
"assetFiles": [
|
||
"bff-store-lookup-keys"
|
||
],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/b0-bff-redis-deploy__03-beans-analysis.txt",
|
||
"../../../final/evidence/raw/b1-redis-session-store__02-autoconfig-after.txt",
|
||
"../../../final/evidence/raw/b1-redis-session-store__03-redis-contents.txt",
|
||
"../../../final/evidence/raw/b2-multi-instance-session__02-schema.txt",
|
||
"../../../final/evidence/raw/b2-multi-instance-session__04-overwrite-test.txt"
|
||
]
|
||
}
|
||
],
|
||
"reference": [
|
||
{
|
||
"title": "저장소를 옮기기 전에 조회 키를 본다",
|
||
"slug": "look-at-the-lookup-key-before-moving-the-store",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-b0",
|
||
"final/document.md#얻은-것-잃은-것-적용하지-않을-때-열린-질문-네-개에-대한-답"
|
||
],
|
||
"classification": "무엇으로 찾는지가 무엇을 옮겨야 하는지를 정한다는 기준이다. 확인한 짝은 세션(세션 id)과 인가된 클라이언트(principal 이름) 하나뿐이고, 다른 짝은 재지 않았다 — 옮기기 전에 같은 확인을 하라는 규칙이다",
|
||
"scope": "서버가 든 상태를 외부 저장소로 옮기는 자리",
|
||
"exceptions": "상태가 하나뿐이고 조회 키도 하나면 이 확인이 필요 없다. 다만 자동구성이 무엇을 골랐는지는 그때도 읽는다",
|
||
"relations": [
|
||
"concept:two-stores-two-lookup-keys",
|
||
"case:a-primary-key-without-the-session-id"
|
||
],
|
||
"kind": "reference",
|
||
"publication": "초안",
|
||
"file": "where-application-state-lives/reference/reference-look-at-the-lookup-key-before-moving-the-store.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/b0-bff-redis-deploy__03-beans-analysis.txt",
|
||
"../../../final/evidence/raw/b1-redis-session-store__02-autoconfig-after.txt",
|
||
"../../../final/evidence/raw/b1-redis-session-store__03-redis-contents.txt",
|
||
"../../../final/evidence/raw/b2-multi-instance-session__02-schema.txt",
|
||
"../../../final/evidence/raw/b2-multi-instance-session__04-overwrite-test.txt",
|
||
"../../../final/evidence/raw/b2-multi-instance-session__05-logout-cleanup.txt"
|
||
]
|
||
}
|
||
],
|
||
"question": [],
|
||
"decision": [
|
||
{
|
||
"title": "세션과 토큰의 저장소를 나눠 각각 설계한다",
|
||
"slug": "split-the-two-stores-and-design-each",
|
||
"readiness": "READY",
|
||
"decision-status": "ADOPTED",
|
||
"source": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-b2",
|
||
"final/document.md#얻은-것-잃은-것-적용하지-않을-때-열린-질문-네-개에-대한-답"
|
||
],
|
||
"decision-evidence": [
|
||
"case:moving-the-session-left-the-tokens-behind",
|
||
"case:a-primary-key-without-the-session-id"
|
||
],
|
||
"grounds": "조회 키가 달라 한쪽을 옮겨도 다른 쪽이 따라오지 않고, 나눠 옮긴 뒤에도 남는 문제의 원인은 저장소가 아니라 스키마였다",
|
||
"classification": "한 저장소로 묶는 대안을 실제로 시도해 반쪽만 옮겨지는 것을 확인한 뒤 나누기로 정했고, 남는 비용을 함께 적었다",
|
||
"relations": [
|
||
"concept:two-stores-two-lookup-keys",
|
||
"case:a-primary-key-without-the-session-id"
|
||
],
|
||
"kind": "decision",
|
||
"publication": "초안",
|
||
"file": "where-application-state-lives/decision/decision-split-the-two-stores-and-design-each.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/b1-redis-session-store__02-autoconfig-after.txt",
|
||
"../../../final/evidence/raw/b2-multi-instance-session__01-jdbc-store-deploy.txt",
|
||
"../../../final/evidence/raw/b2-multi-instance-session__02-schema.txt",
|
||
"../../../final/evidence/raw/b2-multi-instance-session__03-plaintext-tokens.txt",
|
||
"../../../final/evidence/raw/b2-multi-instance-session__04-overwrite-test.txt",
|
||
"../../../final/evidence/raw/b2-multi-instance-session__05-logout-cleanup.txt"
|
||
]
|
||
}
|
||
],
|
||
"setup": [
|
||
{
|
||
"title": "아무 저장소도 주지 않고 Spring 이 무엇을 고르는지 찍어서 확인한다",
|
||
"kind": "setup",
|
||
"slug": "reproduce-b0-default-session-store",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-0",
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서"
|
||
],
|
||
"pinned-versions": [
|
||
"keycloak-pattern-bff = lab"
|
||
],
|
||
"classification": "**아홉 편의 첫 편이고 뒤의 여덟이 이 절차가 만든 상태 위에 선다** — B-1·B-2·B-4·B-5·B-7 이 전제에 「B-0 이 끝나 있다」를 적는다. **주입이 둘인데 첫째가 소스를 B-0 시점으로 되돌리는 일이다** — 어느 브랜치에도 B-0 시점의 파일이 없어서 `pom.xml`·`SecurityConfig`·`application.yml`·`bff-redis.yaml` 넷을 편집기로 열어 B-1·B-2 가 넣은 것을 손으로 뺀다. 그대로 배포하면 B-2 의 결과를 재게 된다. **Redis 는 배포만 하고 연결하지 않는다** — 먼저 붙이면 잴 것이 없어진다. **읽는 사람이 그대로 치는 명령이라 Setup 이다** — `vim`·`docker build`·`k3s ctr images import`·`kcadm`·`kubectl` 이 40~60분어치 이어지고 Case 의 평문 한 칸에 담으면 복사가 안 된다. **순서가 결과를 바꾸는 곳이 셋**(observed) — 이미지를 한 노드에만 import 하면 나머지 replica 가 `ErrImageNeverPull` 이고, `docker build` 기본 출력은 마지막 몇 줄뿐이라 `--progress=plain` 과 파일 없이는 `processDuplicateKeys` 가 안 보이며, brower 로 로그인하기 전에 쿠키를 안 지우면 앞선 실패의 세션이 결과를 섞는다. **판정은** 전체 빈 수 `321` 과 저장소 관련 빈 다섯 줄, 그리고 `--- Redis / Spring Session 이 구성되었는가 ---` 칸의 「★ 없음」이다. `/actuator/beans` 는 117KB 라 프록시에서 `Bad Gateway` 이므로 파드 안에서 받고, `200` 인데 `<!DOCTYPE html` 이면 Keycloak 로그인 페이지를 받은 것이라 `head -c 200` 으로 내용을 본다. **유효 범위** — 스티키 세션을 켜면 replica 2 에서 로그인이 되는지는 재지 않았다. 빈을 세고 이름·타입을 뽑는 `grep`·`sed` 줄과 `jq` 판본은 가이드가 미검증으로 표시했다(unknown).",
|
||
"relations": [
|
||
"case:moving-the-session-left-the-tokens-behind",
|
||
"concept:two-stores-two-lookup-keys",
|
||
"reference:look-at-the-lookup-key-before-moving-the-store",
|
||
"setup:reproduce-b1-redis-session-store",
|
||
"setup:reproduce-b2-jdbc-token-store"
|
||
],
|
||
"publication": "게시됨",
|
||
"file": "where-application-state-lives/setup/setup-reproduce-b0-default-session-store.md",
|
||
"status": "게시 전",
|
||
"studioId": "4ef91d43-1f1d-481a-8b6f-5e015d068578",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": []
|
||
},
|
||
{
|
||
"title": "Redis 를 붙이고 무엇이 옮겨졌는지 빈 목록으로 견준다",
|
||
"kind": "setup",
|
||
"slug": "reproduce-b1-redis-session-store",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-1"
|
||
],
|
||
"pinned-versions": [
|
||
"keycloak-pattern-bff = lab",
|
||
"Redis = 7.4.x"
|
||
],
|
||
"classification": "의존성 **둘**(`spring-session-data-redis` · `spring-boot-starter-data-redis`)을 함께 넣고 다시 빌드해 두 노드에 밀어 넣는 절차다. **하나만 넣으면 오류 없이 in-memory 로 남는다** — 그래서 찍어서 확인하는 단계가 따로 있다. **첫 배포를 일부러 고장 난 채로 한다** — `enableServiceLinks` 를 빼고 올려 쿠버네티스가 Service 이름마다 넣는 `REDIS_PORT=tcp://10.43.57.116:6379` 로 파드가 `Failed to bind properties` 에서 죽는 것을 한 번 보고, 그 뒤에 `enableServiceLinks: false` 로 고친다. 건너뛰고 처방부터 시작해도 결과는 같다고 가이드가 적는다. **순서가 결과를 바꾸는 곳이 둘**(observed) — 주입 전에 `printenv | grep -i redis` 를 안 보면 그 오류를 「Redis 가 죽어서」로 읽고(`redis-cli ping` 은 `PONG` 이다), 테스트에 `spring.session.store-type=none` 을 안 주면 빌드가 테스트 단계에서 죽는데 메시지가 Redis 연결 오류라 배포 문제로 읽힌다. **판정은** 빈 수 `321 → 402 (+81)` 과 새로 생긴 세션 저장소 빈 여섯, 그리고 **안 바뀐** authorized client 셋의 before·after 다. 키를 잡을 때 `grep -v expires` 를 빼면 만료 추적 키를 잡아 다음 명령이 빈 결과를 내고, `tr -d '\\r'` 을 빼면 키가 안 맞는데 오류는 안 난다. **유효 범위** — `--scan` 출력에서 키를 눈으로 골라 치는 두 단계 형태와 `jq` 판본은 가이드에 없다(unknown).",
|
||
"relations": [
|
||
"case:moving-the-session-left-the-tokens-behind",
|
||
"case:nine-injections-that-silently-did-nothing",
|
||
"concept:two-stores-two-lookup-keys",
|
||
"setup:reproduce-b0-default-session-store",
|
||
"setup:reproduce-b2-jdbc-token-store"
|
||
],
|
||
"publication": "게시됨",
|
||
"file": "where-application-state-lives/setup/setup-reproduce-b1-redis-session-store.md",
|
||
"status": "게시 전",
|
||
"studioId": "1e5d05fa-88f4-4ef5-a402-4a520ae4a52d",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": []
|
||
},
|
||
{
|
||
"title": "토큰을 PostgreSQL 로 옮기고 기본키와 로그아웃 정리를 확인한다",
|
||
"kind": "setup",
|
||
"slug": "reproduce-b2-jdbc-token-store",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-2"
|
||
],
|
||
"pinned-versions": [
|
||
"keycloak-pattern-bff = lab",
|
||
"Redis = 7.4.x"
|
||
],
|
||
"classification": "토큰을 `JdbcOAuth2AuthorizedClientService` 로 옮긴 뒤 **주입 전에 기본키를 눈으로 읽는** 절차다. `\\d oauth2_authorized_client` 의 맨 아래 `PRIMARY KEY, btree (client_registration_id, principal_name)` 한 줄이 이 편의 답이라 그 줄을 보기 전에는 다음으로 넘어가지 않는다. **스키마 초기화가 조용히 실패하는 것을 먼저 만난다** — 기본 DDL 의 `blob` 은 PostgreSQL 에 없는 타입이고 `spring.sql.init.continue-on-error: true` 가 그 실패를 삼켜 파드는 정상으로 보인다. DDL 은 터미널에 붙여 넣지 않고 `/tmp/oauth2-pg.sql` 로 만들어 `exec -i` 로 넘기며, **`-i` 를 빼면 아무 일도 안 일어나고 오류도 안 난다**(observed). **주입은 「두 브라우저」가 아니라 세션을 지우고 같은 사용자로 다시 로그인시키는 것이다** — 조회 키가 같아 등가이고, 해설 문서가 처음에 「두 브라우저에서」라고 적었다가 측정하지 않은 것을 적었다고 정정했다. **판정은 셋을 한꺼번에 본다** — 행 수 `(1 row)` 가 그대로인 것, `at_md5` 가 `675af228…` → `e19a63fc…` 로 바뀐 것, `issued_at` 이 삭제 시각 뒤인 것. 하나만 보면 INSERT 인지 UPDATE 인지 갈리지 않는다. **`FLUSHALL` 을 치지 않는다** — 같은 Redis 에 `_oauth2_proxy-` 키가 있어 B-7 이 오염된다. **유효 범위** — 진짜 두 브라우저로 같은 결과가 나오는지, RP-initiated logout 을 열었을 때 세션 수가 줄어드는지는 재지 않았다(unknown).",
|
||
"relations": [
|
||
"case:a-primary-key-without-the-session-id",
|
||
"case:nine-injections-that-silently-did-nothing",
|
||
"reference:look-at-the-lookup-key-before-moving-the-store",
|
||
"decision:split-the-two-stores-and-design-each",
|
||
"setup:reproduce-b1-redis-session-store",
|
||
"setup:reproduce-b3-refresh-contention"
|
||
],
|
||
"publication": "게시됨",
|
||
"file": "where-application-state-lives/setup/setup-reproduce-b2-jdbc-token-store.md",
|
||
"status": "게시 전",
|
||
"studioId": "df2ee798-7f31-4570-a355-11f96c0eea84",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": []
|
||
},
|
||
{
|
||
"title": "같은 refresh token 다섯 개를 동시에 던지고 client session 을 센다",
|
||
"kind": "setup",
|
||
"slug": "reproduce-b3-refresh-contention",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-3"
|
||
],
|
||
"pinned-versions": [
|
||
"curlimages/curl = 8.11.1"
|
||
],
|
||
"classification": "**BFF 를 거치지 않고 토큰 엔드포인트를 직접 치는 유일한 편이다** — Keycloak 쪽 동작만 갈라 보려는 것이고, direct grant 를 쓰므로 브라우저도 필요 없다. Keycloak 이미지에는 `curl` 도 `wget` 도 없어(`exit 127`) `sleep 7200` 짜리 상주 탐침 파드를 띄우고 `exec` 로 이어간다 — `--rm` 임시 파드는 발급받은 토큰을 다음 단계로 못 넘긴다. **주입은 realm 설정 하나다** — `revokeRefreshToken` 이 회전 스위치이고 이름이 「회전」이 아니라 「취소」다. `refreshTokenMaxReuse` 는 그것이 켜져야 의미가 있다. **realm 전체에 걸리므로 같은 realm 을 쓰는 BFF 로그인도 영향을 받고, 끝나면 반드시 되돌린다.** **순서가 결과를 바꾸는 곳이 셋**(observed) — `&` 와 `wait` 을 빼면 다섯이 순차로 돌아 전부 `200` 이 나오는데 그건 「경쟁이 없었다」가 아니라 「주입이 안 걸렸다」이고, 갱신마다 `RT` 를 다시 담지 않으면 `400` 이 경쟁 탓인지 옛 토큰 탓인지 갈리지 않으며, 켜기 전에 발급한 토큰으로 재면 검증 시점의 정책이 섞인다. **판정은 성공 개수가 아니라 오류 문구가 두 종류인 것과, 이긴 요청이 받은 토큰을 다시 썼을 때의 `400` 과, 그 sid 의 `client_sessions` 가 `0`(대조군은 `1`)인 것이다.** `revoked_token` 은 `0` 이라 블랙리스트가 아니라 client session 삭제가 원인이다. **유효 범위** — BFF 를 거쳐도 같은 경쟁이 나는지, 동시성을 5보다 늘리면 어떻게 되는지는 재지 않았고, `maxReuse ≥ N-1` 은 구성 A·C 에서 따라 나온 추론이다(unknown).",
|
||
"relations": [
|
||
"case:the-winner-of-the-rotation-race-also-loses",
|
||
"case:a-primary-key-without-the-session-id",
|
||
"reference:verify-the-injection-landed-separately-from-the-result",
|
||
"setup:reproduce-b2-jdbc-token-store"
|
||
],
|
||
"publication": "게시됨",
|
||
"file": "where-application-state-lives/setup/setup-reproduce-b3-refresh-contention.md",
|
||
"status": "게시 전",
|
||
"studioId": "405c4206-9b59-491f-aed1-8b97cfd9f584",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": []
|
||
},
|
||
{
|
||
"title": "Redis 를 0대로 내리고 파드가 Ready 를 유지하는지 본다",
|
||
"kind": "setup",
|
||
"slug": "reproduce-b5-redis-loss",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-5"
|
||
],
|
||
"pinned-versions": [
|
||
"Redis = 7.4.x",
|
||
"netty-transport = 4.1.135.Final"
|
||
],
|
||
"classification": "주입이 둘이고 **둘째가 지금 실험대에서 그때를 재현하려고 방향을 뒤집은 단계다** — 매니페스트에 B-5 의 결론이 이미 반영되어 PVC 와 `--appendonly yes` 가 들어 있어서, 볼륨을 떼고 AOF 만 켠 채 파드를 지운다. 원래 실행은 반대 순서였다(unknown). **`delete pod` 이 아니라 `scale --replicas=0` 을 쓴다** — 없는 상태가 유지되어야 관찰할 시간이 생긴다. **영속화를 말하기 전에 `/data` 가 볼륨인지부터 본다** — `spec.volumes` · `volumeMounts` 의 `mountPath` · PVC 가 `Bound` 인 것 셋이 전부 성립해야 하고, 하나라도 빠지면 `appendonly yes` 는 장식이다. Redis 는 시킨 대로 `appendonlydir` 을 만들고 매 쓰기를 기록하므로 **디렉터리가 생긴 것을 「영속화가 켜졌다」로 읽으면 틀린다.** **`--max-time` 을 모든 외부 확인에 준다** — 주입 뒤 그 요청은 응답이 안 오고, 없으면 터미널이 붙잡힌 것을 「내 터미널이 이상함」으로 읽는다. **판정은** 세 경로의 `200 · 000 · 503`, `readiness` 가 `200` 이고 `{\"status\":\"UP\"}` 인 것, 엔드포인트에 두 IP 가 그대로 ready 인 것이다. **BFF 를 재시작하고 싶은 충동을 참는다** — 재시작하면 「스스로 회복하는가」를 영영 못 재고, 실제로는 `0,0 회 재시작` 으로 돌아왔다. **측정 실패를 값으로 읽지 않는다** — `03-health-groups.txt` 의 `/actuator/health HTTP server` 는 상태 코드가 아니다. **유효 범위** — Lettuce 타임아웃을 줄여 `000` 이 `500` 이 되는지, readiness 그룹에 `redis` 를 넣으면 A-2 와 같은 모양이 되는지는 재지 않았다.",
|
||
"relations": [
|
||
"case:persistence-without-a-volume-and-rotation-without-overlap",
|
||
"concept:readiness-hides-the-broken-node",
|
||
"concept:the-up-metric-cannot-see-alive-but-useless",
|
||
"setup:reproduce-a2-database-loss",
|
||
"setup:reproduce-b1-redis-session-store"
|
||
],
|
||
"publication": "게시됨",
|
||
"file": "where-application-state-lives/setup/setup-reproduce-b5-redis-loss.md",
|
||
"status": "게시 전",
|
||
"studioId": "af9645a0-8ca8-481d-9624-69fce6449b7c",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": []
|
||
},
|
||
{
|
||
"title": "서명 키를 더한 뒤 옛 키를 지우고 옛 토큰이 언제 끊기는지 본다",
|
||
"kind": "setup",
|
||
"slug": "reproduce-b6-key-rotation",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-6"
|
||
],
|
||
"pinned-versions": [
|
||
"curl = 8.5.0"
|
||
],
|
||
"classification": "**되돌릴 수 없는 절차다** — 지우는 것은 키 공급자이고 개인키가 함께 사라지며, 같은 이름으로 다시 만들어도 `kid` 가 다른 새 키 쌍이 된다. 그래서 관찰 절로 넘어가기 전에 셋을 확인한다 — 이 realm 이 실험대 전용인가, 잃으면 곤란한 세션이 있는가, 주입 검증의 `old 200` 을 실제로 봤는가. **회전은 「바꾸기」가 아니라 「우선순위가 더 높은 공급자를 추가하기」다** — `config.*` 값이 대괄호로 감싼 배열이어야 하고 `priority` 를 낮게 주면 새 키가 만들어져도 발급에 안 쓰여 `kid` 가 안 바뀐다. **`-q` 필터가 오류도 종료코드도 없이 빈 결과를 준다** — 「키 공급자가 하나도 없구나」로 읽으면 이 절차 전체가 무너지므로 `--fields id,name,providerId` 로 전체를 받는다. **지울 대상을 바꿔 지우면 실험이 뒤집힌다** — 남길 것이 `rsa-rotated`, 지울 것이 `rsa-generated` 이고 `\"id\"` 는 `\"name\"` 보다 위에 있다. **판정은** RS256 키 수가 `1 → 2 → 1` 로 움직이는 것과 겹침 구간의 `옛 200 · 새 200`, 제거 뒤의 `옛 401 · 새 200` 이다. **`kid` 를 세는 단위가 다르다** — JWKS 에는 암호화용 키도 실리므로 `grep kid | wc -l` 은 서명 키를 과다 계산한다. **401 을 만나면 만료(60초)부터 가른다.** **유효 범위** — 「겹침은 최소 30분」은 access token 60초와 refresh token 1800초에서 따라 나온 추론이고, 추가와 제거를 30분 벌려 재지는 않았다(unknown).",
|
||
"relations": [
|
||
"case:persistence-without-a-volume-and-rotation-without-overlap",
|
||
"case:nine-injections-that-silently-did-nothing",
|
||
"reference:verify-the-injection-landed-separately-from-the-result",
|
||
"setup:reproduce-b7-cookie-secret-rotation"
|
||
],
|
||
"publication": "게시됨",
|
||
"file": "where-application-state-lives/setup/setup-reproduce-b6-key-rotation.md",
|
||
"status": "게시 전",
|
||
"studioId": "7447ccbf-1800-43a4-a9d2-8ac774965c4b",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": []
|
||
}
|
||
]
|
||
},
|
||
"topic": "where-application-state-lives"
|
||
},
|
||
"trust-handed-over-at-the-edge": {
|
||
"title": "위조 신원 헤더와 로그아웃 전파",
|
||
"readerQuestion": "엣지가 만든 인증 결과를 앱은 어디까지 믿어도 되는가?",
|
||
"kinds": {
|
||
"case": [
|
||
{
|
||
"title": "nginx 는 자기가 설정하지 않은 헤더를 덮어쓰지 않았다",
|
||
"slug": "nginx-does-not-overwrite-a-header-it-never-sets",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-b4"
|
||
],
|
||
"code": [
|
||
"proxy_set_header X-Auth-Request-Roles",
|
||
"--cookie-refresh"
|
||
],
|
||
"classification": "위조 헤더를 실제로 보내 통과하는 것을 관측하고 원인을 지우지 않은 설정에서 확정했으며, 클레임 변경이 재인증까지 반영되지 않는 것도 같은 실험에서 닫았다",
|
||
"missing-verification": "--cookie-refresh 를 켠 구성에서는 재지 않았다. 클레임 반영 지연이 얼마나 줄어드는지는 확인하지 않았다",
|
||
"relations": [
|
||
"reference:clear-the-header-before-you-trust-it",
|
||
"case:nobody-implemented-backchannel-logout"
|
||
],
|
||
"kind": "case",
|
||
"ssot-assets": [
|
||
"b4-header-trust-boundary"
|
||
],
|
||
"ssot-evidence": [
|
||
"raw/b4-edge-authorization__01-header-handling.txt"
|
||
],
|
||
"publication": "초안",
|
||
"file": "trust-handed-over-at-the-edge/case/case-nginx-does-not-overwrite-a-header-it-never-sets.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [
|
||
"b4-header-trust-boundary"
|
||
],
|
||
"assetFiles": [
|
||
"b4-header-trust-boundary"
|
||
],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/b4-edge-authorization__01-header-handling.txt",
|
||
"../../../final/evidence/raw/followup__03-b4-role-propagation.txt"
|
||
]
|
||
},
|
||
{
|
||
"title": "백채널 로그아웃은 양쪽 다 없었다",
|
||
"slug": "nobody-implemented-backchannel-logout",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-c층"
|
||
],
|
||
"code": [
|
||
"backchannelLogoutUrl",
|
||
"oidcLogout"
|
||
],
|
||
"classification": "네 가지를 순서대로 확인해 설정 결손과 기능 부재가 한 경로에 겹쳐 있는 것을 갈랐고, 한쪽만 고쳐서는 되지 않는다는 결론으로 닫힌다",
|
||
"missing-verification": "양쪽을 다 구현해서 전파가 되는지 확인하지 않았다. 원인 확정까지가 이 실험의 범위다",
|
||
"relations": [
|
||
"case:nginx-does-not-overwrite-a-header-it-never-sets",
|
||
"reference:clear-the-header-before-you-trust-it"
|
||
],
|
||
"kind": "case",
|
||
"ssot-assets": [
|
||
"c2-backchannel-both-sides"
|
||
],
|
||
"ssot-evidence": [
|
||
"raw/c2-backchannel-logout__01-current-state.txt",
|
||
"raw/c2-backchannel-logout__03-logout-attempt.txt",
|
||
"raw/c2-backchannel-logout__04-reachability.txt"
|
||
],
|
||
"publication": "초안",
|
||
"file": "trust-handed-over-at-the-edge/case/case-nobody-implemented-backchannel-logout.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [
|
||
"c2-backchannel-both-sides"
|
||
],
|
||
"assetFiles": [
|
||
"c2-backchannel-both-sides"
|
||
],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/c2-backchannel-logout__01-current-state.txt",
|
||
"../../../final/evidence/raw/c2-backchannel-logout__03-logout-attempt.txt",
|
||
"../../../final/evidence/raw/c2-backchannel-logout__04-reachability.txt"
|
||
]
|
||
},
|
||
{
|
||
"title": "쿠키에 세션을 담으면 지울 대상을 잃는다 — TTL 로 되찾은 고아 세션",
|
||
"slug": "orphan-sessions-and-the-ttl-that-finds-them",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-b7-b7a"
|
||
],
|
||
"code": [
|
||
"--cookie-secret",
|
||
"--cookie-refresh",
|
||
"refresh:disabled"
|
||
],
|
||
"classification": "secret 교체가 티켓을 못 풀게 만든 상태에서 무엇을 지울지 고르는 방법을 TTL 역산으로 세우고 로그와 1 초 차로 교차검증해 닫았다",
|
||
"missing-verification": "--cookie-refresh 를 켠 구성에서는 이 역산이 성립하지 않는다. 그 경우의 정리 방법은 재지 않았다",
|
||
"relations": [
|
||
"case:nginx-does-not-overwrite-a-header-it-never-sets",
|
||
"reference:clear-the-header-before-you-trust-it"
|
||
],
|
||
"kind": "case",
|
||
"ssot-assets": [
|
||
"b7-cookie-session-tradeoff"
|
||
],
|
||
"ssot-evidence": [
|
||
"raw/b7a-orphan-session__01-orphan-lifecycle.txt",
|
||
"raw/b7-cookie-secret__01-deploy.txt"
|
||
],
|
||
"publication": "초안",
|
||
"file": "trust-handed-over-at-the-edge/case/case-orphan-sessions-and-the-ttl-that-finds-them.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [
|
||
"b7-cookie-session-tradeoff"
|
||
],
|
||
"assetFiles": [
|
||
"b7-cookie-session-tradeoff"
|
||
],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/b7a-orphan-session__01-orphan-lifecycle.txt",
|
||
"../../../final/evidence/raw/b7-cookie-secret__01-deploy.txt"
|
||
]
|
||
}
|
||
],
|
||
"concept": [],
|
||
"question": [],
|
||
"decision": [],
|
||
"reference": [
|
||
{
|
||
"title": "믿기 전에 그 헤더를 먼저 지운다",
|
||
"slug": "clear-the-header-before-you-trust-it",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-b4",
|
||
"final/document.md#얻은-것-잃은-것-적용하지-않을-때-열린-질문-네-개에-대한-답"
|
||
],
|
||
"classification": "프록시가 동명 헤더를 자동으로 덮어쓸 것이라는 전제가 틀렸다는 것을 확인했다. 본 것은 host nginx 의 설정 한 번이고, 같은 체인에 있는 k3s Traefik 의 헤더 처리조차 재지 않았다 — 그래서 규칙은 「다른 프록시도 이렇다」가 아니라 「믿기 전에 그 프록시에서 확인하라」다",
|
||
"scope": "엣지가 인증 결과를 헤더로 실어 업스트림에 넘기는 구성",
|
||
"exceptions": "업스트림이 헤더가 아니라 서명된 토큰을 검증하면 이 지우기가 필요 없다",
|
||
"relations": [
|
||
"case:nginx-does-not-overwrite-a-header-it-never-sets",
|
||
"case:orphan-sessions-and-the-ttl-that-finds-them"
|
||
],
|
||
"kind": "reference",
|
||
"publication": "초안",
|
||
"file": "trust-handed-over-at-the-edge/reference/reference-clear-the-header-before-you-trust-it.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/b4-edge-authorization__01-header-handling.txt",
|
||
"../../../final/evidence/raw/followup__03-b4-role-propagation.txt"
|
||
]
|
||
}
|
||
],
|
||
"setup": [
|
||
{
|
||
"title": "신원 헤더를 위조해 보내고 어디까지 도착하는지 본다",
|
||
"kind": "setup",
|
||
"slug": "reproduce-b4-forged-identity-headers",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-4"
|
||
],
|
||
"pinned-versions": [
|
||
"curl = 8.5.0"
|
||
],
|
||
"classification": "**앞부분은 클러스터 상태를 하나도 안 바꾸고 `curl` 한 줄로 끝난다 — 그 사실 자체가 이 절차의 무게다.** 1~3절만 하고 멈춰도 결론 대부분이 나오고, 4절부터 Grafana 의 Ingress 를 빌리고 IdP 의 사용자 속성을 바꾼다. **무엇을 재는 경로인지 먼저 못박는다** — 위조 헤더를 보내는 `app1.hyeonworks.com/api/echo` 는 `permitAll` 이라 oauth2-proxy 를 거치지 않는다. **헤더가 도착한 것과 인가가 뚫린 것은 다른 사건이고**, 같은 헤더를 `/api/me` 에 보내면 `401` 이라 그 대조를 같이 친다. **대조군을 안 보면 판정이 성립하지 않는다** — 주입 전에 `x-auth-request-*` 칸이 비어 있는 것을 봐야 뒤에 나온 값을 「내가 보낸 것」이라고 말할 수 있다. **`tr ',' '\\n' | grep` 을 쓰면 안 된다** — 값 배열이 `[\"admin\",\"editor\"]` 처럼 쉼표를 품어 두 줄로 잘리고 「하나만 도착했다」로 읽게 된다. `grep -o '…\\[[^]]*\\]'` 로 대괄호째 뽑는다. **크기 훑기의 `000` 은 오류가 아니라 측정 결과다** — `400`(Tomcat)과 `000`(nginx 가 연결을 끊음)이 다른 값이다. **두 기계의 로그를 나란히 놓기 전에 시계를 맞춘다** — 보정 전에는 12회 요청이 변경 시각보다 앞서 보여 결론이 통째로 뒤집힌다(약 107초). **호스트 sudo 는 비밀번호를 요구하므로 빈 출력을 「설정이 없다」로 읽지 않는다.** **유효 범위** — `proxy_set_header X-Auth-Request-* \"\"` 처방은 이 실험대가 적용한 적이 없어 **위조가 막히는지는 측정으로 없다**(unknown).",
|
||
"relations": [
|
||
"case:nginx-does-not-overwrite-a-header-it-never-sets",
|
||
"reference:clear-the-header-before-you-trust-it",
|
||
"reference:never-subtract-values-from-two-clocks",
|
||
"setup:reproduce-b7-cookie-secret-rotation"
|
||
],
|
||
"publication": "게시됨",
|
||
"file": "trust-handed-over-at-the-edge/setup/setup-reproduce-b4-forged-identity-headers.md",
|
||
"status": "게시 전",
|
||
"studioId": "9bb0b051-c158-497f-a744-b24769c93783",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": []
|
||
},
|
||
{
|
||
"title": "cookie secret 을 갈아치우고 로그인해 있던 세션이 어떻게 되는지 본다",
|
||
"kind": "setup",
|
||
"slug": "reproduce-b7-cookie-secret-rotation",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-7"
|
||
],
|
||
"pinned-versions": [
|
||
"Redis = 7.4.x",
|
||
"curl = 8.5.0"
|
||
],
|
||
"classification": "**남의 도메인을 빌리는 절차다** — 인증서가 `auth`·`app1`·`app2` 만 덮어 네 번째 이름을 못 만들므로 Grafana 의 Ingress 를 내리고 `app2` 를 빌린 뒤 반드시 돌려준다. **백업 파일이 비어 있는데 원본부터 지우는 사고를 `wc -l` 과 `grep -c` 가 막는다.** **바꾸기 전에 「겹칠 수 있는가」부터 묻는다** — `--help | grep cookie-secret` 이 `--cookie-secret string` 한 줄만 내고 복수형도 `--old-cookie-secret` 도 없다. 그 한 줄이 이 편의 답이고 나머지는 「그래서 무슨 일이 나는가」다. **patch 의 `env/1` 은 매니페스트 순서에 달린 값이라 그대로 믿지 않는다** — 틀리면 클라이언트 비밀을 쿠키 secret 으로 덮어쓴다. **회전만으로는 아무 일도 안 난다** — 누군가 옛 쿠키를 들고 와야 벌어지므로 여기서 「실험 실패」로 읽지 않는다. **로그인 화면이 안 뜨는 것을 「무중단」으로 읽으면 정확히 뒤집어 읽는 것이다** — 쿠키는 죽었고 SSO 가 재인증을 가렸으며, 그 증거가 로그의 `Initiating login` 과 `AuthSuccess` 다. **두 줄이 다른 말을 한다** — `stored_session.go:94` 는 「지우겠다」이고 `:97` 이 「그 지우기가 실패했다」이며 94 만 보면 「정리됐구나」로 읽는다. `--prefix` 없이는 replica 둘의 로그를 못 가른다. **회전 시각을 UTC 로 적어 둔다** — B-7a 의 정리 규칙이 이 값을 기준으로 고아를 고른다. **유효 범위** — IdP SSO 가 없거나 만료된 경우에 전원이 로그인 화면을 보는지는 재지 않았다(unknown).",
|
||
"relations": [
|
||
"case:orphan-sessions-and-the-ttl-that-finds-them",
|
||
"case:nginx-does-not-overwrite-a-header-it-never-sets",
|
||
"setup:reproduce-b7a-orphan-session",
|
||
"setup:reproduce-b6-key-rotation",
|
||
"setup:reproduce-b0-default-session-store"
|
||
],
|
||
"publication": "게시됨",
|
||
"file": "trust-handed-over-at-the-edge/setup/setup-reproduce-b7-cookie-secret-rotation.md",
|
||
"status": "게시 전",
|
||
"studioId": "d53181b9-bb28-4703-97ed-98adfb5b18dc",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": []
|
||
},
|
||
{
|
||
"title": "TTL 로 고아 세션을 골라내 지운다",
|
||
"kind": "setup",
|
||
"slug": "reproduce-b7a-orphan-session",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-7a"
|
||
],
|
||
"pinned-versions": [
|
||
"Redis = 7.4.x"
|
||
],
|
||
"classification": "B-7 이 「지우지 못했다」로 멈춘 자리에서 시작해 **못 지우는 주체가 프록시인지 Redis 인지 가르는** 절차다. 회전을 두 번 치고, 두 번째 회전이 「일회성인가 누적인가」를 가른다 — 1차를 살아남은 세션이 2차에서 고아가 된다. **`refresh:disabled` 를 맨 먼저 확인한다. 이 한 단어가 정리 규칙 전체의 전제다** — `--cookie-refresh` 가 켜져 있으면 오래 안 쓴 산 세션이 고아로 오판되어 지워지므로 이 절차를 쓰면 안 되고, 그때는 `_oauth2_proxy-*` 를 전부 지우고 모두 재인증시키는 편이 정직하다. **구분 신호가 TTL 하나뿐이라는 것을 열을 하나씩 지워 보인다** — 이름 접두사도 `type` 도 같고 `strlen` 은 바이트 단위로 `3510` 으로 같으며 값은 암호화되어 뜻을 못 읽는다. **판정 전에 규칙 자체를 검증한다** — 역산한 생성시각 `11:30:26` 과 로그의 `AuthSuccess 11:30:27` 이 1초 차이고, 그 1초는 TTL 이 초 단위 정수라서 생기는 반올림이다. **`del` 을 바로 붙이지 않는다** — 같은 루프를 `echo` 로 먼저 돌려 「산것」이 지금 로그인해 있는 사람 수와 맞는지 본다. **`NOW` 를 루프 밖에서 한 번만 잡는다** — 안에서 잡으면 키마다 기준 시각이 달라진다. **`date` 는 전부 `-u` 로 친다** — 프록시 로그가 UTC 라 섞이면 9시간이 틀어진다. **`FLUSHDB` 를 쓰지 않는다** — 같은 Redis 에 BFF 세션이 있다. **유효 범위** — 고아를 1시간 두고 실제로 만료되는 것을 끝까지 지켜보지는 않았고, 역산으로 고아를 고르는 루프 자체는 가이드가 미검증으로 표시했다(unknown).",
|
||
"relations": [
|
||
"case:orphan-sessions-and-the-ttl-that-finds-them",
|
||
"case:commands-written-as-prose-do-not-run",
|
||
"reference:never-subtract-values-from-two-clocks",
|
||
"setup:reproduce-b7-cookie-secret-rotation"
|
||
],
|
||
"publication": "게시됨",
|
||
"file": "trust-handed-over-at-the-edge/setup/setup-reproduce-b7a-orphan-session.md",
|
||
"status": "게시 전",
|
||
"studioId": "8d6b8a7f-e08e-4c9f-9772-a14a9d569260",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": []
|
||
},
|
||
{
|
||
"title": "두 앱을 한 로그인으로 묶고 IdP 세션만 끊어 앱 세션이 남는지 본다",
|
||
"kind": "setup",
|
||
"slug": "reproduce-c1-multi-app-sso",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#c층-재현-절차-두-편을-직접-치는-순서-c-1",
|
||
"final/document.md#c층-재현-절차-두-편을-직접-치는-순서"
|
||
],
|
||
"pinned-versions": [
|
||
"keycloak-pattern-bff = lab",
|
||
"Redis = 7.4.x"
|
||
],
|
||
"classification": "**두 편의 첫 편이고 C-2 가 이 절차가 만든 상태 위에 선다** — C-2 의 전제가 「C-1 이 끝나 있다」이고, 빌린 이름도 C-2 를 이어서 할 생각이면 아직 돌려주지 않는다. **앱이 둘 필요한 첫 편이다** — B-2 의 app1(BFF)과 B-7 의 app2(oauth2-proxy)가 둘 다 떠 있어야 하고, `app2.hyeonworks.com` 은 Grafana 에서 빌린 이름이라 끝나면 Ingress 를 돌려준다. **브라우저가 필요하다** — SSO 는 `auth.hyeonworks.com` 에 붙은 쿠키가 만드는 현상이라 `curl` 로는 「로그인 화면이 안 떴다」를 못 본다. 다른 브라우저나 시크릿 창에서 열면 로그인 화면이 뜨는 것이 정상이다. **읽는 사람이 그대로 치는 명령이라 Setup 이다** — `psql`·`redis-cli`·`kcadm`·`kubectl`·브라우저가 약 20분어치 이어지고 Keycloak 재시작에만 1~2분이 든다. **주입 전 절차가 한 번 꺾인다** — 세션을 지우려다 `logout-all` 이 오류 없이 안 먹는 것을 먼저 보고(캐시가 답한다) DB 직접 삭제와 `rollout restart` 로 지운 뒤, **세는 법을 고친다.** `offline_user_session` 에는 모든 realm 의 세션이 들어 있고 `kcadm` 을 치는 순간 `master` 에 admin 세션이 생기므로 realm 을 조인하지 않은 숫자는 뜻이 없다. **순서가 결과를 바꾸는 곳이 셋**(observed) — 이 단계를 건너뛰면 관찰 절의 「남은 세션 1」을 「로그아웃이 안 먹었다」로 정반대로 읽고, 세션 id 를 지목하는 `kcadm delete sessions/<id>` 는 오류도 없이 안 먹으므로 `users/<id>/logout` 을 써야 하며, `flushall` 은 BFF 세션까지 지우므로 깨끗한 상태를 만들 때만 친다. **판정은** 같은 `user_session_id` 에 `client_sessions` 가 1 → 2 로 는 것, IdP 세션을 끊은 뒤 남은 세션 1의 realm 이 `master` 인 것, 그리고 Redis 키 두 줄이 **글자 하나까지 같은 것**이다. **화면으로는 판정하지 않는다** — 스크린샷 두 장은 md5 `2c703176…` 로 동일한 파일이고, 화면이 같아 보인다는 것 자체가 이 편의 결론이라 구별은 터미널 출력이 한다. **버전은 이 편이 직접 잰 값이 아니다**(inferred) — C-1 출력에는 판 번호가 한 번도 안 찍혔고, SSOT C층 머리말이 「같은 실험대의 B층 출력을 본다」고 적는다. **유효 범위** — 앱 세션이 실제로 언제 끊기는지는 **기다려서 확인하지 않았다.** `Session not active` 가 나온다는 것은 추론이고, 수명 두 값을 읽는 `get realms … --fields` 와 realm·클라이언트 이름을 조인하는 쿼리들은 가이드가 미검증으로 표시했다(unknown).",
|
||
"relations": [
|
||
"case:nobody-implemented-backchannel-logout",
|
||
"case:session-sharing-is-the-database-not-replication",
|
||
"concept:two-stores-two-lookup-keys",
|
||
"setup:reproduce-c2-backchannel-logout",
|
||
"setup:reproduce-b2-jdbc-token-store",
|
||
"setup:reproduce-b7-cookie-secret-rotation"
|
||
],
|
||
"publication": "게시됨",
|
||
"file": "trust-handed-over-at-the-edge/setup/setup-reproduce-c1-multi-app-sso.md",
|
||
"status": "게시 전",
|
||
"studioId": "3421185f-5f3c-4263-9455-6243306e9fc9",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": []
|
||
},
|
||
{
|
||
"title": "IdP 쪽에만 로그아웃 주소를 넣고 한쪽만 고치면 안 퍼지는 것을 확인한다",
|
||
"kind": "setup",
|
||
"slug": "reproduce-c2-backchannel-logout",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#c층-재현-절차-두-편을-직접-치는-순서-c-2"
|
||
],
|
||
"pinned-versions": [
|
||
"curlimages/curl = 8.11.1",
|
||
"keycloak-pattern-bff = lab"
|
||
],
|
||
"classification": "**의도적으로 한쪽만 고치는 절차다** — 후보 셋(보낼 주소 · 받을 엔드포인트 · 네트워크) 중 첫째만 넣고 여전히 안 퍼지는 것을 보이려는 것이고, 「설정했으니 되겠지」로 넘어가는 실패를 **일부러 재현한다.** **주입 검증이 한 겹 더 앞으로 온다** — A·B층에서는 「주입이 걸렸는가」였는데 여기서는 **끊을 세션이 있는가**이고, 원래 실행이 정확히 거기서 헛돌았다. 로그아웃 전 `keycloak-patterns` 세션이 이미 0 이었고 그래서 나온 「앱 세션이 안 지워졌다」는 끊을 것이 없었다는 뜻이었다. 주입은 정상으로 돌았고 출력도 그럴듯했고 결론도 원하던 방향이었는데, 전제 하나가 틀려 있었다. **읽는 사람이 그대로 치는 명령이라 Setup 이다** — `kcadm`·`grep -rn`·`curl`·임시 파드가 약 20분어치 이어진다. **주입 전에 백업이 먼저다** — `attributes=` 는 통째로 교체하므로 기존 속성이 같이 날아갈 수 있고, 되돌리기가 그 백업 파일에 달려 있다. **순서가 결과를 바꾸는 곳이 셋**(observed) — 점 표기 `-s \"attributes.backchannel.logout.url=…\"` 는 `exit 1` 로 죽고(속성 이름 자체에 점이 있다) `kubectl exec` 가 오류 본문을 잘라 「왜」는 안 보이므로 JSON 으로 통째로 줘야 하며, `$CID` 가 빈 채로 다음 명령을 치면 엉뚱한 클라이언트를 고치고, 후보 경로의 `302` 를 「있다」로 읽으면 판정이 뒤집힌다 — `302` 는 핸들러가 없어 인증 요구로 떨어진 것이라 「없다」의 증거다. **판정은** 로그아웃 뒤 `keycloak-patterns` 세션이 0 인데 Redis 키 이름은 개수도 글자도 그대로인 것과, 임시 파드에서 본 `Address: 100.83.212.4` 와 `HTTP 200` 이다. **로그 0줄로는 아무것도 단정하지 않는다** — `DEBUG` 레벨이면 안 찍히는 것과 구별되지 않아 「Keycloak 이 안 보냈다」의 근거로 쓰지 않는다. **증거 한 곳이 1:1 이 아니다**(observed·출처 주의) — 설정이 들어간 것을 확인한 두 줄은 `02-configure-idp.txt` 에 없고 그 파일은 점 표기 실패로 끝나므로, 따라 하는 사람은 그 값을 지금 직접 재 둔다. **버전은 한 줄만 이 편의 것이다** — `curlimages/curl:8.11.1` 은 이 편이 띄운 임시 파드의 출력이고(observed) 나머지는 B층 값이다(inferred). **유효 범위** — 이 실험대의 `HTTP 200` 은 tailnet 과 split DNS 덕에 공개 이름이 되돌아오는 구성 때문이고 운영에서 같은 값이 나온다고 볼 근거가 없다. 받을 엔드포인트를 실제로 구현한 뒤 전파가 되는지, 부분 실패 시의 재시도 정책, `DEBUG` 로그는 재지 않았다. 임시 파드를 띄우는 `kubectl run c2probe …` 한 줄과 설정 JSON 을 파일로 넣는 형태는 가이드에 없다(unknown).",
|
||
"relations": [
|
||
"case:nobody-implemented-backchannel-logout",
|
||
"case:orphan-sessions-and-the-ttl-that-finds-them",
|
||
"reference:verify-the-injection-landed-separately-from-the-result",
|
||
"setup:reproduce-c1-multi-app-sso",
|
||
"setup:reproduce-b7-cookie-secret-rotation"
|
||
],
|
||
"publication": "게시됨",
|
||
"file": "trust-handed-over-at-the-edge/setup/setup-reproduce-c2-backchannel-logout.md",
|
||
"status": "게시 전",
|
||
"studioId": "5296a106-4c42-437d-b721-33a5e53a045c",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": []
|
||
}
|
||
]
|
||
},
|
||
"topic": "trust-handed-over-at-the-edge"
|
||
},
|
||
"operations-that-report-success": {
|
||
"title": "운영 절차의 완료 판정 — 백업 · 판올림 · Secret · 인증서 갱신",
|
||
"readerQuestion": "운영 명령이 끝났다는 것을 무엇으로 확인하는가?",
|
||
"kinds": {
|
||
"case": [
|
||
{
|
||
"title": "새 인증서가 디스크에 있고 38분 25초 동안 옛 인증서가 나갔다",
|
||
"slug": "the-certificate-that-took-38-minutes-to-reach-the-wire",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-d4"
|
||
],
|
||
"code": [
|
||
"certbot-renew.service ExecStartPost",
|
||
"/etc/letsencrypt/renewal-hooks/",
|
||
"nginx 마스터·워커 PID"
|
||
],
|
||
"classification": "디스크 기록 시각과 실제 서빙 시각을 따로 재어 공백을 확정하고 원인 셋이 모두 비어 있는 것을 확인했으며, 88 일 잠복한다는 성질까지 닫았다",
|
||
"missing-verification": "실제 만료가 임박한 상태를 만들지 않았다. 이 결함이 만료로 드러나는 경로는 재지 않았다",
|
||
"relations": [
|
||
"case:a-deploy-hook-closed-the-gap-to-two-seconds",
|
||
"decision:put-the-reload-in-a-deploy-hook",
|
||
"question:does-the-renewal-timer-actually-renew"
|
||
],
|
||
"kind": "case",
|
||
"ssot-assets": [
|
||
"renewal-to-serving-gap"
|
||
],
|
||
"ssot-evidence": [
|
||
"raw/d4-certificate-renewal__07-renewal-hook-missing.txt",
|
||
"raw/d4-certificate-renewal__09-serial-timeline.txt",
|
||
"raw/d4-certificate-renewal__12-certbot-state.txt",
|
||
"raw/d4-certificate-renewal__13-verdict.txt"
|
||
],
|
||
"publication": "초안",
|
||
"file": "operations-that-report-success/case/case-the-certificate-that-took-38-minutes-to-reach-the-wire.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [
|
||
"renewal-to-serving-gap"
|
||
],
|
||
"assetFiles": [
|
||
"renewal-to-serving-gap"
|
||
],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/d4-certificate-renewal__07-renewal-hook-missing.txt",
|
||
"../../../final/evidence/raw/d4-certificate-renewal__09-serial-timeline.txt",
|
||
"../../../final/evidence/raw/d4-certificate-renewal__12-certbot-state.txt",
|
||
"../../../final/evidence/raw/d4-certificate-renewal__13-verdict.txt"
|
||
]
|
||
},
|
||
{
|
||
"title": "deploy 훅 하나가 그 공백을 1~2초로 줄였다",
|
||
"slug": "a-deploy-hook-closed-the-gap-to-two-seconds",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-d4a"
|
||
],
|
||
"code": [
|
||
"renewal-hooks/deploy/",
|
||
"types_hash 경고"
|
||
],
|
||
"classification": "처방을 실제로 넣고 전후를 같은 방법으로 재어 확인했고, 훅 로그의 error 문구가 실패가 아니라는 것까지 갈라 닫았다",
|
||
"missing-verification": "실제 갱신 주기에서 훅이 도는 것은 확인하지 않았다. 강제 갱신으로만 검증했다",
|
||
"relations": [
|
||
"case:the-certificate-that-took-38-minutes-to-reach-the-wire",
|
||
"decision:put-the-reload-in-a-deploy-hook",
|
||
"reference:judge-a-reload-by-the-worker-pid-not-the-log"
|
||
],
|
||
"kind": "case",
|
||
"ssot-assets": [
|
||
"d4a-hook-effect"
|
||
],
|
||
"ssot-evidence": [
|
||
"raw/d4a-deploy-hook__01-hook-verified.txt",
|
||
"raw/d4a-deploy-hook__02-certbot-with-hook.txt",
|
||
"raw/d4a-deploy-hook__03-after-state.txt"
|
||
],
|
||
"publication": "초안",
|
||
"file": "operations-that-report-success/case/case-a-deploy-hook-closed-the-gap-to-two-seconds.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [
|
||
"d4a-hook-effect"
|
||
],
|
||
"assetFiles": [
|
||
"d4a-hook-effect"
|
||
],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/d4a-deploy-hook__01-hook-verified.txt",
|
||
"../../../final/evidence/raw/d4a-deploy-hook__02-certbot-with-hook.txt",
|
||
"../../../final/evidence/raw/d4a-deploy-hook__03-after-state.txt"
|
||
]
|
||
},
|
||
{
|
||
"title": "되돌리기를 막은 것은 체크섬이었고 그 판정은 조건부였다",
|
||
"slug": "the-upgrade-that-would-not-roll-back",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-d1-d2"
|
||
],
|
||
"code": [
|
||
"liquibase ValidationFailedException",
|
||
"databasechangelog",
|
||
"StatefulSet 롤링 업데이트"
|
||
],
|
||
"classification": "롤백 실패를 관측하고 원인을 체크섬으로 확정한 뒤, 스키마가 움직이지 않은 방향은 실제로 성공하는 것까지 확인해 조건을 붙여 닫았다",
|
||
"missing-verification": "스키마가 크게 바뀌는 메이저 업그레이드에서는 재지 않았다. 두 패치 버전 사이만 확인했다",
|
||
"relations": [
|
||
"reference:judge-a-reload-by-the-worker-pid-not-the-log",
|
||
"decision:put-the-reload-in-a-deploy-hook"
|
||
],
|
||
"kind": "case",
|
||
"ssot-assets": [
|
||
"d2-upgrade-direction"
|
||
],
|
||
"ssot-evidence": [
|
||
"raw/d2-version-upgrade__02-rollback-attempt.txt",
|
||
"raw/d2-version-upgrade__03-roll-forward.txt"
|
||
],
|
||
"publication": "초안",
|
||
"file": "operations-that-report-success/case/case-the-upgrade-that-would-not-roll-back.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [
|
||
"d2-upgrade-direction"
|
||
],
|
||
"assetFiles": [
|
||
"d2-upgrade-direction"
|
||
],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/d2-version-upgrade__02-rollback-attempt.txt",
|
||
"../../../final/evidence/raw/d2-version-upgrade__03-roll-forward.txt"
|
||
]
|
||
}
|
||
],
|
||
"concept": [],
|
||
"reference": [
|
||
{
|
||
"title": "적용됐는지는 로그 문구가 아니라 상태로 판정한다",
|
||
"slug": "judge-a-reload-by-the-worker-pid-not-the-log",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-d4",
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-d4a"
|
||
],
|
||
"classification": "D-4 안에서 「로그는 성공인데 상태는 안 바뀌었다」를 두 번 만났다 — certbot 타이머가 매번 `SUCCESS` 인데 옛 인증서가 나갔고, nginx 워커 PID 는 기동 직후의 첫 fork 그대로였다. D-4a 는 **극성이 반대**다(훅 로그는 `error` 를 찍는데 상태는 바뀌었다). 그래서 규칙은 「로그가 틀린다」가 아니라 「로그는 양쪽으로 다 틀리니 상태로 판정하라」다. 검증한 데몬은 nginx 하나뿐이다",
|
||
"scope": "설정이나 인증서를 다시 읽게 하는 절차. reload · rotate · reconcile",
|
||
"exceptions": "절차가 프로세스를 완전히 교체하면 PID 비교가 판정이 되지 않는다. 그때는 적재한 값 자체를 확인한다",
|
||
"relations": [
|
||
"case:a-deploy-hook-closed-the-gap-to-two-seconds",
|
||
"case:the-upgrade-that-would-not-roll-back"
|
||
],
|
||
"kind": "reference",
|
||
"publication": "초안",
|
||
"file": "operations-that-report-success/reference/reference-judge-a-reload-by-the-worker-pid-not-the-log.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/d4-certificate-renewal__07-renewal-hook-missing.txt",
|
||
"../../../final/evidence/raw/d4-certificate-renewal__09-serial-timeline.txt",
|
||
"../../../final/evidence/raw/d4-certificate-renewal__13-verdict.txt",
|
||
"../../../final/evidence/raw/d4a-deploy-hook__01-hook-verified.txt",
|
||
"../../../final/evidence/raw/d4a-deploy-hook__02-certbot-with-hook.txt",
|
||
"../../../final/evidence/raw/d4a-deploy-hook__03-after-state.txt"
|
||
]
|
||
}
|
||
],
|
||
"question": [
|
||
{
|
||
"title": "갱신 타이머가 실제 갱신에서도 도는가",
|
||
"slug": "does-the-renewal-timer-actually-renew",
|
||
"readiness": "OPEN",
|
||
"source": [
|
||
"final/document.md#얻은-것-잃은-것-적용하지-않을-때-재보지-않은-것",
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-d4a"
|
||
],
|
||
"known": "강제 갱신에서는 deploy 훅이 1~2 초 만에 reload 를 걸었다. 타이머 자체는 매번 SUCCESS 로 끝나고 있다",
|
||
"unknown": "만료 30 일 전 조건이 성립해 타이머가 실제 갱신을 수행할 때도 같은 훅이 도는가",
|
||
"next-verification": "만료 30 일 전(약 59 일 뒤 (증거의 `VALID: 89 days` 는 만료까지다))에 타이머가 돈 뒤 워커 PID 와 서빙 인증서의 일련번호를 확인한다",
|
||
"decision-criterion": "워커 PID 가 바뀌고 서빙 일련번호가 새 인증서와 같으면 닫는다. 그렇지 않으면 훅이 강제 갱신에서만 도는 것이므로 타이머 유닛 쪽에 훅을 다시 건다",
|
||
"relations": [
|
||
"case:the-certificate-that-took-38-minutes-to-reach-the-wire",
|
||
"case:a-deploy-hook-closed-the-gap-to-two-seconds"
|
||
],
|
||
"kind": "question",
|
||
"publication": "초안",
|
||
"file": "operations-that-report-success/question/question-does-the-renewal-timer-actually-renew.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/d4-certificate-renewal__07-renewal-hook-missing.txt",
|
||
"../../../final/evidence/raw/d4-certificate-renewal__12-certbot-state.txt",
|
||
"../../../final/evidence/raw/d4-certificate-renewal__13-verdict.txt",
|
||
"../../../final/evidence/raw/d4a-deploy-hook__01-hook-verified.txt",
|
||
"../../../final/evidence/raw/d4a-deploy-hook__03-after-state.txt"
|
||
]
|
||
}
|
||
],
|
||
"decision": [
|
||
{
|
||
"title": "reload 를 사람이 아니라 deploy 훅이 부르게 한다",
|
||
"slug": "put-the-reload-in-a-deploy-hook",
|
||
"readiness": "READY",
|
||
"decision-status": "ADOPTED",
|
||
"source": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-d4a"
|
||
],
|
||
"decision-evidence": [
|
||
"case:the-certificate-that-took-38-minutes-to-reach-the-wire",
|
||
"case:a-deploy-hook-closed-the-gap-to-two-seconds"
|
||
],
|
||
"grounds": "훅이 없을 때 갱신에서 서빙까지 2305 초가 걸렸고 그 reload 를 부른 것은 자동화가 아니라 사람이었다. 훅을 넣자 1~2 초가 됐다",
|
||
"classification": "사람이 치는 절차로 두는 대안을 실제 공백(2305초)으로 확인한 뒤 훅으로 정했다. 훅 로그에 `error` 가 찍히는 것은 **훅을 고른 비용이 아니다** — `types_hash` 경고는 nginx 설정 자체의 것이라 사람이 reload 해도 똑같이 뜬다. 비용으로 적을 것은 「성공한 훅을 로그로 감시하면 실패로 오독한다」쪽이다",
|
||
"relations": [
|
||
"case:a-deploy-hook-closed-the-gap-to-two-seconds",
|
||
"reference:judge-a-reload-by-the-worker-pid-not-the-log",
|
||
"question:does-the-renewal-timer-actually-renew"
|
||
],
|
||
"kind": "decision",
|
||
"publication": "초안",
|
||
"file": "operations-that-report-success/decision/decision-put-the-reload-in-a-deploy-hook.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/d4-certificate-renewal__13-verdict.txt",
|
||
"../../../final/evidence/raw/d4a-deploy-hook__01-hook-verified.txt",
|
||
"../../../final/evidence/raw/d4a-deploy-hook__02-certbot-with-hook.txt",
|
||
"../../../final/evidence/raw/d4a-deploy-hook__03-after-state.txt"
|
||
]
|
||
}
|
||
],
|
||
"setup": [
|
||
{
|
||
"title": "스키마를 통째로 지우고 덤프 하나로 되살아나는지 본다",
|
||
"kind": "setup",
|
||
"slug": "reproduce-d1-backup-restore",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서-d-1",
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서"
|
||
],
|
||
"pinned-versions": [
|
||
"Keycloak = 26.7.0"
|
||
],
|
||
"classification": "**다섯 편의 첫 편이고 D-2 가 이 절차가 남긴 덤프 위에 선다** — D-2 의 전제가 「D-1 이 끝나 있고 덤프가 손에 있다」이고, 그래서 복구 확인표가 덤프를 지우지 말라고 적는다. **되돌리는 수단이 방금 뜬 덤프 파일 하나뿐이라 검증을 먼저 한다** — `DROP SCHEMA public CASCADE` 는 realm·client·user·세션을 전부 지우고, 가이드는 덤프 검증 넷(크기·`dump complete`·테이블 수 `101`·`COPY` 블록에 붙은 세션 행)을 통과하기 전에는 주입 절로 넘어가지 않는다. **읽는 사람이 그대로 치는 명령이라 Setup 이다** — `pg_dump`·`psql`·`curl`·`kubectl` 이 약 20분어치 이어지고 파괴 구간만 1분 안쪽이다. **이 편은 스크립트를 일부러 안 쓴다** — `DROP SCHEMA` 와 복구가 한 파일에 있으면 중간에 멈췄을 때 무엇이 실행됐는지 모르므로, 파괴를 손으로 치고 눈으로 확인하고 복구도 손으로 친다. **복구가 조용히 실패하는 곳이 하나 있고 그것이 이 편의 함정이다**(observed) — `kubectl exec` 에 `-i` 를 빼면 파드 안의 `psql` 이 빈 입력을 받고 정상 종료하는데 오류도 종료 코드도 안 나고 시각 두 줄은 「1초 만에 끝났다」로 찍혀 **복구한 것과 구별되지 않는다.** **순서가 결과를 바꾸는 곳이 둘** — 파괴 전에 같은 명령으로 대조값을 안 잡으면 「완전 일치」를 판정할 상대가 없고, 개수 쿼리에는 `offline_flag='0'` 필터가 있어 나열 쿼리와 수가 다르므로 **같은 쿼리끼리** 견준다. **판정은** 파괴 뒤 남은 테이블 `0` 과 `relation \"realm\" does not exist`, 그런데도 정문과 app1 이 `HTTP 200` 인 것, 세 경로가 `certs` 200 · `.well-known` 500 · 토큰 발급 400 으로 갈리는 것, 복구 뒤 대조 한 줄이 문자 단위로 같은 것과 `RTO = 41초` 다. **정문의 200 을 「파괴가 실패했다」로 읽지 않는다** — 테이블이 0개인 것을 바로 앞에서 봤고, realm 캐시가 DB 와 대조하지 않고 답한다. 헬스체크는 커넥션만 보므로 빈 데이터베이스를 통과시키고, 이 사고에서 정직한 지표는 토큰 발급 하나다. **버전은 이 편이 직접 잰 값이 아니다**(inferred) — D-1 절에는 판 번호가 한 줄도 없고, 바로 이어 돈 D-2 가 시작 태그를 `quay.io/keycloak/keycloak:26.7.0` 으로 적는다. **유효 범위** — 백업 자동화·보존 주기·복구 리허설의 정기 실행은 확인하지 않았고 손으로 한 번 뜨고 한 번 되돌린 것만 참이다. **덤프를 다른 기계로 옮기는 두 줄은 이 실험대가 치지 않았다** — 덤프는 DB 와 같은 기계에 남았다(unknown). 남은 테이블을 세는 `pg_tables` 쿼리와 토큰 발급 `curl` 한 줄도 미검증이다.",
|
||
"relations": [
|
||
"case:the-upgrade-that-would-not-roll-back",
|
||
"case:nine-injections-that-silently-did-nothing",
|
||
"concept:the-up-metric-cannot-see-alive-but-useless",
|
||
"setup:reproduce-d2-version-upgrade",
|
||
"setup:reproduce-a2-database-loss",
|
||
"setup:reproduce-a3-database-crash"
|
||
],
|
||
"publication": "게시됨",
|
||
"file": "operations-that-report-success/setup/setup-reproduce-d1-backup-restore.md",
|
||
"status": "게시 전",
|
||
"studioId": "7dc48b91-e31b-455c-9a9d-c766f95ff491",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": []
|
||
},
|
||
{
|
||
"title": "이미지 태그를 올렸다 내리며 롤백이 언제 막히는지 가른다",
|
||
"kind": "setup",
|
||
"slug": "reproduce-d2-version-upgrade",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서-d-2"
|
||
],
|
||
"pinned-versions": [
|
||
"Keycloak (시작·복귀 태그) = 26.7.0",
|
||
"Keycloak (정방향) = 26.7.3",
|
||
"Keycloak (역방향 대조) = 26.0",
|
||
"Infinispan (26.7.3 에 실린 판) = 16.0.14"
|
||
],
|
||
"classification": "**태그를 세 번 바꾸고 마지막 것이 파드를 `CrashLoopBackOff` 로 만드는 절차다**(정방향 → 롤백 → 선택적으로 실패하는 방향). 전 구간 약 20분이고, **이 실험은 백업 없이 시작하지 않는다** — 스키마가 움직이는 방향으로 가면 태그로는 못 돌아온다. **주입 전에 안 재면 다시 못 재는 값이 하나 있다** — 업그레이드 전의 `databasechangelog` 행 수이고, 올린 뒤에는 「롤백해도 되는가」를 판정할 근거가 사라진다. **판정 기준이 버전 번호가 아니라 행 수의 변화다** — 26.7.0 ↔ 26.7.3 은 `210 → 210` 으로 롤백이 **되고**, 26.7.0 → 26.0 은 체크섬 불일치로 **안 된다.** **읽는 사람이 그대로 치는 명령이라 Setup 이다** — `set image`·`rollout status`·`psql`·폴링 루프가 이어지고 터미널 둘을 여는 편이 낫다. **실측이 두 실행에서 나온다**(observed) — 처음 실행(15:00–15:10, 역방향 26.0)과 후속 실행(15:22–15:26, 26.7.3 정방향과 롤백)이며 어느 쪽인지 매번 적는다. **해설이 한 번 정정됐다** — 처음에는 「롤백은 안 된다」고 단정했다가 후속 실험에서 조건을 붙였다. **순서가 결과를 바꾸는 곳이 셋**(observed) — 레지스트리 태그 목록을 안 보면 「26.7.0 보다 새 이미지가 없다」고 적게 되고(실제로는 셋이 있었다), 가용성 대조군을 먼저 안 띄우면 주입 중의 `000` 을 귀속할 수 없으며, `Running` 인데 `0/1` 인 상태를 「떴다」로 읽으면 실패를 못 본다. **판정은** 업그레이드 후 `마이그레이션 후: 210 (전: 210)`, 정방향 폴링 `200 87회 / 비200 0`, 롤백 폴링 `43회 / 비200 1` 과 그 1이 `000` 인 것, 그리고 실패한 기동 뒤에도 `210` 인 것을 함께 본다. **`000` 은 서버 오류가 아니다** — `--max-time 3` 을 넘긴 것이고, 끊긴 것과 느린 것의 구별이 그 옵션을 알고 있어야 선다. **실패한 기동이 스키마를 못 건드린 것이 두 경우를 가른다** — Liquibase 가 검증 단계에서 멈추면 이미지만 되돌리면 되고, 이미 적용한 뒤였으면 D-1 의 덤프 복구까지 가야 한다. **유효 범위** — 「행 수가 늘면 태그로 못 돌아온다」는 역방향(26.0)에서 관측한 실패를 근거로 한 추론이며(inferred) 실제로 행 수가 늘어난 뒤 되돌려 본 적은 없다. 26.7.x 사이에는 스키마 변경이 없어 이 실험대에서는 재현하지 못했고, 마이그레이션 도중에 죽는 경우와 대규모 마이그레이션 소요 시간도 재지 않았다. `kubectl rollout undo` 와 `databasechangelog` 마지막 다섯 줄을 뽑는 쿼리는 이 실험이 쓰지 않았다(unknown).",
|
||
"relations": [
|
||
"case:the-upgrade-that-would-not-roll-back",
|
||
"case:rolling-restart-keeps-sessions-drops-cache",
|
||
"concept:readiness-hides-the-broken-node",
|
||
"setup:reproduce-d1-backup-restore",
|
||
"setup:reproduce-a8-rolling-restart"
|
||
],
|
||
"publication": "게시됨",
|
||
"file": "operations-that-report-success/setup/setup-reproduce-d2-version-upgrade.md",
|
||
"status": "게시 전",
|
||
"studioId": "e53c5947-e1df-400a-ad79-e9d55b1da452",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": []
|
||
},
|
||
{
|
||
"title": "카나리아 Secret 을 심고 네 경로에서 평문이 어디까지 나오는지 본다",
|
||
"kind": "setup",
|
||
"slug": "reproduce-d3-secret-exposure",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서-d-3"
|
||
],
|
||
"pinned-versions": [
|
||
"k3s 저장소 암호화 = Disabled",
|
||
"판 번호 = SSOT D-3 절에 한 줄도 없다"
|
||
],
|
||
"classification": "**파괴적인 단계가 없는 유일한 편이다** — 만드는 것은 카나리아 Secret 하나뿐이고 복구 절에서 지운다. 전 구간 약 15분. **카나리아를 쓰는 까닭이 절차의 일부다** — 관찰 절에서 저장 파일 안을 `grep` 해야 하는데 진짜 비밀번호를 인자로 쓰면 그 값이 셸 히스토리와 `ps` 에 남으므로, 찾아도 피해가 없는 값을 하나 심는다. 실험 대상이 값이 아니라 **경로**라 결론은 같다. **읽는 사람이 그대로 치는 명령이라 Setup 이다** — `kubectl`·`sudo k3s`·`sudo grep`·`auth can-i` 가 네 경로를 하나씩 열고, 게스트의 `sudo` 는 무암호라 호스트와 다르다. **비밀을 화면에 띄우는 편이라 규칙 셋을 먼저 정한다** — 남의 진짜 비밀은 길이(`wc -c`)와 키 이름까지만 보고, 값을 찍어야 하는 곳은 카나리아를 쓰며, 실린 값들은 이름에 `change-me` 가 들어간 실험대 전용 문자열이다. **순서가 결과를 바꾸는 곳이 둘**(observed) — 카나리아를 심기 전에 `describe` 화면을 안 봐 두면 심은 뒤의 화면이 **같은 화면**이라는 것이 안 보이고, `-wal` 이 10MB 인 것을 안 봐 두면 방금 만든 값이 아직 본체에 없을 수 있다는 것을 놓친다. **판정은** `describe` 가 `CANARY: 25 bytes` 만 주는데 같은 값이 `base64 -d` 한 줄로 평문이 되는 것, `Encryption Status: Disabled, no configuration file found`, 저장 파일에서 평문 일치 **`2`**, 파드 안 `env` 두 줄, 그리고 `default SA: no` 다. **`0` 을 「없다」로 읽지 않는다** — 같은 파일에 같은 명령을 걸었는데 키에 따라 `2` 와 `0` 이 나왔고, `0` 은 「이 파일의 이 시점에 이 형태로는 못 찾았다」이다. `2` 가 나온 순간 판정은 이미 났다. **`grep -c` 를 쓰는 것도 이 편의 규칙이다** — 바이너리에서도 숫자가 나오고 값 자체를 화면에 안 띄운다. **판 번호가 이 절에 한 줄도 없다** — SSOT D층 머리말에는 B층 같은 버전 표가 없고, 같은 시각에 D-2 가 태그를 바꾸고 있었으므로 다른 편의 값을 끌어오지 않았다. 적어 둔 것은 이 절차가 성립한 구성인 「저장소 암호화 꺼짐」 하나다. **유효 범위** — k3s `--secrets-encryption` 을 켠 뒤의 상태는 시험하지 않았고, `0` 이 나온 키의 원인도 가리지 않았으며, 볼륨 마운트·SealedSecret·외부 KMS 도 전부 안 했다. 카나리아로 저장 파일을 찾는 두 줄, `-wal`·`strings` 로 다시 보는 두 줄, `exec deploy/bff` 형태, `/proc/1/environ` 을 읽는 줄, `auth can-i --list`, 삭제 뒤 다시 `grep` 하는 두 줄은 전부 미검증이고 **이 실험대는 카나리아 대신 실제 값으로 쟀다**(unknown).",
|
||
"relations": [
|
||
"case:persistence-without-a-volume-and-rotation-without-overlap",
|
||
"concept:the-up-metric-cannot-see-alive-but-useless",
|
||
"setup:reproduce-d1-backup-restore",
|
||
"setup:reproduce-b6-key-rotation",
|
||
"setup:reproduce-b7-cookie-secret-rotation"
|
||
],
|
||
"publication": "게시됨",
|
||
"file": "operations-that-report-success/setup/setup-reproduce-d3-secret-exposure.md",
|
||
"status": "게시 전",
|
||
"studioId": "186443e8-a32a-4a94-8609-845a4247d120",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": []
|
||
},
|
||
{
|
||
"title": "인증서를 강제로 갱신하고 밖에서 보이는 일련번호가 언제 바뀌는지 잰다",
|
||
"kind": "setup",
|
||
"slug": "reproduce-d4-certificate-renewal",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서-d-4"
|
||
],
|
||
"pinned-versions": [
|
||
"certbot = 5.7.0"
|
||
],
|
||
"classification": "**클러스터가 아니라 호스트를 보는 유일한 편이다** — `kubectl` 은 한 번도 안 쓰고, 관찰은 개발 머신에서 한다. 밖에서 본 것이 이 실험의 답이고 **그 기계의 시계가 이 실험대에서 유일하게 정확하다.** **되돌릴 수 없는 한 줄이 있다** — `certbot renew --force-renewal` 은 진짜 인증서를 발급하고 Let's Encrypt 의 주당 중복 인증서 5장 한도를 한 장 깎으므로, 먼저 `--dry-run` 으로 절차만 확인하고 강제 갱신은 전체에서 한 번만 쓰며 그 한 번을 헛되게 쓰지 않도록 **대조군을 먼저 잡는다.** **읽는 사람이 그대로 치는 명령이라 Setup 이다** — 주입 전에 재는 칸만 여덟이고 `openssl`·`systemctl`·`ps`·감시 스크립트 셋이 이어진다. **호스트에서 비대화 `sudo` 는 반드시 실패한다**(observed) — `sudo -n -l` 이 `a password is required` 를 내므로 네 단계는 자동화할 수 없고 `ssh -t` 로 사람이 비밀번호를 친다. 그래서 이 실험은 처음에 강제 갱신을 못 하고 그 항목을 미측정으로 남겼다. **빈 출력을 「비었다」로 읽지 않는다** — 훅 디렉터리는 root 전용이라 `sudo` 없이 보면 `Permission denied` 이고, B-7 에서 같은 실수를 한 적이 있다. **판정 기준을 주입 전에 세운다** — 「reload 됐는가」는 로그 문구가 아니라 **nginx 워커 PID** 로 판정하고, 마스터가 유지되고 워커만 바뀌면 reload, 둘 다 바뀌면 재시작이다. 주입 전 워커는 `586` 이고 `lstart` 가 22.4시간 전이라 그동안 reload 가 한 번도 없었다. **시계를 주입 전에 잰다** — 두 기계가 **106초** 어긋나 있고 주입 후에는 그때의 왜곡을 되짚을 수 없다. 이 실험은 그것을 나중에 하는 바람에 공백을 `2199초` 로 적었다가 `2305초` 로 정정했다. **판정은** 갱신 성공(`Congratulations, all renewals succeeded`)과 디스크의 새 일련번호 `6c7cb6df…` 옆에서 밖의 일련번호가 161표본 동안 `0520BB…` 그대로인 것, 워커 PID 가 안 바뀐 것, 그리고 경로 셋(`ExecStartPost` · `renewal-hooks/` 셋 · nginx 플러그인)이 전부 비어 있는 것을 함께 본다. **`notBefore` 로 발급 시각을 계산하지 않는다** — Let's Encrypt 는 정확히 한 시간 백데이트하므로 기준은 SCT 다. **대조군이 오보를 막았다**(observed) — in-flight 감시의 실패 76건은 같은 순간 폴링 49건이 전부 200 이고 연결수 0, 소요 50µs, 재현 0/100 이라 서버 탓이 아니었다. **유효 범위** — 이 편이 잰 reload 는 **사람이 건 것이다.** 훅이 거는 자동 reload 는 D-4a 에서 따로 쟀다. 훅이 진짜로 실패했을 때 certbot 이 무엇을 찍는지, in-flight 아티팩트 76건의 원인, 타이머가 스스로 갱신하는 경로(만료 30일 전에야 조건이 성립한다)는 재지 않았다. `--dry-run` 의 `Running deploy-hook command` 줄, `-checkend 2592000` 감시 한 줄, `systemctl reload nginx` 형태는 미검증이다(unknown).",
|
||
"relations": [
|
||
"case:the-certificate-that-took-38-minutes-to-reach-the-wire",
|
||
"case:seventy-six-failures-that-were-not-the-servers",
|
||
"reference:judge-a-reload-by-the-worker-pid-not-the-log",
|
||
"reference:never-subtract-values-from-two-clocks",
|
||
"question:does-the-renewal-timer-actually-renew",
|
||
"setup:reproduce-d4a-deploy-hook"
|
||
],
|
||
"publication": "게시됨",
|
||
"file": "operations-that-report-success/setup/setup-reproduce-d4-certificate-renewal.md",
|
||
"status": "게시 전",
|
||
"studioId": "9349a3fe-5234-48ae-af9f-029ffc0d2296",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": []
|
||
},
|
||
{
|
||
"title": "deploy 훅 파일 하나를 넣고 nginx 워커가 저절로 갈리는지 확인한다",
|
||
"kind": "setup",
|
||
"slug": "reproduce-d4a-deploy-hook",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서-d-4a"
|
||
],
|
||
"pinned-versions": [
|
||
"certbot = 5.7.0"
|
||
],
|
||
"classification": "**D-4 가 적어 두고 검증하지 않은 처방을 실제로 넣는 절차라 따로 선다** — D-4 는 「deploy 훅을 넣으면 자동 반영되는가」를 **미측정**으로 남겼고, 처방이 듣는지 모르는 채 「이렇게 고치면 된다」고 쓰지 않으려고 편을 나눴다. **D-4 가 끝나 있어야 성립한다** — 「reload 판정은 워커 PID 로 한다」는 기준과 두 기계 시계의 왜곡을 **미리** 재 둔 값, 둘이 없으면 이 편은 못 읽는다. 출발점의 워커 `28829` 자체가 D-4 에서 사람이 친 `nginx -s reload` 의 결과이고, 이 편이 재는 것은 **훅이 거는 reload** 다. **인증서를 한 장 더 쓴다** — `--force-renewal` 을 또 한 번 치므로 주당 한도를 두 장 쓴 셈이 되고, 절차만 확인하려면 `--dry-run` 을 먼저 쓴다. **되돌리기는 한 줄인데 되돌리지 않는 편이 낫다** — 훅은 결함을 고치는 파일이라 지우면 D-4 의 상태로 돌아가고, 그 결함은 약 59일 뒤 (증거의 `VALID: 89 days` 는 만료까지다) 인증서 만료로 나타난다. **읽는 사람이 그대로 치는 명령이라 Setup 이다** — 바꾸는 것은 파일 하나 두 줄인데 `ssh -t`·`sudo install`·`certbot`·`ps`·`openssl` 이 이어지고 주입은 전부 호스트 쪽이라 사람이 비밀번호를 친다. **`deploy/` 여야 한다** — `post/` 는 갱신이 없는 날에도 하루 두 번 nginx 를 reload 하고, `deploy/` 는 certbot 이 `RENEWED_LINEAGE` 를 넘길 때만 돈다. **`nginx -t &&` 를 앞에 두는 것도 절차의 일부다** — 설정이 깨진 채 reload 를 보내면 마스터가 새 워커를 못 띄우고, `restart` 를 걸면 `Restart=on-failure` · `StartLimitBurst=5` · `StartLimitIntervalUSec=10s` 때문에 10초 안에 5번 실패하고 systemd 가 포기한다. **판정을 문구로 하지 않는다** — certbot 이 찍는 `Hook 'deploy-hook' ran with error output:` 은 훅이 stderr 에 무엇이라도 쓰면 붙는 문구이고 종료 코드를 말하지 않는다. 여기서 stderr 로 나간 것은 nginx 의 `types_hash` 경고 하나이고 내용은 전부 성공이다. **판정은** 마스터 `585` 가 그대로고 워커가 `28829 → 37252` 로 바뀐 것, `etimes 74` 와 `lstart` 가 방금을 가리키는 것, 새 일련번호 `06F3E0EF…` 와 SAN 세 이름이다. `etimes` 를 같이 보는 까닭은 PID 가 재사용될 수 있기 때문이다. **시계 보정이 결과를 정한다** — 106초를 빼면 SCT `12:27:49.05` 뒤 1~2초에 훅과 새 워커가 놓이고, 보정하지 않고 그냥 빼면 107초가 나와 참값보다 약 106초 어긋나고, 보정을 반대로 걸면 음수 지연이 나와 성립하지 않는다. 독립 시계인 SCT 가 보정을 자기 검증한다. **`notBefore` 는 발급 시각이 아니다** — Let's Encrypt 가 정확히 한 시간 백데이트하고, 한 시간을 더한 값도 SCT 보다 일관되게 약 89초 늦다. **유효 범위** — 타이머가 스스로 갱신하는 경로는 만료 30일 전(약 59일 뒤 (증거의 `VALID: 89 days` 는 만료까지다))에야 조건이 성립해 확인하지 않았고, 그때 볼 두 줄만 적어 두었다. 훅이 진짜로 실패했을 때의 출력, `/tmp` 에 남긴 훅 로그가 `PrivateTmp=true` 때문에 안 보이는지, `notBefore`+1시간과 SCT 사이 약 89초 차이의 원인은 재지 않았다. 훅 파일을 편집기로 만드는 형태와 설치·갱신을 한 줄씩 치는 형태도 이 형태로는 실행하지 않았다 — **이 실험대는 `printf … > /tmp/reload-nginx.sh` 로 만들고 설치와 강제 갱신을 한 줄로 쳤다**(observed·unknown).",
|
||
"relations": [
|
||
"case:a-deploy-hook-closed-the-gap-to-two-seconds",
|
||
"case:the-certificate-that-took-38-minutes-to-reach-the-wire",
|
||
"decision:put-the-reload-in-a-deploy-hook",
|
||
"reference:judge-a-reload-by-the-worker-pid-not-the-log",
|
||
"reference:never-subtract-values-from-two-clocks",
|
||
"question:does-the-renewal-timer-actually-renew",
|
||
"setup:reproduce-d4-certificate-renewal"
|
||
],
|
||
"publication": "게시됨",
|
||
"file": "operations-that-report-success/setup/setup-reproduce-d4a-deploy-hook.md",
|
||
"status": "게시 전",
|
||
"studioId": "4f32b469-185a-4dea-8eba-599864a3b476",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": []
|
||
}
|
||
]
|
||
},
|
||
"topic": "operations-that-report-success"
|
||
},
|
||
"when-the-measurement-lies": {
|
||
"title": "주입이 걸렸는지 무엇으로 아는가",
|
||
"readerQuestion": "주입이 안 걸린 것과 영향이 없는 것을 화면에서 어떻게 가르는가?",
|
||
"kinds": {
|
||
"case": [
|
||
{
|
||
"title": "주입이 아홉 번 조용히 실패했고 전부 아무 일도 없는 것처럼 보였다",
|
||
"slug": "nine-injections-that-silently-did-nothing",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#문제를-어렵게-만든-제약-주입이-먹지-않는다"
|
||
],
|
||
"code": [
|
||
"conntrack",
|
||
"kube-router FORWARD",
|
||
"PID 1 SIGKILL",
|
||
"flannel VXLAN"
|
||
],
|
||
"classification": "아홉 건이 각각 다른 이유로 실패한 것을 하나씩 확정하고, 실패한 주입과 영향 없는 주입이 구별되지 않는다는 공통 원인으로 닫았다",
|
||
"missing-verification": "아홉 건 모두를 고친 뒤 다시 돌린 것은 아니다. 일부는 다른 주입 방법으로 우회했다",
|
||
"relations": [
|
||
"reference:verify-the-injection-landed-separately-from-the-result",
|
||
"case:seventy-six-failures-that-were-not-the-servers",
|
||
"concept:the-up-metric-cannot-see-alive-but-useless"
|
||
],
|
||
"kind": "case",
|
||
"ssot-assets": [
|
||
"injection-verification",
|
||
"a5-partition-asymmetry"
|
||
],
|
||
"ssot-evidence": [
|
||
"raw/a1-jgroups-transport-block__05-conntrack-problem.txt",
|
||
"raw/a6-latency-injection__03-flannel-injection.txt"
|
||
],
|
||
"publication": "초안",
|
||
"file": "when-the-measurement-lies/case/case-nine-injections-that-silently-did-nothing.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [
|
||
"injection-verification",
|
||
"a5-partition-asymmetry"
|
||
],
|
||
"assetFiles": [
|
||
"injection-verification",
|
||
"a5-partition-asymmetry"
|
||
],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/a1-jgroups-transport-block__05-conntrack-problem.txt",
|
||
"../../../final/evidence/raw/a6-latency-injection__03-flannel-injection.txt"
|
||
]
|
||
},
|
||
{
|
||
"title": "실패 76 건이 서버 탓이 아니었다 — 대조군이 오보를 막았다",
|
||
"slug": "seventy-six-failures-that-were-not-the-servers",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#결정이-지켜지는지-확인하는-방법-대조군-없이는"
|
||
],
|
||
"code": [
|
||
"TCP 연결수 0",
|
||
"50µs"
|
||
],
|
||
"classification": "같은 순간의 대조 폴링과 연결수·소요 시간·재현율 넷으로 귀속을 뒤집었고, 대조군이 없었으면 무엇이 됐을지까지 닫았다",
|
||
"missing-verification": "클라이언트 쪽 원인을 특정하지 않았다. 서버 탓이 아니라는 것까지가 이 확인의 범위다",
|
||
"relations": [
|
||
"reference:verify-the-injection-landed-separately-from-the-result",
|
||
"concept:the-up-metric-cannot-see-alive-but-useless",
|
||
"reference:never-subtract-values-from-two-clocks"
|
||
],
|
||
"kind": "case",
|
||
"ssot-assets": [
|
||
"measurement-control"
|
||
],
|
||
"ssot-evidence": [
|
||
"raw/d4-certificate-renewal__05-control-no-injection.txt",
|
||
"raw/d4-certificate-renewal__11-inflight-full.txt"
|
||
],
|
||
"publication": "초안",
|
||
"file": "when-the-measurement-lies/case/case-seventy-six-failures-that-were-not-the-servers.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [
|
||
"measurement-control"
|
||
],
|
||
"assetFiles": [
|
||
"measurement-control"
|
||
],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/d4-certificate-renewal__05-control-no-injection.txt",
|
||
"../../../final/evidence/raw/d4-certificate-renewal__11-inflight-full.txt"
|
||
]
|
||
},
|
||
{
|
||
"title": "산문으로 적힌 측정 장치를 실행 가능하게 고쳤더니 한 건이 깨졌다",
|
||
"slug": "commands-written-as-prose-do-not-run",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#결정이-지켜지는지-확인하는-방법-재현-가능성",
|
||
"final/document.md#검토한-선택지와-막힌-지점-스크립트를-쓰지-않는다"
|
||
],
|
||
"code": [
|
||
"kubectl run --rm -i",
|
||
"상주 탐침"
|
||
],
|
||
"classification": "헤드라인 수치를 만든 명령 넷이 산문이던 것을 셸 표현식으로 바꾸고 실제로 돌려 확인했으며, 그 확인에서 한 건이 깨진 것까지 닫았다",
|
||
"missing-verification": "고친 명령을 다른 환경에서 돌려 보지 않았다. 이 실험대에서만 확인했다",
|
||
"relations": [
|
||
"case:nine-injections-that-silently-did-nothing",
|
||
"reference:verify-the-injection-landed-separately-from-the-result"
|
||
],
|
||
"kind": "case",
|
||
"ssot-assets": [
|
||
"reproducibility-gap"
|
||
],
|
||
"ssot-evidence": [
|
||
"raw/a6-latency-injection__04-pool-under-load.txt",
|
||
"raw/followup__05-command-reproducibility.txt"
|
||
],
|
||
"publication": "초안",
|
||
"file": "when-the-measurement-lies/case/case-commands-written-as-prose-do-not-run.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [
|
||
"reproducibility-gap"
|
||
],
|
||
"assetFiles": [
|
||
"reproducibility-gap"
|
||
],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/a6-latency-injection__04-pool-under-load.txt",
|
||
"../../../final/evidence/raw/followup__05-command-reproducibility.txt"
|
||
]
|
||
},
|
||
{
|
||
"title": "가이드 26편을 순서대로 따라가니 첫 명령부터 막혔다",
|
||
"slug": "the-guides-broke-at-the-first-command",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#재현-가이드-26편과-그것을-따라가다-드러난-결함"
|
||
],
|
||
"code": [
|
||
"sudo kubectl",
|
||
"~/.kube/config",
|
||
"kubectl apply -f deploy/...",
|
||
"-l app=bff"
|
||
],
|
||
"classification": "가이드 26편을 실제로 쳐 가며 감사해 재현을 막는 결함을 계열로 확정했고 — sudo kubectl 905건, 저장소 클론 단계 누락(05·06), 그 단계에 아직 없는 리소스 조회(05) — 개별 명령은 전부 실제로 돌았던 것이라는 사실까지 확인해 「틀린 것은 명령이 아니라 그 명령이 놓인 위치」라는 공통 원인으로 닫았다",
|
||
"missing-verification": "SSOT 는 감사에서 무엇이 막혔는지까지만 적는다. 고친 가이드를 처음부터 다시 따라가 끝까지 도는지는 적혀 있지 않다. 905건 중 게스트에서 도는 것이 0건이라는 계수도 이 감사 한 번의 결과다",
|
||
"relations": [
|
||
"case:commands-written-as-prose-do-not-run",
|
||
"case:nine-injections-that-silently-did-nothing",
|
||
"reference:verify-the-injection-landed-separately-from-the-result"
|
||
],
|
||
"kind": "case",
|
||
"publication": "초안",
|
||
"file": "when-the-measurement-lies/case/case-the-guides-broke-at-the-first-command.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": []
|
||
}
|
||
],
|
||
"concept": [
|
||
{
|
||
"title": "up 지표는 살아 있지만 쓸모없는 상태를 보지 못한다",
|
||
"slug": "the-up-metric-cannot-see-alive-but-useless",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#검토한-선택지와-막힌-지점-관측을-어디에",
|
||
"final/document.md#결정이-지켜지는지-확인하는-방법-관측-도구는-부분집합"
|
||
],
|
||
"basis-version": "Prometheus up 지표 · Keycloak 26.7.0 /metrics · 이 실험대의 스크레이프 대상",
|
||
"classification": "프로세스가 살아 있고 /metrics 가 응답하기만 하면 1 이 된다는 것을 알아야 503 중에도 1 이던 관측을 읽을 수 있다",
|
||
"relations": [
|
||
"case:seventy-six-failures-that-were-not-the-servers",
|
||
"concept:readiness-hides-the-broken-node",
|
||
"reference:verify-the-injection-landed-separately-from-the-result"
|
||
],
|
||
"kind": "concept",
|
||
"publication": "초안",
|
||
"file": "when-the-measurement-lies/concept/concept-the-up-metric-cannot-see-alive-but-useless.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/a2-database-loss__04-health-and-service.txt",
|
||
"../../../final/evidence/raw/a2-database-loss__05-recovery.txt"
|
||
]
|
||
}
|
||
],
|
||
"reference": [
|
||
{
|
||
"title": "예측을 먼저 적고, 주입이 걸렸는지 결과와 따로 확인하고, 대조군 없이 귀속하지 않는다",
|
||
"slug": "verify-the-injection-landed-separately-from-the-result",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#결국-지키려던-것은-무엇이었나",
|
||
"final/document.md#문제를-어렵게-만든-제약-주입이-먹지-않는다",
|
||
"final/document.md#결정이-지켜지는지-확인하는-방법-대조군-없이는"
|
||
],
|
||
"classification": "세 규칙 가운데 **「대조군 없이 귀속하지 않는다」만 어긴 사례가 남아 있고 그것도 둘이다** — A-6 에서 −41% 인 대조군을 「영향 없음」이라 적은 것과 A-8 에서 표본 9개로 무중단을 주장한 것이고, 둘 다 나중에 고쳤다. **D-4 의 in-flight 76건은 위반이 아니라 이 규칙이 작동해 오보를 막은 사례다.** 「예측을 먼저 적는다」는 위반 사례가 0건이고, 「주입 확인을 결과와 따로」의 근거인 아홉 번은 규칙을 세우기 전의 실패다. SSOT 가 말하는 적용 범위는 이 실험대 이후의 실험까지다",
|
||
"scope": "상태를 일부러 망가뜨리고 그 영향을 재는 모든 실험",
|
||
"exceptions": "주입 없이 평시를 관측하는 측정에는 첫 두 규칙이 걸리지 않는다. 대조군 규칙은 그때도 걸린다",
|
||
"relations": [
|
||
"case:nine-injections-that-silently-did-nothing",
|
||
"case:seventy-six-failures-that-were-not-the-servers",
|
||
"concept:the-up-metric-cannot-see-alive-but-useless"
|
||
],
|
||
"kind": "reference",
|
||
"publication": "초안",
|
||
"file": "when-the-measurement-lies/reference/reference-verify-the-injection-landed-separately-from-the-result.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/a1-jgroups-transport-block__07-cluster-size.txt",
|
||
"../../../final/evidence/raw/a6-latency-injection__03-flannel-injection.txt",
|
||
"../../../final/evidence/raw/a8-rolling-restart__01-restart-availability.txt",
|
||
"../../../final/evidence/raw/d4-certificate-renewal__05-control-no-injection.txt",
|
||
"../../../final/evidence/raw/d4-certificate-renewal__08-inflight-artifact.txt"
|
||
]
|
||
},
|
||
{
|
||
"title": "두 시계에서 온 값을 빼지 않는다",
|
||
"slug": "never-subtract-values-from-two-clocks",
|
||
"readiness": "READY",
|
||
"source": [
|
||
"final/document.md#결정이-지켜지는지-확인하는-방법-두-시계"
|
||
],
|
||
"classification": "보정 없이 뺀 값이 자릿수가 아니라 방향까지 틀리는 것을 D-4·D-4a 한 쌍에서 만났다. 근거는 그 사례 하나이고, 규칙은 「다른 측정도 틀린다」가 아니라 「빼기 전에 두 시계가 같은지 확인하라」다",
|
||
"scope": "서로 다른 기계에서 온 타임스탬프의 차이를 재는 자리",
|
||
"exceptions": "같은 기계의 단조 시계로만 잰 구간에는 걸리지 않는다",
|
||
"relations": [
|
||
"case:seventy-six-failures-that-were-not-the-servers",
|
||
"case:commands-written-as-prose-do-not-run"
|
||
],
|
||
"kind": "reference",
|
||
"publication": "초안",
|
||
"file": "when-the-measurement-lies/reference/reference-never-subtract-values-from-two-clocks.md",
|
||
"status": "게시 전",
|
||
"studioId": "",
|
||
"assets": [],
|
||
"assetFiles": [],
|
||
"evidenceFiles": [
|
||
"../../../final/evidence/raw/d4a-deploy-hook__01-hook-verified.txt",
|
||
"../../../final/evidence/raw/followup__03-b4-role-propagation.txt"
|
||
]
|
||
}
|
||
],
|
||
"question": [],
|
||
"decision": []
|
||
},
|
||
"topic": "when-the-measurement-lies"
|
||
}
|
||
},
|
||
"candidates": [
|
||
{
|
||
"id": "SSOT-session-sharing-is-the-database-not-replication",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#코드보다-먼저-드러난-문제-전제가-무너졌다",
|
||
"final/document.md#선택의-이유와-지킨-경계-a1",
|
||
"final/document.md#코드보다-먼저-드러난-문제-그런데-첫-실험에서-전제가-무너졌다"
|
||
],
|
||
"summary": "클러스터는 형성됐는데 세션을 나르는 것은 데이터베이스였다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:session-sharing-is-the-database-not-replication",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-cache-temperature-decides-the-outcome",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#선택의-이유와-지킨-경계-a7-a7a"
|
||
],
|
||
"summary": "같은 설정이 캐시 온도만으로 세 가지 답을 냈다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:cache-temperature-decides-the-outcome",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-rolling-restart-keeps-sessions-drops-cache",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#선택의-이유와-지킨-경계-a8"
|
||
],
|
||
"summary": "롤링 재시작은 세션을 남기고 캐시만 지웠다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:rolling-restart-keeps-sessions-drops-cache",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-persistent-vs-volatile-user-sessions",
|
||
"kindCandidate": "CONCEPT",
|
||
"sourceRefs": [
|
||
"final/document.md#코드보다-먼저-드러난-문제-버전-조건",
|
||
"final/document.md#코드보다-먼저-드러난-문제-그리고-이-결론에는-버전-조건이-붙어-있었다"
|
||
],
|
||
"summary": "persistent-user-sessions 가 세션의 거처를 정한다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "concept:persistent-vs-volatile-user-sessions",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-state-the-version-and-the-setting-with-the-result",
|
||
"kindCandidate": "REFERENCE",
|
||
"sourceRefs": [
|
||
"final/document.md#코드보다-먼저-드러난-문제-버전-조건",
|
||
"final/document.md#얻은-것-잃은-것-적용하지-않을-때-적용되지-않는-조건",
|
||
"final/document.md#코드보다-먼저-드러난-문제-그리고-이-결론에는-버전-조건이-붙어-있었다",
|
||
"final/document.md#얻은-것-잃은-것-적용하지-않을-때-이-기록이-적용되지-않는-조건"
|
||
],
|
||
"summary": "버전과 설정을 결과와 함께 적는다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "reference:state-the-version-and-the-setting-with-the-result",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-four-logins-that-returned-200-and-vanished",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#선택의-이유와-지킨-경계-a2-a3"
|
||
],
|
||
"summary": "200 과 토큰을 받은 로그인 네 건이 데이터베이스에 없었다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:four-logins-that-returned-200-and-vanished",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-two-ways-to-lose-a-node",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#선택의-이유와-지킨-경계-a4"
|
||
],
|
||
"summary": "노드를 잃는 두 가지 — 저장소가 같이 죽는 것과 들어갈 길이 없는 것",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:two-ways-to-lose-a-node",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-200ms-of-delay-became-22-seconds",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#선택의-이유와-지킨-경계-a6"
|
||
],
|
||
"summary": "200 밀리초를 넣었더니 응답이 22.2 초가 됐다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:200ms-of-delay-became-22-seconds",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-readiness-hides-the-broken-node",
|
||
"kindCandidate": "CONCEPT",
|
||
"sourceRefs": [
|
||
"final/document.md#검토한-선택지와-막힌-지점-관측을-어디에"
|
||
],
|
||
"summary": "readiness 가 깨진 노드를 시야에서 먼저 치운다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "concept:readiness-hides-the-broken-node",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-most-of-an-outage-is-noticing",
|
||
"kindCandidate": "REFERENCE",
|
||
"sourceRefs": [
|
||
"final/document.md#선택의-이유와-지킨-경계-a4"
|
||
],
|
||
"summary": "장애 시간의 대부분은 알아채는 데 걸린다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "reference:most-of-an-outage-is-noticing",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-moving-the-session-left-the-tokens-behind",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-b0",
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-b1"
|
||
],
|
||
"summary": "세션만 Redis 로 옮기자 토큰이 따라오지 않았다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:moving-the-session-left-the-tokens-behind",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-a-primary-key-without-the-session-id",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-b2"
|
||
],
|
||
"summary": "기본키에 세션 id 가 없어서 두 번째 로그인이 첫 토큰을 덮어썼다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:a-primary-key-without-the-session-id",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-the-winner-of-the-rotation-race-also-loses",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-b3"
|
||
],
|
||
"summary": "회전 경쟁에서 이긴 요청의 토큰도 쓸 수 없었다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:the-winner-of-the-rotation-race-also-loses",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-persistence-without-a-volume-and-rotation-without-overlap",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-b5-b6"
|
||
],
|
||
"summary": "볼륨 없는 영속화와 유예 없는 키 회전",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:persistence-without-a-volume-and-rotation-without-overlap",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-two-stores-two-lookup-keys",
|
||
"kindCandidate": "CONCEPT",
|
||
"sourceRefs": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-b0"
|
||
],
|
||
"summary": "세션과 인가된 클라이언트는 조회 키가 다르다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "concept:two-stores-two-lookup-keys",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-look-at-the-lookup-key-before-moving-the-store",
|
||
"kindCandidate": "REFERENCE",
|
||
"sourceRefs": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-b0",
|
||
"final/document.md#얻은-것-잃은-것-적용하지-않을-때-열린-질문-네-개에-대한-답"
|
||
],
|
||
"summary": "저장소를 옮기기 전에 조회 키를 본다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "reference:look-at-the-lookup-key-before-moving-the-store",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-split-the-two-stores-and-design-each",
|
||
"kindCandidate": "DECISION",
|
||
"sourceRefs": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-b2",
|
||
"final/document.md#얻은-것-잃은-것-적용하지-않을-때-열린-질문-네-개에-대한-답"
|
||
],
|
||
"summary": "세션과 토큰의 저장소를 나눠 각각 설계한다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "decision:split-the-two-stores-and-design-each",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-nginx-does-not-overwrite-a-header-it-never-sets",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-b4"
|
||
],
|
||
"summary": "nginx 는 자기가 설정하지 않은 헤더를 덮어쓰지 않았다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:nginx-does-not-overwrite-a-header-it-never-sets",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-nobody-implemented-backchannel-logout",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-c층"
|
||
],
|
||
"summary": "백채널 로그아웃은 양쪽 다 없었다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:nobody-implemented-backchannel-logout",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-orphan-sessions-and-the-ttl-that-finds-them",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-b7-b7a"
|
||
],
|
||
"summary": "쿠키에 세션을 담으면 지울 대상을 잃는다 — TTL 로 되찾은 고아 세션",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:orphan-sessions-and-the-ttl-that-finds-them",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-clear-the-header-before-you-trust-it",
|
||
"kindCandidate": "REFERENCE",
|
||
"sourceRefs": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-b4",
|
||
"final/document.md#얻은-것-잃은-것-적용하지-않을-때-열린-질문-네-개에-대한-답"
|
||
],
|
||
"summary": "믿기 전에 그 헤더를 먼저 지운다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "reference:clear-the-header-before-you-trust-it",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-the-certificate-that-took-38-minutes-to-reach-the-wire",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-d4"
|
||
],
|
||
"summary": "새 인증서가 디스크에 있고 38분 25초 동안 옛 인증서가 나갔다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:the-certificate-that-took-38-minutes-to-reach-the-wire",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-a-deploy-hook-closed-the-gap-to-two-seconds",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-d4a"
|
||
],
|
||
"summary": "deploy 훅 하나가 그 공백을 1~2초로 줄였다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:a-deploy-hook-closed-the-gap-to-two-seconds",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-the-upgrade-that-would-not-roll-back",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-d1-d2"
|
||
],
|
||
"summary": "되돌리기를 막은 것은 체크섬이었고 그 판정은 조건부였다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:the-upgrade-that-would-not-roll-back",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-judge-a-reload-by-the-worker-pid-not-the-log",
|
||
"kindCandidate": "REFERENCE",
|
||
"sourceRefs": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-d4",
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-d4a"
|
||
],
|
||
"summary": "적용됐는지는 로그 문구가 아니라 상태로 판정한다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "reference:judge-a-reload-by-the-worker-pid-not-the-log",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-does-the-renewal-timer-actually-renew",
|
||
"kindCandidate": "QUESTION",
|
||
"sourceRefs": [
|
||
"final/document.md#얻은-것-잃은-것-적용하지-않을-때-재보지-않은-것",
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-d4a"
|
||
],
|
||
"summary": "갱신 타이머가 실제 갱신에서도 도는가",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "question:does-the-renewal-timer-actually-renew",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-put-the-reload-in-a-deploy-hook",
|
||
"kindCandidate": "DECISION",
|
||
"sourceRefs": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-d4a"
|
||
],
|
||
"summary": "reload 를 사람이 아니라 deploy 훅이 부르게 한다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "decision:put-the-reload-in-a-deploy-hook",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-nine-injections-that-silently-did-nothing",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#문제를-어렵게-만든-제약-주입이-먹지-않는다"
|
||
],
|
||
"summary": "주입이 아홉 번 조용히 실패했고 전부 아무 일도 없는 것처럼 보였다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:nine-injections-that-silently-did-nothing",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-seventy-six-failures-that-were-not-the-servers",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#결정이-지켜지는지-확인하는-방법-대조군-없이는"
|
||
],
|
||
"summary": "실패 76 건이 서버 탓이 아니었다 — 대조군이 오보를 막았다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:seventy-six-failures-that-were-not-the-servers",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-commands-written-as-prose-do-not-run",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#결정이-지켜지는지-확인하는-방법-재현-가능성",
|
||
"final/document.md#검토한-선택지와-막힌-지점-스크립트를-쓰지-않는다"
|
||
],
|
||
"summary": "산문으로 적힌 측정 장치를 실행 가능하게 고쳤더니 한 건이 깨졌다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:commands-written-as-prose-do-not-run",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-the-up-metric-cannot-see-alive-but-useless",
|
||
"kindCandidate": "CONCEPT",
|
||
"sourceRefs": [
|
||
"final/document.md#검토한-선택지와-막힌-지점-관측을-어디에",
|
||
"final/document.md#결정이-지켜지는지-확인하는-방법-관측-도구는-부분집합"
|
||
],
|
||
"summary": "up 지표는 살아 있지만 쓸모없는 상태를 보지 못한다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "concept:the-up-metric-cannot-see-alive-but-useless",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-verify-the-injection-landed-separately-from-the-result",
|
||
"kindCandidate": "REFERENCE",
|
||
"sourceRefs": [
|
||
"final/document.md#결국-지키려던-것은-무엇이었나",
|
||
"final/document.md#문제를-어렵게-만든-제약-주입이-먹지-않는다",
|
||
"final/document.md#결정이-지켜지는지-확인하는-방법-대조군-없이는",
|
||
"final/document.md#선택의-이유와-지킨-경계-a층-keycloak-자체가-깨질-때"
|
||
],
|
||
"summary": "예측을 먼저 적고, 주입이 걸렸는지 결과와 따로 확인하고, 대조군 없이 귀속하지 않는다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "reference:verify-the-injection-landed-separately-from-the-result",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-never-subtract-values-from-two-clocks",
|
||
"kindCandidate": "REFERENCE",
|
||
"sourceRefs": [
|
||
"final/document.md#결정이-지켜지는지-확인하는-방법-두-시계"
|
||
],
|
||
"summary": "두 시계에서 온 값을 빼지 않는다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "reference:never-subtract-values-from-two-clocks",
|
||
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
|
||
},
|
||
{
|
||
"id": "SSOT-a1-transport-vs-discovery",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#선택의-이유와-지킨-경계-a1"
|
||
],
|
||
"summary": "7800 을 끊었더니 세션 공유는 안 깨지고 로그아웃 전파가 깨졌다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:session-sharing-is-the-database-not-replication",
|
||
"reason": "SSOT 가 이 결과를 A-0 의 확증으로 적는다. 발견은 DB 를 쓰고 전송은 7800 을 쓴다는 같은 사실의 두 면이다"
|
||
},
|
||
{
|
||
"id": "SSOT-a5-asymmetric-partition",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#선택의-이유와-지킨-경계-a5"
|
||
],
|
||
"summary": "비대칭 분단은 클러스터를 가르지 못하고 전면 장애 경로도 없다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:two-ways-to-lose-a-node",
|
||
"reason": "결론이 「전면 장애가 없다」라 독립 기록으로 읽을 이유가 약하다. 여기서 나온 주입 실패 셋은 측정 Topic 의 Case 가 담는다"
|
||
},
|
||
{
|
||
"id": "SSOT-c1-sso-across-apps",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-c층"
|
||
],
|
||
"summary": "두 앱이 같은 realm 으로 SSO 되는 것을 확인했다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:nobody-implemented-backchannel-logout",
|
||
"reason": "SSO 가 되는 것 자체는 기대한 동작이고, 이 실험이 남긴 것은 그 옆에서 관측한 로그아웃 미전파다"
|
||
},
|
||
{
|
||
"id": "SSOT-d1-backup-restore",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-d1-d2"
|
||
],
|
||
"summary": "백업과 복구 — 본문 RTO 30초와 타임라인 41초가 어긋났다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:seventy-six-failures-that-were-not-the-servers",
|
||
"reason": "SSOT 가 이 건을 문서와 증거가 어긋난 사례 표에 넣었고, 그 표가 측정 Topic 의 근거다"
|
||
},
|
||
{
|
||
"id": "SSOT-d3-secret-base64",
|
||
"kindCandidate": "REFERENCE",
|
||
"sourceRefs": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-d3"
|
||
],
|
||
"summary": "kubectl get secret 의 base64 는 암호화가 아니라 인코딩이다",
|
||
"disposition": "KEEP_IN_SSOT",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": null,
|
||
"reason": "널리 알려진 사실이고 이 실험이 더한 것은 두 노출 경로를 확인한 것까지다. 규칙으로 빼면 다른 프로젝트에 줄 것이 없다"
|
||
},
|
||
{
|
||
"id": "SSOT-lab-topology",
|
||
"kindCandidate": "CONCEPT",
|
||
"sourceRefs": [
|
||
"final/document.md#문제를-어렵게-만든-제약-실험대"
|
||
],
|
||
"summary": "베어메탈 한 대 위의 VM 두 대와 인증서 SAN 셋",
|
||
"disposition": "KEEP_IN_SSOT",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": null,
|
||
"reason": "측정 환경의 구성이다. 각 Case 의 검증 환경이 필요한 만큼을 이미 담는다"
|
||
},
|
||
{
|
||
"id": "SSOT-guest-host-sudo",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#문제를-어렵게-만든-제약-sudo"
|
||
],
|
||
"summary": "호스트의 sudo 가 비밀번호를 요구해 조용히 실패했다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:nine-injections-that-silently-did-nothing",
|
||
"reason": "아홉 건 표의 8 번이 이 사건이다. 표 밖으로 빼면 같은 이야기가 둘이 된다"
|
||
},
|
||
{
|
||
"id": "SSOT-observation-three-points",
|
||
"kindCandidate": "REFERENCE",
|
||
"sourceRefs": [
|
||
"final/document.md#검토한-선택지와-막힌-지점-관측을-어디에"
|
||
],
|
||
"summary": "관측 지점을 외부·지표·데이터베이스 셋으로 늘렸다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "reference:verify-the-injection-landed-separately-from-the-result",
|
||
"reason": "주입이 걸렸는지 결과와 따로 확인한다는 규칙이 이 세 지점을 요구한다. 그 규칙의 한 절이다"
|
||
},
|
||
{
|
||
"id": "SSOT-techviz-remake",
|
||
"kindCandidate": "REFERENCE",
|
||
"sourceRefs": [
|
||
"final/document.md#이-기록에-아직-없는-것"
|
||
],
|
||
"summary": "손그림 28 건을 techviz 로 다시 만들며 lint 가 잡은 두 가지",
|
||
"disposition": "KEEP_IN_SSOT",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": null,
|
||
"reason": "이 저장소의 그림 제작 절차이지 이 주제의 독자 질문에 답하지 않는다. 후보 범위 밖이다"
|
||
},
|
||
{
|
||
"id": "SSOT-ghost-rows-are-cleaned-by-the-surviving-coordinator",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#2026-09-11-추가-측정-워크로드-종류가-클러스터에-미치는-영향-무엇을-쟀나",
|
||
"final/document.md#2026-09-11-추가-측정-워크로드-종류가-클러스터에-미치는-영향-관측",
|
||
"final/document.md#2026-09-11-추가-측정-워크로드-종류가-클러스터에-미치는-영향-결론"
|
||
],
|
||
"summary": "워크로드 종류 둘 × 종료 방식 둘에서 JGROUPS_PING 에 유령 행이 남지 않았고, 정리 주체는 남은 코디네이터였다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:ghost-rows-are-cleaned-by-the-surviving-coordinator",
|
||
"reason": "하나의 물음(유령 행은 어떻게 정리되는가)에 네 조합의 관측과 코디네이터 로그로 답하고, experiment-plan.md 의 미해결 항목 하나를 「자동」으로 닫는다. 다른 Case 의 한 절로 넣으면 그 Case 의 결론이 달라진다"
|
||
},
|
||
{
|
||
"id": "SSOT-statefulset-does-not-reuse-the-membership-row",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#2026-09-11-추가-측정-워크로드-종류가-클러스터에-미치는-영향-관측",
|
||
"final/document.md#2026-09-11-추가-측정-워크로드-종류가-클러스터에-미치는-영향-결론"
|
||
],
|
||
"summary": "StatefulSet 이라도 같은 행을 덮어쓰지 않는다 — address 는 매번 새로 발급되고 name 의 접미사도 바뀐다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:ghost-rows-are-cleaned-by-the-surviving-coordinator",
|
||
"reason": "같은 측정의 관측표 두 행이다. 같은 질문에서 나와 같은 결론(워크로드 종류가 클러스터 동작을 바꾸지 않는다)에 닿으므로 그 Case 의 표로 들어간다"
|
||
},
|
||
{
|
||
"id": "SSOT-workload-kind-for-a-keycloak-cluster",
|
||
"kindCandidate": "DECISION",
|
||
"sourceRefs": [
|
||
"final/document.md#2026-09-11-추가-측정-워크로드-종류가-클러스터에-미치는-영향-결론"
|
||
],
|
||
"summary": "StatefulSet 의 근거가 사람이 읽기 위한 성질 둘로 좁혀졌고, 세션을 DB 에 두고 롤링 정책을 명시하면 Deployment 도 성립한다",
|
||
"disposition": "NEEDS_DECISION",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": null,
|
||
"reason": "SSOT 는 「Deployment 도 성립한다」를 inferred 로 적을 뿐 이 프로젝트가 워크로드 종류를 바꾸기로 정한 기록이 없다. 정해진 뒤에 다시 판정한다"
|
||
},
|
||
{
|
||
"id": "SSOT-who-cleans-when-the-coordinator-itself-dies",
|
||
"kindCandidate": "QUESTION",
|
||
"sourceRefs": [
|
||
"final/document.md#2026-09-11-추가-측정-워크로드-종류가-클러스터에-미치는-영향-결론"
|
||
],
|
||
"summary": "코디네이터 자신이 죽으면 유령 행을 누가 정리하는가 — 정상 종료와 SIGKILL 만 쟀다",
|
||
"disposition": "NEEDS_EVIDENCE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": null,
|
||
"reason": "SSOT 가 unknown 으로 적고 다음 검증도 종료 기준도 적지 않았다. Open Question 이 요구하는 칸을 지어내지 않고 Case 의 missing-verification 에 남긴다. 재고 나서 다시 판정한다"
|
||
},
|
||
{
|
||
"id": "SSOT-the-guides-broke-at-the-first-command",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#재현-가이드-26편과-그것을-따라가다-드러난-결함"
|
||
],
|
||
"summary": "가이드 26편을 순서대로 따라가며 감사하니 재현을 막는 결함이 계열로 나왔고, 틀린 것은 명령이 아니라 명령이 놓인 위치였다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:the-guides-broke-at-the-first-command",
|
||
"reason": "감사라는 하나의 검증에 규모가 붙은 관측(905건·05·06편)과 공통 원인이 있고 결론이 닫힌다. 측정 장치가 산문이던 Case 와는 물음도 진단도 다르다"
|
||
},
|
||
{
|
||
"id": "SSOT-905-sudo-kubectl-in-the-guides",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#재현-가이드-26편과-그것을-따라가다-드러난-결함"
|
||
],
|
||
"summary": "실험 가이드 전편이 sudo kubectl 을 쓰는데 kubeconfig 는 lab host 의 ~/.kube/config 에 있어 첫 명령부터 막힌다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:the-guides-broke-at-the-first-command",
|
||
"reason": "결함 표의 첫 행이다. 표의 행 하나가 될 것을 기록 하나로 만들지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-follow-your-own-guide-in-order-once",
|
||
"kindCandidate": "REFERENCE",
|
||
"sourceRefs": [
|
||
"final/document.md#재현-가이드-26편과-그것을-따라가다-드러난-결함"
|
||
],
|
||
"summary": "가이드를 쓴 뒤 처음부터 순서대로 한 번 따라간다 — 개별 명령이 맞아도 놓인 위치가 틀린다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:the-guides-broke-at-the-first-command",
|
||
"reason": "그 Case 의 결론을 선언문으로 바꾼 것이다. 적용 조건과 예외를 SSOT 에서 댈 수 없어 Reference 로 서지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-guides-are-baseline-inject-verify-observe-recover",
|
||
"kindCandidate": "CONCEPT",
|
||
"sourceRefs": [
|
||
"final/document.md#재현-가이드-26편과-그것을-따라가다-드러난-결함"
|
||
],
|
||
"summary": "가이드 26편은 기준선 → 주입 → 주입 검증 → 관찰 → 복구 구조이고 주입 검증이 핵심인 편이 많다",
|
||
"disposition": "KEEP_IN_SSOT",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": null,
|
||
"reason": "재현 문서의 구성이지 Case 를 읽기 전에 처음부터 설명해야 하는 메커니즘이 아니다. 주입 검증이 왜 핵심인지는 이미 case:nine-injections-that-silently-did-nothing 이 담는다"
|
||
},
|
||
{
|
||
"id": "SSOT-reproduce-a0-session-sharing-path",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-0"
|
||
],
|
||
"summary": "세션 공유 경로를 가르는 시험 넷을 순서대로 치고 문장 로깅으로 SQL 을 포획한다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-a0-session-sharing-path",
|
||
"reason": "SSOT 가 이 편을 명령·예상 출력·되돌리기까지 갖춘 한 줄기 절차로 담고 있고, 읽는 사람이 그대로 쳐야 성립한다. 발견은 A층 Case 가 이미 담으므로 겹치지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-reproduce-a1-jgroups-transport-block",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-1"
|
||
],
|
||
"summary": "NetworkPolicy 로 7800 을 빼고 conntrack 을 지운 뒤 파드를 재시작해 분단을 만든다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-a1-jgroups-transport-block",
|
||
"reason": "SSOT 가 이 편을 명령·예상 출력·되돌리기까지 갖춘 한 줄기 절차로 담고 있고, 읽는 사람이 그대로 쳐야 성립한다. 발견은 A층 Case 가 이미 담으므로 겹치지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-reproduce-a2-database-loss",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-2"
|
||
],
|
||
"summary": "DB 를 정상 종료시키고 네 경로와 헬스·엔드포인트·up 을 같은 명령으로 견준다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-a2-database-loss",
|
||
"reason": "SSOT 가 이 편을 명령·예상 출력·되돌리기까지 갖춘 한 줄기 절차로 담고 있고, 읽는 사람이 그대로 쳐야 성립한다. 발견은 A층 Case 가 이미 담으므로 겹치지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-reproduce-a3-database-crash",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-3"
|
||
],
|
||
"summary": "죽이는 데 실패하는 두 방법을 먼저 밟고 백엔드를 죽인 뒤 클라이언트 sid 와 DB 를 견준다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-a3-database-crash",
|
||
"reason": "SSOT 가 이 편을 명령·예상 출력·되돌리기까지 갖춘 한 줄기 절차로 담고 있고, 읽는 사람이 그대로 쳐야 성립한다. 발견은 A층 Case 가 이미 담으므로 겹치지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-reproduce-a4-node-loss",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-4"
|
||
],
|
||
"summary": "virsh destroy 로 워커와 컨트롤 플레인을 차례로 끄고 인지·축출·재배치 실패를 시각으로 잰다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-a4-node-loss",
|
||
"reason": "SSOT 가 이 편을 명령·예상 출력·되돌리기까지 갖춘 한 줄기 절차로 담고 있고, 읽는 사람이 그대로 쳐야 성립한다. 발견은 A층 Case 가 이미 담으므로 겹치지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-reproduce-a5-asymmetric-partition",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-5"
|
||
],
|
||
"summary": "실패하는 주입 둘을 먼저 밟고 수신측 raw PREROUTING 에 넣은 뒤 양방향으로 갈라 놓는다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-a5-asymmetric-partition",
|
||
"reason": "SSOT 가 이 편을 명령·예상 출력·되돌리기까지 갖춘 한 줄기 절차로 담고 있고, 읽는 사람이 그대로 쳐야 성립한다. 발견은 A층 Case 가 이미 담으므로 겹치지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-reproduce-a6-latency-injection",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-6"
|
||
],
|
||
"summary": "인터페이스 이름과 캡슐화에 두 번 막힌 뒤 flannel.1 에 지연을 걸고 동시 20건으로 큐잉을 만든다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-a6-latency-injection",
|
||
"reason": "SSOT 가 이 편을 명령·예상 출력·되돌리기까지 갖춘 한 줄기 절차로 담고 있고, 읽는 사람이 그대로 쳐야 성립한다. 발견은 A층 Case 가 이미 담으므로 겹치지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-reproduce-a7-volatile-comparison",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-7"
|
||
],
|
||
"summary": "args 한 줄로 volatile 로 바꾸고 A-0·A-8·A-1·A-2 를 같은 명령으로 다시 친 뒤 원복한다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-a7-volatile-comparison",
|
||
"reason": "SSOT 가 이 편을 명령·예상 출력·되돌리기까지 갖춘 한 줄기 절차로 담고 있고, 읽는 사람이 그대로 쳐야 성립한다. 발견은 A층 Case 가 이미 담으므로 겹치지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-reproduce-a7a-volatile-cause",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-7a"
|
||
],
|
||
"summary": "표식과 문장 로깅으로 실패한 SQL 을 확정하고 재시작을 끼워 캐시 온도 셋을 따로 재현한다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-a7a-volatile-cause",
|
||
"reason": "SSOT 가 이 편을 명령·예상 출력·되돌리기까지 갖춘 한 줄기 절차로 담고 있고, 읽는 사람이 그대로 쳐야 성립한다. 발견은 A층 Case 가 이미 담으므로 겹치지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-reproduce-a8-rolling-restart",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-8"
|
||
],
|
||
"summary": "재시작 전 토큰을 파드 안에 담고 롤링 재시작을 건 뒤 refresh·DB 행·캐시를 함께 본다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-a8-rolling-restart",
|
||
"reason": "SSOT 가 이 편을 명령·예상 출력·되돌리기까지 갖춘 한 줄기 절차로 담고 있고, 읽는 사람이 그대로 쳐야 성립한다. 발견은 A층 Case 가 이미 담으므로 겹치지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-a-layer-guide-file-map",
|
||
"kindCandidate": "REFERENCE",
|
||
"sourceRefs": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서"
|
||
],
|
||
"summary": "열 편과 근거 가이드 파일·줄 수를 짝지은 대응표",
|
||
"disposition": "KEEP_IN_SSOT",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": null,
|
||
"reason": "어느 절이 어느 가이드에서 왔는지를 대는 색인이다. 열 Setup 의 source 가 같은 것을 가리키므로 따로 기록으로 만들면 두 곳이 같은 말을 한다"
|
||
},
|
||
{
|
||
"id": "SSOT-a-layer-observed-and-unknown-marks",
|
||
"kindCandidate": "REFERENCE",
|
||
"sourceRefs": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서"
|
||
],
|
||
"summary": "가이드의 실측·형태·미검증 표시를 (observed)·(unknown) 으로 옮기는 규약",
|
||
"disposition": "KEEP_IN_SSOT",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": null,
|
||
"reason": "이 문서가 인용을 표시하는 규약이지 다음 프로젝트에 줄 규칙이 아니다. 적용 범위와 예외를 SSOT 에서 댈 수 없어 Reference 로 서지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-a-layer-two-forms-for-each-command",
|
||
"kindCandidate": "REFERENCE",
|
||
"sourceRefs": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서"
|
||
],
|
||
"summary": "가이드가 규범을 어긴 곳마다 이 실험대가 친 형태와 따라 할 형태를 나란히 적고, 안 친 쪽은 unknown 으로 표시한다",
|
||
"disposition": "KEEP_IN_SSOT",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": null,
|
||
"reason": "열 Setup 이 그대로 따르는 편집 규약이라 본문에서 매번 보인다. 규칙 하나로 빼면 적용 조건이 이 문서 밖으로 안 나간다"
|
||
},
|
||
{
|
||
"id": "SSOT-a-layer-preamble-sudo-kubectl-mismatch",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서"
|
||
],
|
||
"summary": "열 편 중 아홉의 전제 한 줄이 sudo kubectl 을 시키는데 같은 폴더의 README 는 붙이지 말라고 적는다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:the-guides-broke-at-the-first-command",
|
||
"reason": "905건을 센 그 Case 의 첫 행과 같은 고장이다. A층 전문이 더한 것은 본문 명령 블록에는 한 번도 안 쓰이고 전제 한 줄만 옛 형태로 남았다는 확인까지다"
|
||
},
|
||
{
|
||
"id": "SSOT-a3-two-clean-zero-loss-results-that-were-not-kills",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서",
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-3"
|
||
],
|
||
"summary": "A-3 은 세 번 죽여 두 번 실패했고, 실패한 두 번이 모두 「유실 0건」이라는 깨끗한 결과를 냈다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:nine-injections-that-silently-did-nothing",
|
||
"reason": "「주입했다고 걸린 것이 아니다」의 사례 둘이고 그 Case 의 표가 이미 같은 두 행을 담는다. 절차 쪽은 setup:reproduce-a3-database-crash 가 받는다"
|
||
},
|
||
{
|
||
"id": "SSOT-a5-three-blocked-injections",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-5"
|
||
],
|
||
"summary": "A-5 에서 iptables 가 세 번 막혔다 — kube-router 의 체인 재삽입, 방향을 잘못 짚은 raw 규칙, 그리고 성공 뒤의 역방향 재연결",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:nine-injections-that-silently-did-nothing",
|
||
"reason": "아홉 번 표의 4번과 5번이 그것이다. 새 기록으로 만들면 같은 표를 두 번 쓴다"
|
||
},
|
||
{
|
||
"id": "SSOT-scripts-printed-success-while-nothing-landed",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-6",
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-7a"
|
||
],
|
||
"summary": "A-6 의 스크립트는 Cannot find device 네 줄 사이에 「적용완료」를 찍었고, A-7a 의 표식 함수는 출력을 /dev/null 로 버렸다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:commands-written-as-prose-do-not-run",
|
||
"reason": "성공 메시지가 커널이 아니라 스크립트에서 나온 같은 고장 둘이고, 「스크립트를 쓰지 않는다」를 근거로 대는 그 Case 가 이미 이 절을 가리킨다"
|
||
},
|
||
{
|
||
"id": "SSOT-jgroups-connection-direction-is-not-fixed",
|
||
"kindCandidate": "CONCEPT",
|
||
"sourceRefs": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-5"
|
||
],
|
||
"summary": "JGroups 의 TCP 연결 방향은 고정이 아니라 먼저 JOIN 을 건 쪽에 따라 정해지고 파드가 재시작될 때마다 바뀔 수 있다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-a5-asymmetric-partition",
|
||
"reason": "이 사실이 쓰이는 곳은 「규칙을 어느 노드에 어느 목적지로 넣나」 하나뿐이고, A-5 절차가 매번 conntrack -L 로 보라고 적는다. 메커니즘만 떼면 읽을 사람이 없다"
|
||
},
|
||
{
|
||
"id": "SSOT-why-the-entry-point-returned-000-for-40-seconds",
|
||
"kindCandidate": "QUESTION",
|
||
"sourceRefs": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-4"
|
||
],
|
||
"summary": "노드를 뽑은 뒤 처음 40초 동안 정문이 503 이 아니라 000 이었던 까닭을 nginx 로그로 확인하지 못했다",
|
||
"disposition": "NEEDS_EVIDENCE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": null,
|
||
"reason": "SSOT 가 upstream 에 두 노드가 다 들어 있다는 구조로 설명하지만, 그것을 잡았어야 할 증거 파일의 마지막 절이 제목만 있고 비어 있다. 답을 낼 명령은 A-4 절에 unknown 으로 적혀 있고 아직 안 쳤다"
|
||
},
|
||
{
|
||
"id": "SSOT-how-long-the-client-scope-cache-stays-warm",
|
||
"kindCandidate": "QUESTION",
|
||
"sourceRefs": [
|
||
"final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-7a"
|
||
],
|
||
"summary": "CLIENT_SCOPE_CLIENT 조회 결과의 캐시가 얼마나 오래 더운지를 재지 않았다",
|
||
"disposition": "NEEDS_EVIDENCE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": null,
|
||
"reason": "만료 시간을 모르므로 재현 C 를 한참 뒤에 다시 재면 또 다른 답이 나올 수 있다. 캐시 온도가 결과를 가른다는 판정의 유효 기간이 여기에 달려 있는데 아직 측정이 없다"
|
||
},
|
||
{
|
||
"id": "SSOT-reproduce-b0-default-session-store",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-0"
|
||
],
|
||
"summary": "소스 넷을 B-0 시점으로 되돌려 다시 빌드하고 두 노드에 import 한 뒤 자동구성이 고른 빈을 찍는다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-b0-default-session-store",
|
||
"reason": "SSOT 가 이 편을 명령·예상 출력·되돌리기까지 갖춘 한 줄기 절차로 담고 있고, 읽는 사람이 그대로 쳐야 성립한다. 발견은 B층 Case 가 이미 담으므로 겹치지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-reproduce-b1-redis-session-store",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-1"
|
||
],
|
||
"summary": "의존성 둘을 넣고 일부러 고장 난 채 한 번 배포한 뒤 enableServiceLinks 로 고치고 빈 목록을 견준다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-b1-redis-session-store",
|
||
"reason": "SSOT 가 이 편을 명령·예상 출력·되돌리기까지 갖춘 한 줄기 절차로 담고 있고, 읽는 사람이 그대로 쳐야 성립한다. 발견은 B층 Case 가 이미 담으므로 겹치지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-reproduce-b2-jdbc-token-store",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-2"
|
||
],
|
||
"summary": "PostgreSQL 전용 DDL 을 파일로 태우고 기본키를 읽은 뒤 덮어쓰기와 로그아웃 정리를 같은 명령으로 잰다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-b2-jdbc-token-store",
|
||
"reason": "SSOT 가 이 편을 명령·예상 출력·되돌리기까지 갖춘 한 줄기 절차로 담고 있고, 읽는 사람이 그대로 쳐야 성립한다. 발견은 B층 Case 가 이미 담으므로 겹치지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-reproduce-b3-refresh-contention",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-3"
|
||
],
|
||
"summary": "상주 탐침 파드에서 회전을 켜고 같은 refresh token 다섯 개를 동시에 던져 client session 을 센다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-b3-refresh-contention",
|
||
"reason": "SSOT 가 이 편을 명령·예상 출력·되돌리기까지 갖춘 한 줄기 절차로 담고 있고, 읽는 사람이 그대로 쳐야 성립한다. 발견은 B층 Case 가 이미 담으므로 겹치지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-reproduce-b4-forged-identity-headers",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-4"
|
||
],
|
||
"summary": "대조군을 먼저 잡고 동명 헤더·쉼표·크기·위조 신원을 차례로 보내며 JWT 경로와 견준다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-b4-forged-identity-headers",
|
||
"reason": "SSOT 가 이 편을 명령·예상 출력·되돌리기까지 갖춘 한 줄기 절차로 담고 있고, 읽는 사람이 그대로 쳐야 성립한다. 발견은 B층 Case 가 이미 담으므로 겹치지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-reproduce-b5-redis-loss",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-5"
|
||
],
|
||
"summary": "Redis 를 0대로 내려 세 경로와 health 그룹을 재고, 볼륨을 떼고 AOF 만 켠 채 파드를 지운다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-b5-redis-loss",
|
||
"reason": "SSOT 가 이 편을 명령·예상 출력·되돌리기까지 갖춘 한 줄기 절차로 담고 있고, 읽는 사람이 그대로 쳐야 성립한다. 발견은 B층 Case 가 이미 담으므로 겹치지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-reproduce-b6-key-rotation",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-6"
|
||
],
|
||
"summary": "우선순위가 높은 RSA 공급자를 더해 겹침을 확인한 뒤 옛 공급자를 지우고 두 토큰을 다시 친다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-b6-key-rotation",
|
||
"reason": "SSOT 가 이 편을 명령·예상 출력·되돌리기까지 갖춘 한 줄기 절차로 담고 있고, 읽는 사람이 그대로 쳐야 성립한다. 발견은 B층 Case 가 이미 담으므로 겹치지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-reproduce-b7-cookie-secret-rotation",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-7"
|
||
],
|
||
"summary": "Grafana Ingress 를 빌려 oauth2-proxy 를 띄우고 secret 참조를 A→B 로 바꾼 뒤 로그 두 줄을 가른다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-b7-cookie-secret-rotation",
|
||
"reason": "SSOT 가 이 편을 명령·예상 출력·되돌리기까지 갖춘 한 줄기 절차로 담고 있고, 읽는 사람이 그대로 쳐야 성립한다. 발견은 B층 Case 가 이미 담으므로 겹치지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-reproduce-b7a-orphan-session",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-7a"
|
||
],
|
||
"summary": "refresh:disabled 를 확인하고 두 번 회전시킨 뒤 TTL 역산으로 고아를 골라 지운다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-b7a-orphan-session",
|
||
"reason": "SSOT 가 이 편을 명령·예상 출력·되돌리기까지 갖춘 한 줄기 절차로 담고 있고, 읽는 사람이 그대로 쳐야 성립한다. 발견은 B층 Case 가 이미 담으므로 겹치지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-b-layer-shared-preconditions",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서"
|
||
],
|
||
"summary": "B층 아홉 편의 공통 전제 — 애플리케이션 소스와 매니페스트를 고쳐 다시 빌드하고 두 노드에 각각 import 한다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-b0-default-session-store",
|
||
"reason": "레지스트리가 없어 `imagePullPolicy: Never` 이고 되돌리기가 `git checkout` 뒤 재빌드까지 가는 것은 아홉 편에 공통이지만, 읽는 사람이 그 순서를 처음 치는 자리는 B-0 하나다. 떼어 놓으면 명령이 없는 전제 목록만 남는다"
|
||
},
|
||
{
|
||
"id": "SSOT-b-layer-marker-convention",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서"
|
||
],
|
||
"summary": "가이드가 출력에 붙인 표시 셋 — 실측 · 형태 · 미검증",
|
||
"disposition": "KEEP_IN_SSOT",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "",
|
||
"reason": "A층과 같은 셋이고 SSOT 가 이미 그 뜻을 표로 적어 두었다. 아홉 편이 각자 그 규약을 다시 쓰므로 독립 기록으로 떼면 같은 말이 열 번째로 늘어난다"
|
||
},
|
||
{
|
||
"id": "SSOT-b-layer-premise-says-sudo-kubectl",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서"
|
||
],
|
||
"summary": "아홉 편 중 여덟의 전제가 `sudo kubectl` 인데 같은 폴더 README 는 반대로 적고, 본문 명령 블록은 `sudo kubectl` 을 한 번도 쓰지 않는다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:the-guides-broke-at-the-first-command",
|
||
"reason": "A층에서 이미 같은 결함을 관측해 Case 로 담았고, B층은 그 결함이 한 층 더 남아 있다는 같은 사실의 두 번째 표본이다. 표본이 하나 늘었다고 기록이 하나 늘 이유는 없다"
|
||
},
|
||
{
|
||
"id": "SSOT-b-layer-long-unverified-grep-lines",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서"
|
||
],
|
||
"summary": "`jq` 가 없어 JSON 을 `grep`·`sed` 로 읽고, 그 줄들이 길고 전부 미검증으로 표시되어 있다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:commands-written-as-prose-do-not-run",
|
||
"reason": "「산문으로 적힌 측정 장치」와 같은 결함이고 그 Case 가 이미 판정 기준을 갖고 있다. B층 아홉 편은 같은 기준을 적용받는 대상이지 새 결론이 아니다"
|
||
},
|
||
{
|
||
"id": "SSOT-b1-service-links-injects-redis-port",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-1"
|
||
],
|
||
"summary": "쿠버네티스가 Service 이름마다 넣는 환경변수가 `REDIS_PORT` 를 URL 로 덮어써 파드가 설정 바인딩에서 죽는다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-b1-redis-session-store",
|
||
"reason": "가이드가 이 함정을 **일부러 한 번 겪게** 해 두었고 쓰이는 곳이 「첫 배포를 어떻게 하나」 하나뿐이다. 메커니즘만 떼면 읽을 사람이 없다"
|
||
},
|
||
{
|
||
"id": "SSOT-b4-header-size-cliff",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-4"
|
||
],
|
||
"summary": "헤더가 8KB 를 넘으면 Tomcat 이 `400`, 16KB 이상이면 nginx 가 연결을 끊어 `000` 이 된다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:nginx-does-not-overwrite-a-header-it-never-sets",
|
||
"reason": "같은 실험의 관측이고 그 Case 가 B-4 의 발견을 담는 자리다. 없애고 그 Case 의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
|
||
},
|
||
{
|
||
"id": "SSOT-b4-strip-header-prescription-unmeasured",
|
||
"kindCandidate": "REFERENCE",
|
||
"sourceRefs": [
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-4"
|
||
],
|
||
"summary": "`proxy_set_header X-Auth-Request-* \"\"` 로 먼저 지우면 위조가 막힌다",
|
||
"disposition": "NEEDS_EVIDENCE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "",
|
||
"reason": "이 실험대는 그 수정을 적용한 적이 없고 적용 뒤 다시 재는 절도 미검증이다. **위조가 막히는지는 이 SSOT 어디에도 측정으로 없다** — 규칙으로 올리려면 적용하고 같은 명령을 다시 쳐야 한다"
|
||
},
|
||
{
|
||
"id": "SSOT-b6-overlap-must-cover-the-longest-token",
|
||
"kindCandidate": "REFERENCE",
|
||
"sourceRefs": [
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-6"
|
||
],
|
||
"summary": "키 겹침 구간은 옛 키로 서명된 것 중 가장 오래 사는 것의 수명만큼 — 이 실험대에서는 최소 30분",
|
||
"disposition": "NEEDS_EVIDENCE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "",
|
||
"reason": "access token 60초와 refresh token 1800초라는 설정에서 따라 나온 추론이고, 겹침을 실제로 30분 유지하며 그 사이 발급된 refresh token 이 제거 뒤에 어떻게 되는지는 측정하지 않았다"
|
||
},
|
||
{
|
||
"id": "SSOT-b5-no-metrics-for-the-b-layer",
|
||
"kindCandidate": "QUESTION",
|
||
"sourceRefs": [
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-5"
|
||
],
|
||
"summary": "Prometheus 가 긁는 대상에 Redis·PostgreSQL·BFF 가 없어 B층 네 지표가 전부 시계열 0개다",
|
||
"disposition": "NEEDS_DECISION",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "",
|
||
"reason": "「관측은 실험 설계에 포함되어야 한다」까지가 SSOT 가 적은 것이고, exporter 를 붙일지 이 프로젝트가 정하지 않았다. 다음 검증과 종료 조건이 없으면 Open Question 의 칸을 못 채운다"
|
||
},
|
||
{
|
||
"id": "SSOT-b7a-back-calculation-breaks-under-cookie-refresh",
|
||
"kindCandidate": "REFERENCE",
|
||
"sourceRefs": [
|
||
"final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-7a"
|
||
],
|
||
"summary": "TTL 역산 규칙은 `refresh:disabled` 에서만 유효하고 `--cookie-refresh` 를 켜면 산 세션이 고아로 오판된다",
|
||
"disposition": "KEEP_IN_SSOT",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "",
|
||
"reason": "규칙의 적용 범위이지 따로 읽을 규칙이 아니다. B-7a 절차의 첫 단계가 그 전제를 확인하는 일이라 거기서 읽혀야 뜻이 있고, 켠 뒤 어떻게 어긋나는지는 재지도 않았다"
|
||
},
|
||
{
|
||
"id": "SSOT-reproduce-c1-multi-app-sso",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#c층-재현-절차-두-편을-직접-치는-순서-c-1"
|
||
],
|
||
"summary": "두 앱에 차례로 들어가 SSO 상태를 만든 뒤 IdP 세션만 끊고 앱 세션이 남는지 본다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-c1-multi-app-sso",
|
||
"reason": "SSOT 가 이 편을 명령·예상 출력·되돌리기까지 갖춘 한 줄기 절차로 담고 있고, 읽는 사람이 그대로 쳐야 성립한다. 발견은 C·D층 Case 가 이미 담으므로 겹치지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-reproduce-c2-backchannel-logout",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#c층-재현-절차-두-편을-직접-치는-순서-c-2"
|
||
],
|
||
"summary": "클라이언트 속성을 백업하고 보낼 주소만 넣은 뒤 로그아웃이 퍼지는지 세 후보로 가른다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-c2-backchannel-logout",
|
||
"reason": "SSOT 가 이 편을 명령·예상 출력·되돌리기까지 갖춘 한 줄기 절차로 담고 있고, 읽는 사람이 그대로 쳐야 성립한다. 발견은 C·D층 Case 가 이미 담으므로 겹치지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-c-layer-shared-preconditions",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#c층-재현-절차-두-편을-직접-치는-순서"
|
||
],
|
||
"summary": "C층 두 편의 공통 전제 — 앱이 둘 필요하고, app2 는 Grafana 에서 빌린 이름이며, 브라우저가 있어야 한다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-c1-multi-app-sso",
|
||
"reason": "두 편에 공통이지만 읽는 사람이 그 준비를 처음 하는 곳은 C-1 하나다. 떼어 놓으면 명령이 없는 전제 목록만 남고, 빌린 이름을 언제 돌려주는가도 두 편의 순서에 달려 있어 절차 안에서만 뜻이 선다"
|
||
},
|
||
{
|
||
"id": "SSOT-c-layer-marker-convention",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#c층-재현-절차-두-편을-직접-치는-순서"
|
||
],
|
||
"summary": "가이드가 출력에 붙인 표시 셋 — 실측 · 형태 · 미검증",
|
||
"disposition": "KEEP_IN_SSOT",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": null,
|
||
"reason": "A·B층과 같은 셋이고 SSOT 가 이미 그 뜻을 표로 적어 두었다. 두 편이 각자 그 규약을 다시 쓰므로 독립 기록으로 떼면 같은 말이 세 번째로 늘어난다"
|
||
},
|
||
{
|
||
"id": "SSOT-c-layer-premise-says-sudo-kubectl",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#c층-재현-절차-두-편을-직접-치는-순서"
|
||
],
|
||
"summary": "두 편의 전제가 모두 `sudo kubectl` 인데 같은 폴더 README 는 반대로 적고, 본문 명령 블록은 `sudo kubectl` 을 한 번도 쓰지 않는다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:the-guides-broke-at-the-first-command",
|
||
"reason": "A층과 B층에서 이미 같은 결함을 관측해 Case 로 담았고, C층은 그 결함이 한 층 더 남아 있다는 같은 사실의 세 번째 표본이다. 표본이 하나 늘었다고 기록이 하나 늘 이유는 없다"
|
||
},
|
||
{
|
||
"id": "SSOT-c1-user-session-and-client-session-are-two-layers",
|
||
"kindCandidate": "CONCEPT",
|
||
"sourceRefs": [
|
||
"final/document.md#c층-재현-절차-두-편을-직접-치는-순서-c-1"
|
||
],
|
||
"summary": "Keycloak 은 세션을 user session(사람 하나)과 client session(그 사람이 쓰는 앱 하나)으로 나눠 두고, A-3 은 앞엣것을 B-3 은 뒤엣것을 지웠다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-c1-multi-app-sso",
|
||
"reason": "이 구조가 쓰이는 곳은 「주입이 걸렸는가」를 `client_sessions 1 → 2` 로 판정하는 한 곳뿐이고, C-1 절차가 그 자리에서 표로 갈라 적는다. 메커니즘만 떼면 읽을 사람이 없다"
|
||
},
|
||
{
|
||
"id": "SSOT-c1-counting-sessions-without-the-realm-means-nothing",
|
||
"kindCandidate": "REFERENCE",
|
||
"sourceRefs": [
|
||
"final/document.md#c층-재현-절차-두-편을-직접-치는-순서-c-1"
|
||
],
|
||
"summary": "`offline_user_session` 에는 모든 realm 의 세션이 들어 있어 realm 을 조인하지 않은 세션 수는 결론을 정반대로 읽게 한다",
|
||
"disposition": "KEEP_IN_SSOT",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": null,
|
||
"reason": "이 실험대의 테이블 모양에 달린 세는 법이지 다음 프로젝트에 줄 규칙이 아니다. C-1 절차가 「세는 법을 여기서 고친다」를 한 단계로 두고 있어 거기서 읽혀야 뜻이 있다"
|
||
},
|
||
{
|
||
"id": "SSOT-c1-two-screenshots-are-the-same-file",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#c층-재현-절차-두-편을-직접-치는-순서-c-1"
|
||
],
|
||
"summary": "두 시점의 브라우저 증거가 md5 `2c703176…` 로 동일한 파일이라 시점을 구별하는 증거가 되지 못한다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-c1-multi-app-sso",
|
||
"reason": "화면이 같아 보인다는 것 자체가 이 편의 결론이라 증거로는 못 쓴다는 주의이고, 그 주의가 필요한 곳은 판정을 터미널 출력으로 옮기는 C-1 절차 안이다. 조작이 아니므로 결함 Case 로도 서지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-c1-when-does-the-app-session-actually-break",
|
||
"kindCandidate": "QUESTION",
|
||
"sourceRefs": [
|
||
"final/document.md#c층-재현-절차-두-편을-직접-치는-순서-c-1"
|
||
],
|
||
"summary": "IdP 세션을 지운 뒤 앱 세션이 실제로 언제 끊기는지를 기다려서 확인하지 않았다",
|
||
"disposition": "NEEDS_EVIDENCE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": null,
|
||
"reason": "`Session not active` 가 나온다는 것은 세 수명에서 따라 나온 추론이고, 수명 두 값을 읽는 줄도 가이드가 미검증으로 표시했다. 기다려서 재기 전에는 Open Question 의 다음 검증과 종료 조건을 채울 수 없다"
|
||
},
|
||
{
|
||
"id": "SSOT-c2-nobody-implemented-either-side",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#c층-재현-절차-두-편을-직접-치는-순서-c-2"
|
||
],
|
||
"summary": "보낼 주소도 받을 엔드포인트도 없었고, 한쪽만 고치면 여전히 안 퍼진다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:nobody-implemented-backchannel-logout",
|
||
"reason": "그 Case 가 이 발견을 담는 곳이고 네 가지를 순서대로 확인해 닫는 구조까지 같다. 재현 절차 쪽은 setup:reproduce-c2-backchannel-logout 이 받는다"
|
||
},
|
||
{
|
||
"id": "SSOT-c2-evidence-file-does-not-match-the-printed-output",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#c층-재현-절차-두-편을-직접-치는-순서-c-2"
|
||
],
|
||
"summary": "설정이 들어간 것을 확인한 두 줄이 `02-configure-idp.txt` 에 없다 — 그 파일은 점 표기 실패로 끝난다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-c2-backchannel-logout",
|
||
"reason": "증거 파일과 인쇄된 값이 1:1 이 아닌 유일한 곳이고, 처방이 「따라 하는 사람은 지금 직접 재 둔다」라 쓰이는 곳이 C-2 의 주입 검증 절 하나다. 떼면 처방 없는 지적만 남는다"
|
||
},
|
||
{
|
||
"id": "SSOT-c2-zero-log-lines-prove-nothing",
|
||
"kindCandidate": "REFERENCE",
|
||
"sourceRefs": [
|
||
"final/document.md#c층-재현-절차-두-편을-직접-치는-순서-c-2"
|
||
],
|
||
"summary": "로그의 `backchannel` 0줄은 「Keycloak 이 안 보냈다」가 아니라 「기본 로그 레벨에서는 안 보인다」이다",
|
||
"disposition": "NEEDS_EVIDENCE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": null,
|
||
"reason": "`DEBUG` 를 켜서 다시 재지 않았다. 「0줄로는 단정하지 않는다」까지가 SSOT 가 적은 것이고, 무엇을 켜면 보이는지를 재기 전에는 적용 조건과 예외를 댈 수 없다"
|
||
},
|
||
{
|
||
"id": "SSOT-c2-hairpin-200-is-a-property-of-this-lab",
|
||
"kindCandidate": "REFERENCE",
|
||
"sourceRefs": [
|
||
"final/document.md#c층-재현-절차-두-편을-직접-치는-순서-c-2"
|
||
],
|
||
"summary": "클러스터 안에서 공개 이름이 되돌아온 `HTTP 200` 은 tailnet 과 split DNS 구성 덕이고 운영에서는 안 되는 경우가 흔하다",
|
||
"disposition": "KEEP_IN_SSOT",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": null,
|
||
"reason": "이 판정의 유효 범위이지 따로 읽을 규칙이 아니다. 도달성을 재는 단계 바로 옆에서 읽혀야 「200 이 나왔으니 운영도 된다」로 넘어가는 것을 막고, 운영에서 어떻게 갈리는지는 재지도 않았다"
|
||
},
|
||
{
|
||
"id": "SSOT-reproduce-d1-backup-restore",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서-d-1"
|
||
],
|
||
"summary": "덤프를 뜨고 검증 넷을 통과한 뒤 스키마를 통째로 지우고 같은 명령으로 복구를 대조한다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-d1-backup-restore",
|
||
"reason": "SSOT 가 이 편을 명령·예상 출력·되돌리기까지 갖춘 한 줄기 절차로 담고 있고, 읽는 사람이 그대로 쳐야 성립한다. 발견은 C·D층 Case 가 이미 담으므로 겹치지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-reproduce-d2-version-upgrade",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서-d-2"
|
||
],
|
||
"summary": "`databasechangelog` 행 수를 먼저 세고 태그를 정방향·롤백·역방향으로 세 번 바꾼다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-d2-version-upgrade",
|
||
"reason": "SSOT 가 이 편을 명령·예상 출력·되돌리기까지 갖춘 한 줄기 절차로 담고 있고, 읽는 사람이 그대로 쳐야 성립한다. 발견은 C·D층 Case 가 이미 담으므로 겹치지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-reproduce-d3-secret-exposure",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서-d-3"
|
||
],
|
||
"summary": "카나리아 Secret 을 심고 API·노드 디스크·파드 안·RBAC 네 경로를 하나씩 연다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-d3-secret-exposure",
|
||
"reason": "SSOT 가 이 편을 명령·예상 출력·되돌리기까지 갖춘 한 줄기 절차로 담고 있고, 읽는 사람이 그대로 쳐야 성립한다. 발견은 C·D층 Case 가 이미 담으므로 겹치지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-reproduce-d4-certificate-renewal",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서-d-4"
|
||
],
|
||
"summary": "감시 셋과 시계 왜곡을 먼저 잡고 강제 갱신을 한 번 쳐서 디스크와 네트워크가 갈리는 것을 잰다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-d4-certificate-renewal",
|
||
"reason": "SSOT 가 이 편을 명령·예상 출력·되돌리기까지 갖춘 한 줄기 절차로 담고 있고, 읽는 사람이 그대로 쳐야 성립한다. 발견은 C·D층 Case 가 이미 담으므로 겹치지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-reproduce-d4a-deploy-hook",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서-d-4a"
|
||
],
|
||
"summary": "`deploy/` 에 두 줄짜리 훅을 설치하고 다시 강제 갱신해 워커 PID 와 SCT 로 1~2초를 잰다",
|
||
"disposition": "PROMOTE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-d4a-deploy-hook",
|
||
"reason": "SSOT 가 이 편을 명령·예상 출력·되돌리기까지 갖춘 한 줄기 절차로 담고 있고, 읽는 사람이 그대로 쳐야 성립한다. 발견은 C·D층 Case 가 이미 담으므로 겹치지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-d-layer-marker-convention",
|
||
"kindCandidate": "SETUP",
|
||
"sourceRefs": [
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서"
|
||
],
|
||
"summary": "가이드가 출력에 붙인 표시 셋 — 실측 · 형태 · 미검증",
|
||
"disposition": "KEEP_IN_SSOT",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": null,
|
||
"reason": "A·B·C층과 같은 셋이고 SSOT 가 이미 그 뜻을 표로 적어 두었다. 다섯 편이 각자 그 규약을 다시 쓰므로 독립 기록으로 떼면 같은 말이 네 번째로 늘어난다"
|
||
},
|
||
{
|
||
"id": "SSOT-d-layer-premise-says-sudo-kubectl",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서"
|
||
],
|
||
"summary": "다섯 편의 전제도 `sudo kubectl` 인데 같은 폴더 README 는 반대로 적고, D-1 만 「kubeconfig 를 홈에 복사해 뒀다면 빼도 된다」는 괄호를 스스로 달았다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:the-guides-broke-at-the-first-command",
|
||
"reason": "A·B·C층에서 이미 같은 결함을 관측해 Case 로 담았고, D층은 네 번째 표본이다. D-1 이 괄호를 단 것도 같은 결함이 각 편에서 다르게 새어 나온 모양이지 새 결론이 아니다"
|
||
},
|
||
{
|
||
"id": "SSOT-d1-restore-without-i-is-indistinguishable-from-success",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서",
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서-d-1"
|
||
],
|
||
"summary": "`kubectl exec` 에 `-i` 를 빼면 `psql` 이 빈 입력을 받고 정상 종료해 오류도 종료 코드도 없이 「1초 만에 복구됐다」로 찍힌다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:nine-injections-that-silently-did-nothing",
|
||
"reason": "조용히 실패하고 아무 일도 없는 것처럼 보이는 같은 고장이고, D층에서는 그것이 주입이 아니라 복구 쪽에서 온다는 것만 다르다. 그 Case 의 표가 이미 같은 판정 기준을 갖고 있다"
|
||
},
|
||
{
|
||
"id": "SSOT-d1-empty-database-still-answers-200",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서-d-1"
|
||
],
|
||
"summary": "데이터베이스를 통째로 비웠는데 파드는 `1/1 Running` 이고 정문이 `200` 이며 로그는 `WARN` 이다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "concept:the-up-metric-cannot-see-alive-but-useless",
|
||
"reason": "살아 있지만 쓸모없는 상태를 프로브가 못 보는 같은 성질이고, 헬스체크가 커넥션만 보기 때문에 빈 데이터베이스를 통과시킨다는 설명이 그 Concept 의 몫이다. 「토큰 발급만 정직한 지표다」도 같은 문장의 뒷면이다"
|
||
},
|
||
{
|
||
"id": "SSOT-d1-the-dump-sits-in-the-same-failure-domain",
|
||
"kindCandidate": "REFERENCE",
|
||
"sourceRefs": [
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서-d-1"
|
||
],
|
||
"summary": "덤프가 DB 와 같은 기계에 있으면 백업이 아니다 — 진짜 요건은 다른 기계가 아니라 다른 장애 도메인이다",
|
||
"disposition": "NEEDS_EVIDENCE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": null,
|
||
"reason": "이 실험대는 덤프를 옮기는 마지막 단계를 하지 않았고 그 두 줄도 미검증이다. 덤프는 DB 와 같은 기계에 남았으므로, 규칙으로 올리려면 옮긴 뒤 그 파일로 실제로 복구해 봐야 한다"
|
||
},
|
||
{
|
||
"id": "SSOT-d2-the-conclusion-was-corrected-by-a-second-run",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서-d-2"
|
||
],
|
||
"summary": "「롤백은 안 된다」고 단정했다가 후속 실행에서 「행 수가 안 바뀌면 된다」로 정정했다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:the-upgrade-that-would-not-roll-back",
|
||
"reason": "그 Case 가 이미 「그 판정은 조건부였다」로 닫고 정정 자체를 담고 있다. 재현 절차 쪽은 setup:reproduce-d2-version-upgrade 가 받는다"
|
||
},
|
||
{
|
||
"id": "SSOT-d2-schema-growth-blocks-a-tag-rollback",
|
||
"kindCandidate": "REFERENCE",
|
||
"sourceRefs": [
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서-d-2"
|
||
],
|
||
"summary": "`databasechangelog` 행 수가 늘면 이미지 태그를 되돌리는 것만으로는 못 돌아온다",
|
||
"disposition": "NEEDS_EVIDENCE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": null,
|
||
"reason": "역방향(26.0)에서 관측한 체크섬 실패를 근거로 한 추론이며(inferred) 실제로 행 수가 늘어난 뒤 되돌려 본 적이 없다. 26.7.x 사이에는 스키마 변경이 없어 이 실험대에서는 재현하지 못했다"
|
||
},
|
||
{
|
||
"id": "SSOT-d3-node-disk-holds-the-plaintext",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서-d-3"
|
||
],
|
||
"summary": "k3s 의 저장 파일 `state.db` 안에서 비밀번호 평문이 두 번 잡히고 저장소 암호화는 켠 적이 없다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-d3-secret-exposure",
|
||
"reason": "앞선 분해가 D-3 의 base64 관측을 KEEP_IN_SSOT 로 두어 이 발견을 담을 Case 가 없고, 노드 디스크 경로는 그 관측의 두 번째 겹이라 새 기록을 세우면 같은 주장이 두 곳에서 갈린다. 절차 안에서는 ②의 판정 출력으로 그대로 읽힌다"
|
||
},
|
||
{
|
||
"id": "SSOT-d3-a-grep-that-returns-zero-is-not-absence",
|
||
"kindCandidate": "REFERENCE",
|
||
"sourceRefs": [
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서-d-3"
|
||
],
|
||
"summary": "같은 파일에 같은 명령을 걸었는데 키에 따라 `2` 와 `0` 이 나왔고, `0` 은 「이 파일의 이 시점에 이 형태로는 못 찾았다」이다",
|
||
"disposition": "NEEDS_EVIDENCE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": null,
|
||
"reason": "`0` 이 나온 원인을 이 실험이 가리지 않았고 `-wal`·`strings` 로 다시 보는 두 줄도 미검증이다. 왜 안 나왔는지를 재기 전에는 규칙의 적용 조건과 예외를 댈 수 없다"
|
||
},
|
||
{
|
||
"id": "SSOT-d3-deleting-a-secret-does-not-erase-the-file",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서-d-3"
|
||
],
|
||
"summary": "Secret 을 지워도 저장 파일에서 바로 사라지지는 않는다 — 그래서 유출에 필요한 것은 삭제가 아니라 회전이다",
|
||
"disposition": "NEEDS_EVIDENCE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": null,
|
||
"reason": "이 실험은 삭제 후를 재지 않았고 그 두 줄을 미검증으로 표시했다. 어느 쪽이 나오든 관찰 절의 결론은 안 바뀌지만, 「지워도 남는다」를 독립된 관측으로 적으려면 지운 뒤 실제로 세야 한다"
|
||
},
|
||
{
|
||
"id": "SSOT-d4-non-interactive-sudo-cannot-be-automated",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서-d-4",
|
||
"final/document.md#문제를-어렵게-만든-제약-게스트와-호스트의-sudo-가-다르다"
|
||
],
|
||
"summary": "호스트에서 `sudo -n` 은 반드시 `a password is required` 로 실패하고, 그 빈 출력을 「설정이 없다」로 읽으면 진단이 통째로 틀린다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-d4-certificate-renewal",
|
||
"reason": "이 사실이 쓰이는 곳은 「어느 네 줄을 사람이 직접 치는가」 하나뿐이고, D-4 절차가 그것을 표로 갈라 적는다. 이 실험이 처음에 강제 갱신을 미측정으로 남긴 까닭도 같은 표 안에서만 뜻이 선다"
|
||
},
|
||
{
|
||
"id": "SSOT-d4-nginx-error-log-truncates-at-2048-bytes",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서-d-4"
|
||
],
|
||
"summary": "nginx 에러 로그는 한 항목이 `NGX_MAX_ERROR_STR` 2048바이트에서 잘리고 저널 포맷을 바꿔도 안 늘어난다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-d4-certificate-renewal",
|
||
"reason": "막히는 증상 하나의 원인과 우회(access 로그를 본다)이고 쓰이는 곳이 그 한 곳이다. 떼면 어디서 만나는 문제인지가 빠진 상식 한 줄이 된다"
|
||
},
|
||
{
|
||
"id": "SSOT-d4-crt-sh-indexes-a-subset-of-the-truth",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서-d-4"
|
||
],
|
||
"summary": "서빙 중인 인증서에는 SCT 가 둘 박혀 있는데 crt.sh 조회는 `[]` 를 돌려준다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "concept:the-up-metric-cannot-see-alive-but-useless",
|
||
"reason": "관측 도구가 진실의 부분집합만 본다는 같은 함정이고 SSOT 자신이 A-2 의 `up` 지표와 같은 종류라고 적는다. 그 Concept 이 이미 그 성질을 설명하는 곳이다"
|
||
},
|
||
{
|
||
"id": "SSOT-d4-2199-was-corrected-to-2305",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서-d-4",
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서-d-4a"
|
||
],
|
||
"summary": "디스크 mtime(호스트 시계)과 일련번호 관측(개발 머신 시계)을 그대로 빼서 2199초로 적었다가 106초를 보정해 2305초로 고쳤다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "reference:never-subtract-values-from-two-clocks",
|
||
"reason": "그 Reference 가 담는 규칙의 실제 사례이고, 같은 106초가 D-4 에서는 결론을 안 바꾸고 D-4a 에서는 뒤집는다는 것이 그 규칙의 적용 조건이다. 사례를 따로 세우면 규칙과 예시가 두 곳으로 갈린다"
|
||
},
|
||
{
|
||
"id": "SSOT-d4a-ran-with-error-output-is-not-a-failure",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서-d-4a"
|
||
],
|
||
"summary": "certbot 의 `Hook 'deploy-hook' ran with error output:` 은 훅이 stderr 에 무엇이라도 쓰면 붙는 문구이고 종료 코드를 말하지 않는다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "reference:judge-a-reload-by-the-worker-pid-not-the-log",
|
||
"reason": "「적용됐는지는 로그 문구가 아니라 상태로 판정한다」가 그 Reference 이고 이 문구가 그 규칙이 막는 오독의 사례다. 로그에서 `error` 를 긁는 감시가 성공한 훅을 실패로 읽는다는 것까지 같은 규칙 안에 든다"
|
||
},
|
||
{
|
||
"id": "SSOT-d4a-notbefore-is-not-the-issue-time",
|
||
"kindCandidate": "CONCEPT",
|
||
"sourceRefs": [
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서-d-4a"
|
||
],
|
||
"summary": "Let's Encrypt 는 `notBefore` 를 정확히 한 시간 백데이트하고, 한 시간을 더한 값도 SCT 보다 일관되게 약 89초 늦다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-d4a-deploy-hook",
|
||
"reason": "이 사실이 쓰이는 곳은 「발급 시각의 기준을 무엇으로 잡는가」 하나뿐이고, 약 89초 차이의 원인은 이 실험이 규명하지 않았다. 메커니즘만 떼면 설명할 수 없는 상수 하나가 남는다"
|
||
},
|
||
{
|
||
"id": "SSOT-d4a-timer-path-is-still-unmeasured",
|
||
"kindCandidate": "QUESTION",
|
||
"sourceRefs": [
|
||
"final/document.md#d층-재현-절차-다섯-편을-직접-치는-순서-d-4a"
|
||
],
|
||
"summary": "훅은 `--force-renewal` 로만 검증했고 타이머가 스스로 갱신하는 경로는 만료 30일 전에야 조건이 성립한다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "question:does-the-renewal-timer-actually-renew",
|
||
"reason": "같은 미지수를 이미 Open Question 으로 담고 있고, D-4a 가 더한 것은 그날이 오면 볼 두 줄까지다. 질문을 하나 더 세우면 같은 종료 조건이 두 곳에 적힌다"
|
||
},
|
||
{
|
||
"id": "SSOT-concepts-layer-to-topic-map",
|
||
"kindCandidate": "REFERENCE",
|
||
"sourceRefs": [
|
||
"final/document.md#실험대가-쓴-개념-조사한-것-여덟-층이-받치는-것"
|
||
],
|
||
"summary": "여덟 층과 계약의 주제 여섯을 짝지은 대응표, 그리고 실험 문서에서 이름만 쓰이고 설명된 적이 없는 용어가 24개라는 계수",
|
||
"disposition": "KEEP_IN_SSOT",
|
||
"dispositionReview": "CONFIRMED",
|
||
"reason": "분해 계약이 이미 선 뒤에 그 계약을 설명하려고 쓴 자리이고, 24개는 조사 범위를 센 값이다. 「구현 클래스 51개를 전부 읽었다」와 같은 꼴이라 독립 기록으로 읽을 사람이 없다"
|
||
},
|
||
{
|
||
"id": "SSOT-concepts-l0-guest-is-a-host-process",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#실험대가-쓴-개념-조사한-것-0층-가상화"
|
||
],
|
||
"summary": "게스트는 호스트에서 qemu-system 프로세스 하나이고 virsh destroy 는 ACPI 신호 없이 그 프로세스를 끊는다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:two-ways-to-lose-a-node",
|
||
"reason": "A-4 에서 노드를 뽑은 뒤 45초 동안 아무 일도 없었던 까닭이 이것이다. 노드를 잃는 두 경로를 담은 그 Case 의 한 절로 들어간다. 같은 층의 인터페이스 이름(enp1s0)과 virtio 시드 디스크는 이미 nine-injections Case 와 a6 Setup 이 흡수했다"
|
||
},
|
||
{
|
||
"id": "SSOT-concepts-l0-memory-reads-differently-in-three-places",
|
||
"kindCandidate": "CONCEPT",
|
||
"sourceRefs": [
|
||
"final/document.md#실험대가-쓴-개념-조사한-것-0층-가상화"
|
||
],
|
||
"summary": "호스트 RSS · 게스트 available · kubectl top 이 같은 메모리를 다르게 내고, 상한을 바꾸려면 껐다 켜야 하며 게스트에는 swap 을 두지 않았다",
|
||
"disposition": "KEEP_IN_SSOT",
|
||
"dispositionReview": "CONFIRMED",
|
||
"reason": "이 실험대의 자원 배치 사실이라 독립 기록이 되지 않는다. 같은 층의 정의는 virtualization 프로젝트가 concept:memory-pressure-reclaim-swap-oom 과 question:qemu-resident-memory-distribution 으로 담고 있어 두 프로젝트가 같은 것을 두 번 올리지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-concepts-l0-what-is-under-this-layer",
|
||
"kindCandidate": "CONCEPT",
|
||
"sourceRefs": [
|
||
"final/document.md#실험대가-쓴-개념-조사한-것-0층-가상화"
|
||
],
|
||
"summary": "KVM 의 ioctl 층과 struct kvm_run, VMX root 와 non-root, virtio 의 virtqueue 와 전송 계층, vhost-user, VFIO 의 IOMMU 그룹과 DMA 리매핑",
|
||
"disposition": "KEEP_IN_SSOT",
|
||
"dispositionReview": "CONFIRMED",
|
||
"reason": "SSOT 가 스스로 「이 실험대에서 잰 것이 아니라 커널·Intel·QEMU 문서를 읽은 결과」라고 적고 패스스루는 쓴 적조차 없다고 적는다. 이 자리의 정의는 virtualization 프로젝트의 SSOT 가 정본이고 concept:kvm-vcpu-to-physical-cpu · guest-packet-path-to-physical-nic · guest-block-io-path-to-virtqueue 가 같은 것을 담는다. 저쪽이 정의이고 이쪽이 측정이라 이쪽에서 글감으로 올리지 않는다"
|
||
},
|
||
{
|
||
"id": "SSOT-concepts-l1-systemd-unit-semantics",
|
||
"kindCandidate": "CONCEPT",
|
||
"sourceRefs": [
|
||
"final/document.md#실험대가-쓴-개념-조사한-것-1층-리눅스와-systemd"
|
||
],
|
||
"summary": "Type= 이 「떴다」를 판정하고 KillMode·KillSignal 이 멈추는 방식을 정하며, systemctl cat 에 적힌 것과 systemctl show 가 내는 적용값이 다르다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "reference:judge-a-reload-by-the-worker-pid-not-the-log",
|
||
"reason": "이 층이 받치는 기록들이 이미 필요한 만큼씩 흡수했다 — deploy 훅 Case 가 Restart=on-failure·RestartUSec=100ms·StartLimitBurst=5 와 KillMode=mixed·SIGQUIT 을, d4 Setup 이 systemctl cat 대 show 와 Type=forking 을, 이 Reference 가 마스터·워커 PID 와 cgroup.procs 를 들고 있다. 남은 slice 와 PrivateTmp 는 절 하나 분량이라 그 Reference 의 배경 절로 들어간다"
|
||
},
|
||
{
|
||
"id": "SSOT-concepts-l1-pid1-ignores-sigkill-from-its-own-namespace",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#실험대가-쓴-개념-조사한-것-1층-리눅스와-systemd"
|
||
],
|
||
"summary": "A-3 에서 컨테이너 안으로 보낸 kill -9 1 이 아무 일도 하지 않은 것은 PID 1 이 자기 네임스페이스에서 온 SIGKILL 을 무시하기 때문이다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:nine-injections-that-silently-did-nothing",
|
||
"reason": "아홉 건의 조용한 실패 중 하나이고 그 Case 의 표에 SSOT-a3-two-clean-zero-loss-results-that-were-not-kills 로 이미 들어가 있다. 이 설명은 그 행의 원인 칸이다"
|
||
},
|
||
{
|
||
"id": "SSOT-concepts-l1-journald-already-held-the-b7-cause",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#실험대가-쓴-개념-조사한-것-1층-리눅스와-systemd"
|
||
],
|
||
"summary": "B-7 이 계층을 나눠 좁힌 502 의 원인을 호스트 nginx 가 저널에 upstream sent too big header 로 적어 두었고, 증거 파일 147개 중 그 줄을 담은 것은 없다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "reference:verify-the-injection-landed-separately-from-the-result",
|
||
"reason": "관측 지점을 늘리라는 규칙에 빠져 있던 네 번째 자리다 — 외부·지표·데이터베이스는 적었는데 호스트 계층 저널이 없었다. 그 Reference 가 이미 세 지점을 적고 있어 행 하나로 들어간다"
|
||
},
|
||
{
|
||
"id": "SSOT-concepts-l2-why-a-correct-rule-never-sees-the-packet",
|
||
"kindCandidate": "CONCEPT",
|
||
"sourceRefs": [
|
||
"final/document.md#실험대가-쓴-개념-조사한-것-2층-네트워크"
|
||
],
|
||
"summary": "conntrack 이 ESTABLISHED 를 먼저 판정하고, netfilter 는 raw 를 conntrack 앞에 돌리며, kube-router 가 자기 체인을 FORWARD 맨 위에 다시 끼우고, flannel VXLAN 이 파드 IP 를 캡슐 안에 감춘다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:nine-injections-that-silently-did-nothing",
|
||
"reason": "아홉 실패 중 넷의 원인이고, 그 Case 본문이 conntrack 표 원문과 nf_conntrack_tcp_timeout_established 86400 과 패킷 카운터 0 과 flannel.1 을 이미 같은 순서로 설명한다. raw PREROUTING 으로 옮긴 절차는 a5 Setup 이 담는다. 독립 Concept 으로 올리면 같은 문장이 두 곳에 생긴다"
|
||
},
|
||
{
|
||
"id": "SSOT-concepts-l3-commit-returns-before-the-flush",
|
||
"kindCandidate": "CONCEPT",
|
||
"sourceRefs": [
|
||
"final/document.md#실험대가-쓴-개념-조사한-것-3층-postgresql"
|
||
],
|
||
"summary": "WAL 을 먼저 쓰고, synchronous_commit 이 그 flush 를 기다릴지 정하며, wal_writer_delay 가 잃을 수 있는 창의 크기를 정한다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:four-logins-that-returned-200-and-vanished",
|
||
"reason": "그 Case 가 SET LOCAL synchronous_commit TO OFF 포획과 wal_writer_delay 200ms 실측과 not properly shut down·redo starts 재생 로그를 이미 본문에 담고 있다. 설명을 떼어 내면 200 을 받은 네 건이 어디로 사라졌는지 읽는 순서가 끊긴다"
|
||
},
|
||
{
|
||
"id": "SSOT-concepts-l3-write-is-not-durable-without-fsync",
|
||
"kindCandidate": "CONCEPT",
|
||
"sourceRefs": [
|
||
"final/document.md#실험대가-쓴-개념-조사한-것-3층-postgresql"
|
||
],
|
||
"summary": "write() 는 페이지 캐시까지이고 디스크에 닿게 하려면 fsync 가 따로 필요한데, B-5 는 그 한 단계 밖에서 /data 가 컨테이너 파일시스템이라 함께 사라졌다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:persistence-without-a-volume-and-rotation-without-overlap",
|
||
"reason": "그 Case 의 결론이 「볼륨 없는 영속화」이고 이 설명이 그 결론의 전제다. 한 절로 들어간다"
|
||
},
|
||
{
|
||
"id": "SSOT-concepts-l3-liquibase-checksum-blocks-a-downgrade",
|
||
"kindCandidate": "CONCEPT",
|
||
"sourceRefs": [
|
||
"final/document.md#실험대가-쓴-개념-조사한-것-3층-postgresql"
|
||
],
|
||
"summary": "Liquibase 가 changeset 체크섬을 databasechangelog 에 남기고 기동할 때 파일과 대조하므로, 새 버전이 남긴 행을 옛 버전이 읽으면 멈춘다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:the-upgrade-that-would-not-roll-back",
|
||
"reason": "그 Case 가 ValidationFailedException 원문과 「행 수가 안 바뀌면 롤백된다」는 판정 기준을 이미 담고 있다"
|
||
},
|
||
{
|
||
"id": "SSOT-concepts-l3-optimistic-lock-never-fired-because-login-inserts",
|
||
"kindCandidate": "CONCEPT",
|
||
"sourceRefs": [
|
||
"final/document.md#실험대가-쓴-개념-조사한-것-3층-postgresql"
|
||
],
|
||
"summary": "A-6 이 예측한 낙관적 락 충돌이 0건이었던 것은 로그인이 세션 행을 INSERT 하지 UPDATE 하지 않아 경합할 대상이 없었기 때문이다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:200ms-of-delay-became-22-seconds",
|
||
"reason": "그 Case 가 「예측 두 개 가운데 하나는 틀렸다 · 낙관적 락 충돌 0건 · 로그인은 INSERT 라 경합할 대상이 없다」를 이미 본문에 담고 있다"
|
||
},
|
||
{
|
||
"id": "SSOT-concepts-l4-two-timers-before-a-pod-moves",
|
||
"kindCandidate": "CONCEPT",
|
||
"sourceRefs": [
|
||
"final/document.md#실험대가-쓴-개념-조사한-것-4층-쿠버네티스"
|
||
],
|
||
"summary": "node-monitor-grace-period 와 tolerationSeconds 를 더한 340초는 계산이고 실제 축출은 240~270초였으며, 죽은 노드의 파드는 보고할 주체가 없어 Running 으로 남고 StatefulSet 은 이름을 못 비워 대체를 만들지 않는다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:two-ways-to-lose-a-node",
|
||
"reason": "셋 다 A-4 의 관측이고 case:two-ways-to-lose-a-node 와 concept:readiness-hides-the-broken-node 와 case:ghost-rows-are-cleaned-by-the-surviving-coordinator 가 나눠 담고 있다. 340초가 계산이라는 단서도 그 Case 에 이미 붙어 있다"
|
||
},
|
||
{
|
||
"id": "SSOT-concepts-l4-allowlist-policy-and-injected-service-env",
|
||
"kindCandidate": "CONCEPT",
|
||
"sourceRefs": [
|
||
"final/document.md#실험대가-쓴-개념-조사한-것-4층-쿠버네티스"
|
||
],
|
||
"summary": "NetworkPolicy 는 허용 목록이라 7800 은 빼는 방식으로만 막히고 9000 을 빠뜨리면 분단이 아니라 죽은 파드를 재게 되며, enableServiceLinks 가 REDIS_PORT 를 tcp:// URL 로 덮어쓴다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "setup:reproduce-a1-jgroups-transport-block",
|
||
"reason": "앞엣것은 a1 Setup 의 절차 그 자체이고, 뒤엣것은 이미 SSOT-b1-service-links-injects-redis-port 로 b1 Setup 에 들어가 있다"
|
||
},
|
||
{
|
||
"id": "SSOT-concepts-l5-rotation-invalidates-the-whole-sso-session",
|
||
"kindCandidate": "CONCEPT",
|
||
"sourceRefs": [
|
||
"final/document.md#실험대가-쓴-개념-조사한-것-5층-keycloak"
|
||
],
|
||
"summary": "revokeRefreshToken 이 켜진 상태에서 재사용이 감지되면 그 토큰만이 아니라 SSO 세션 전체가 무효화되고, 세션은 SSO·클라이언트·애플리케이션 세 겹이다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:the-winner-of-the-rotation-race-also-loses",
|
||
"reason": "그 Case 의 결론이 이 규격 그대로라 「이긴 요청의 토큰도 못 쓴다」가 설명된다. 세 겹 구분은 이미 SSOT-c1-user-session-and-client-session-are-two-layers 로 c1 Setup 에 들어가 있다"
|
||
},
|
||
{
|
||
"id": "SSOT-concepts-l5-discovery-transport-scope-and-backchannel",
|
||
"kindCandidate": "CONCEPT",
|
||
"sourceRefs": [
|
||
"final/document.md#실험대가-쓴-개념-조사한-것-5층-keycloak"
|
||
],
|
||
"summary": "디스커버리는 JGROUPS_PING 이고 트랜스포트는 TCP 7800 이며, refresh 만 CLIENT_SCOPE_CLIENT 를 조회하고, 백채널 로그아웃은 IdP 와 앱 양쪽이 있어야 한다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:session-sharing-is-the-database-not-replication",
|
||
"reason": "셋이 각각 그 Case 와 case:cache-temperature-decides-the-outcome 과 case:nobody-implemented-backchannel-logout 의 본문에 이미 있다. 한 Concept 으로 묶으면 세 Case 가 같은 설명을 밖에서 다시 읽어야 한다"
|
||
},
|
||
{
|
||
"id": "SSOT-concepts-l6-lookup-keys-primary-key-and-the-pool",
|
||
"kindCandidate": "CONCEPT",
|
||
"sourceRefs": [
|
||
"final/document.md#실험대가-쓴-개념-조사한-것-6층-spring"
|
||
],
|
||
"summary": "세션은 세션 id 로 인가된 클라이언트는 principal 이름으로 찾고, JDBC 스키마의 기본키에 세션 id 가 없으며, Redis 안의 값은 Java 네이티브 직렬화이고, agroal 이 지연을 한 번 더 곱한다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "concept:two-stores-two-lookup-keys",
|
||
"reason": "앞의 셋은 그 Concept 과 case:a-primary-key-without-the-session-id 와 b1 Setup 이 이미 원문으로 담고 있고(\\xac\\xed 까지), agroal 은 case:200ms-of-delay-became-22-seconds 가 22.2초의 두 번째 단계로 담고 있다"
|
||
},
|
||
{
|
||
"id": "SSOT-concepts-l7-hook-directory-backdate-and-sct",
|
||
"kindCandidate": "CONCEPT",
|
||
"sourceRefs": [
|
||
"final/document.md#실험대가-쓴-개념-조사한-것-7층-tls"
|
||
],
|
||
"summary": "pre·deploy·post 의 실행 조건이 다르고, Let's Encrypt 는 notBefore 를 정확히 한 시간 백데이트하며, SCT 는 CT 로그의 시계로 찍혀 제3의 기준이 된다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "decision:put-the-reload-in-a-deploy-hook",
|
||
"reason": "셋이 그 Decision 과 SSOT-d4a-notbefore-is-not-the-issue-time 과 SSOT-d4-crt-sh-indexes-a-subset-of-the-truth 로 이미 들어가 있다"
|
||
},
|
||
{
|
||
"id": "SSOT-concepts-l7-fullchain-jwks-and-the-proxy-ticket",
|
||
"kindCandidate": "CONCEPT",
|
||
"sourceRefs": [
|
||
"final/document.md#실험대가-쓴-개념-조사한-것-7층-tls"
|
||
],
|
||
"summary": "fullchain.pem 이 중간 인증서까지 담고, 모르는 kid 를 만난 NimbusJwtDecoder 는 캐시 만료를 안 기다리며, oauth2-proxy 의 티켓은 세션 ID 와 복호화 키를 함께 담아 secret 을 바꾸면 지울 대상을 잃는다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:orphan-sessions-and-the-ttl-that-finds-them",
|
||
"reason": "티켓 구조가 그 Case 의 전제이고, kid 는 case:persistence-without-a-volume-and-rotation-without-overlap 이, fullchain.pem 은 case:the-certificate-that-took-38-minutes-to-reach-the-wire 가 이미 담고 있다"
|
||
},
|
||
{
|
||
"id": "SSOT-concepts-l8-clock-skew-up-and-the-scrape-list",
|
||
"kindCandidate": "CONCEPT",
|
||
"sourceRefs": [
|
||
"final/document.md#실험대가-쓴-개념-조사한-것-8층-측정"
|
||
],
|
||
"summary": "test-server 는 NTP 를 안 쓰고 106초 빠르며, up 은 스크레이프 성공만 말해 살아 있지만 쓸모없는 상태를 못 보고, Prometheus 가 긁는 대상에 Redis·BFF·PostgreSQL 이 없다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "reference:never-subtract-values-from-two-clocks",
|
||
"reason": "셋이 그 Reference 와 concept:the-up-metric-cannot-see-alive-but-useless 와 SSOT-b5-no-metrics-for-the-b-layer 로 이미 들어가 있다"
|
||
},
|
||
{
|
||
"id": "SSOT-concepts-what-this-survey-stands-on",
|
||
"kindCandidate": "REFERENCE",
|
||
"sourceRefs": [
|
||
"final/document.md#실험대가-쓴-개념-조사한-것-이-조사가-선-근거"
|
||
],
|
||
"summary": "여기 적은 값은 대부분 시스템에서 직접 읽었고, 직접 읽을 수 없는 둘에는 출처를 달았으며, 0층의 아래쪽은 문서에서 옮긴 것이라는 근거 표시",
|
||
"disposition": "KEEP_IN_SSOT",
|
||
"dispositionReview": "CONFIRMED",
|
||
"reason": "조사 범위와 근거 표시 규약이다. 분석에는 반드시 남아야 하고 독립 기록으로 읽을 사람은 없다 — A층·B층 가이드의 (observed)·(unknown) 규약을 KEEP_IN_SSOT 로 둔 것과 같은 자리다"
|
||
},
|
||
{
|
||
"id": "SSOT-concepts-restart-on-failure-was-read-not-measured",
|
||
"kindCandidate": "QUESTION",
|
||
"sourceRefs": [
|
||
"final/document.md#실험대가-쓴-개념-조사한-것-이-조사가-선-근거"
|
||
],
|
||
"summary": "진입점 nginx 의 Restart=on-failure 는 유닛 파일을 읽어 적었을 뿐, 마스터를 죽여 100ms 안에 살아나는지와 그동안 외부 요청이 몇 건 떨어지는지를 재지 않았다",
|
||
"disposition": "NEEDS_EVIDENCE",
|
||
"dispositionReview": "CONFIRMED",
|
||
"reason": "decision:put-the-reload-in-a-deploy-hook 이 restart 대신 reload 를 고른 근거로 이 설정과 StartLimitBurst=5 를 쓰고 있어 판정이 걸려 있다. 그런데 이 실험대가 스물여섯 번 배운 것이 「설정이 그렇다고 그렇게 동작하지는 않는다」이고 SSOT 도 재지 않았다고 스스로 적는다. SSOT 가 다음 측정을 이미 적어 두었으므로 재고 나서 다시 판정한다"
|
||
},
|
||
{
|
||
"id": "SSOT-four-open-questions-carried-over",
|
||
"kindCandidate": "QUESTION",
|
||
"sourceRefs": [
|
||
"final/document.md#코드보다-먼저-드러난-문제-답할-수-없던-질문-네-개"
|
||
],
|
||
"summary": "앞선 프로젝트가 남긴 열린 질문 넷(Q1~Q4)과 게시일, 그리고 넷이 함께 서 있는 전제 — 인스턴스가 둘 이상이고 요청이 어느 쪽으로 갈지 모른다",
|
||
"disposition": "KEEP_IN_SSOT",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": null,
|
||
"reason": "이 실험대가 왜 섰는지를 적는 틀이다. 넷은 이미 답이 나와 「열린 질문 네 개에 대한 답」 표가 담고, 그 표를 reference:look-at-the-lookup-key-before-moving-the-store · reference:clear-the-header-before-you-trust-it · decision:split-the-two-stores-and-design-each 셋이 가리킨다. 답이 있는 질문은 Open Question 의 「답이 아직 없고 다음 검증과 종료 기준이 있다」를 못 채운다. 질문 목록과 게시일은 SSOT 에 남긴다"
|
||
},
|
||
{
|
||
"id": "SSOT-q2-reproduces-only-after-the-store-is-shared",
|
||
"kindCandidate": "CASE",
|
||
"sourceRefs": [
|
||
"final/document.md#코드보다-먼저-드러난-문제-답할-수-없던-질문-네-개"
|
||
],
|
||
"summary": "Q2 의 경쟁은 저장소를 공유한 뒤에야 재현되므로 로드맵의 Q2 → Q3 순서를 Q3 → Q2 로 뒤집었고, 그것이 B-1·B-2 가 B-3 보다 앞에 오는 이유다",
|
||
"disposition": "MERGE_INTO",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": "case:the-winner-of-the-rotation-race-also-loses",
|
||
"reason": "그 Case 가 성립하기 위한 전제다 — 저장소가 process-local 이면 두 replica 가 같은 refresh token 항목을 보지 않아 경쟁할 대상이 없다. Case 의 「문제」 절이 이미 「인스턴스가 둘이고 요청이 어느 쪽으로 갈지 모르는 구성」을 적고 있어 그 옆 한 문단으로 들어간다. 따로 세우면 실험 순서만 말하는 기록이 된다"
|
||
},
|
||
{
|
||
"id": "SSOT-checking-the-roadmap-against-the-published-questions",
|
||
"kindCandidate": "REFERENCE",
|
||
"sourceRefs": [
|
||
"final/document.md#코드보다-먼저-드러난-문제-답할-수-없던-질문-네-개"
|
||
],
|
||
"summary": "로드맵을 밖에 게시된 질문 넷과 대조하자 Q4 가 통째로 빠져 있었고(제외가 아니라 누락) 저장소 질문도 「Redis 로 간다」에서 「Redis 와 JDBC 중 무엇인가」로 넓어져 B-1·B-2 가 나뉘었다",
|
||
"disposition": "KEEP_IN_SSOT",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": null,
|
||
"reason": "「계획을 게시한 질문과 대조한다」는 규칙처럼 읽히지만 SSOT 가 적용 범위도 예외도 대지 않고, 이 실험대에서 한 번 일어난 계획 수정이다. 흡수할 자리도 없다 — B-4 의 발견은 case:nginx-does-not-overwrite-a-header-it-never-sets 가, 저장소를 둘로 나눈 판단은 decision:split-the-two-stores-and-design-each 가 담는데 여기 적힌 것은 결과가 아니라 계획 쪽이다. 실험 번호가 왜 그렇게 붙었는지의 근거로 SSOT 에 남긴다"
|
||
},
|
||
{
|
||
"id": "SSOT-a-layer-prediction-table-keeps-the-old-roadmap-numbers",
|
||
"kindCandidate": "REFERENCE",
|
||
"sourceRefs": [
|
||
"final/document.md#선택의-이유와-지킨-경계-a층-keycloak-자체가-깨질-때"
|
||
],
|
||
"summary": "A층 예측표는 A-0 의 「9. 다음 실험에 대한 예측」을 그대로 옮긴 것이라 실험 번호가 옛 로드맵의 것이고(옛 `B-5` 가 최종 B-3, 옛 `A-3 노드 상실` 이 최종 A-4, 옛 `A-4 volatile 비교` 가 최종 A-7) 고쳐 적지 않았다",
|
||
"disposition": "KEEP_IN_SSOT",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": null,
|
||
"reason": "인용 표시 규약이다 — 예측을 언제 썼는지가 번호에 남아 있어 고치지 않는다는 것. 같은 표에서 예측 칸이 빈 두 행을 「틀린 예측 다섯」에 넣지 않은 것도 같은 계수 규약이다. A층·B층 가이드의 (observed)·(unknown) 규약을 KEEP_IN_SSOT 로 둔 것과 같은 자리이고, 번호 대응표를 읽을 사람은 이 문서를 읽는 사람뿐이다. 예측을 먼저 적고 대조한다는 실천 자체는 reference:verify-the-injection-landed-separately-from-the-result 가 담는다"
|
||
},
|
||
{
|
||
"id": "SSOT-layer-headings-for-b-and-d",
|
||
"kindCandidate": "CONCEPT",
|
||
"sourceRefs": [
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-b층-열린-질문-네-개에-대한-답",
|
||
"final/document.md#선택이-코드와-흐름에-반영되는-방식-d층-운영"
|
||
],
|
||
"summary": "B층·D층의 층 머리말 — B층은 BFF(Spring Boot) 두 인스턴스와 Redis, oauth2-proxy 두 replica 를 올리고 잰다는 두 문장이고, D층은 제목 아래에 본문이 한 줄도 없다",
|
||
"disposition": "KEEP_IN_SSOT",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": null,
|
||
"reason": "아래 h4 로 가는 이정표다. B층 두 문장이 적는 구성은 「실험대」 절과 B층 Setup 여섯이 이미 명령과 매니페스트로 담고, D층은 제목 바로 다음이 D-1 이라 뗄 본문이 없다. 두 층의 발견은 각각 Case 와 Setup 으로 이미 올라가 있다"
|
||
},
|
||
{
|
||
"id": "SSOT-where-the-measurement-goes-wrong-outlasts-the-result-tables",
|
||
"kindCandidate": "REFERENCE",
|
||
"sourceRefs": [
|
||
"final/document.md#결정이-지켜지는지-확인하는-방법-측정이-거짓말할-때"
|
||
],
|
||
"summary": "이 실험대가 남긴 것 중 결과표보다 오래 갈 것은 어디서 측정이 틀리는가다 — 아래 네 갈래를 묶는 머리말 한 문장",
|
||
"disposition": "KEEP_IN_SSOT",
|
||
"dispositionReview": "CONFIRMED",
|
||
"target": null,
|
||
"reason": "주제 when-the-measurement-lies 의 독자 질문이 이미 이 문장이 말하는 것이고, 아래 네 갈래 중 셋은 case:seventy-six-failures-that-were-not-the-servers · reference:never-subtract-values-from-two-clocks · concept:the-up-metric-cannot-see-alive-but-useless 로 올라가 있다. 머리말을 따로 기록으로 만들면 주제 설명이 기록 한 편으로 중복된다"
|
||
}
|
||
],
|
||
"unlisted": [],
|
||
"history": {},
|
||
"counts": {
|
||
"topics": 6,
|
||
"nodes": 61,
|
||
"written": 61,
|
||
"unwritten": 0,
|
||
"unlisted": 0,
|
||
"candidates": 152
|
||
},
|
||
"unassignedSsotAssets": {
|
||
"d3-secret-exposure": "D-3 의 base64 관측은 KEEP_IN_SSOT 로 두어 글감이 없다. 그림도 함께 남긴다",
|
||
"wrong-predictions": "틀린 예측 다섯을 한 자리에 모은 그림이라 특정 Case 에 붙지 않는다. 주제 전체의 그림이다",
|
||
"open-questions-answered": "앞 프로젝트가 남긴 열린 질문 넷에 답한 것을 모은 그림이라 이 프로젝트의 어느 한 글감에 붙지 않는다",
|
||
"not-applicable-conditions": "적용되지 않는 조건을 모은 그림이고 그것을 담는 것은 Reference 다. Reference 에는 본문이 없어 그림을 렌더링할 자리가 없다",
|
||
"guest-as-host-process": "「실험대가 쓴 개념」 0층의 그림이다. 그 층에서 나온 후보 셋이 MERGE_INTO·KEEP_IN_SSOT 라 붙을 글감이 없다 — virsh destroy 가 ACPI 없이 프로세스를 끊는 사실은 case:two-ways-to-lose-a-node 의 한 절로 들어가고, 그 Case 는 노드를 잃는 두 경로를 자기 그림으로 이미 그리고 있다",
|
||
"cpu-io-passthrough-paths": "같은 0층의 그림인데 그리는 것이 CPU·virtio·패스스루 세 갈래라 이 실험대가 잰 것이 아니다. SSOT 스스로 문서에서 옮긴 것이라고 적었고 후보도 KEEP_IN_SSOT 다. 같은 구조를 정의로 담는 것은 virtualization 프로젝트이고 이 프로젝트에는 붙을 글감이 없다"
|
||
}
|
||
}
|