diff --git a/docs/keycloak-session-store/final/document.md b/docs/keycloak-session-store/final/document.md index 0276750..29b3965 100644 --- a/docs/keycloak-session-store/final/document.md +++ b/docs/keycloak-session-store/final/document.md @@ -141,7 +141,7 @@ kc-lab-1/2 는 무암호 sudo 라 `conntrack`·`tc`·`iptables` 를 자유롭게 ![주입이 걸렸는지 따로 확인한다](assets/injection-verification/injection-verification.svg) -아홉 번의 실패가 전부 같은 자리에서 생겼다 — 주입과 관측 사이가 비어 있었다. +아홉 번의 실패가 모두 같은 단계에서 생겼다 — 주입과 관측 사이가 비어 있었다. --- @@ -177,7 +177,7 @@ kc-lab-1/2 는 무암호 sudo 라 `conntrack`·`tc`·`iptables` 를 자유롭게 그래서 모든 절차를 **셸에 그대로 붙여넣을 수 있는 명령**으로 적었다. 이 결정에는 대가가 따랐다. 나중에 재현 절차를 점검해 보니 `( curl ... ) & -를 20개 띄우고 wait` 처럼 **측정 장치 자체가 산문으로 적힌 자리가 여럿** +를 20개 띄우고 wait` 처럼 **측정 장치 자체를 산문으로 적어 둔 곳이 여럿** 있었고, 22.2초라는 헤드라인 수치를 만든 부하 생성기부터가 실행 가능한 형태가 아니었다. @@ -300,7 +300,7 @@ COMMIT 이 WAL 디스크 기록을 기다리지 않고 즉시 반환한다. 그 **거기서 한 번 더** 곱해진다. 마지막에는 파드가 죽는다. readiness 가 타임아웃으로 실패해 느린 노드가 -로드밸런서에서 빠지므로, **느림이 그 자리에서 장애로 승격된다.** +로드밸런서에서 빠지므로, **느림이 곧바로 장애로 승격된다.** ![지연이 곱해지는 두 단계](assets/a6-latency-multiplication/a6-latency-multiplication.svg) @@ -645,7 +645,7 @@ nginx 는 인증서를 기동 시점에 읽어 메모리에 들고 있는데 cer `live/` 는 심볼릭 링크라 **경로가 그대로이고 가리키는 대상만 바뀐다.** 그래서 nginx 설정을 고칠 필요가 없고, 바로 그 때문에 「설정이 그대로니 괜찮다」고 착각하기 쉽다. 필요한 것은 설정 변경이 아니라 reload 이며, -그 reload 를 부르는 자리가 이 실험대에서는 셋 다 비어 있었다. +그 reload 를 부르는 경로가 이 실험대에서는 셋 다 비어 있었다. reload 가 실제로 일어났는지 가리는 방법도 여기서 나왔다. **마스터 PID 는 유지되고 워커 PID 만 바뀌면 reload 된 것**이다. @@ -687,7 +687,7 @@ reload 자체는 무중단이었다 — 새 연결 **8856건 전부 200**, p95 2 ## 결정이 지켜지는지 확인하는 방법 -### 측정이 거짓말하는 자리들 +### 측정이 거짓말할 때 이 실험대가 남긴 것 중 결과표보다 오래 갈 것은 **어디서 측정이 틀리는가**다. @@ -715,7 +715,7 @@ D-4 에서 갱신 중 비200 이 한 번 나왔다고 하면, **평시 오류율 ![대조군 없이는 귀속할 수 없다](assets/measurement-control/measurement-control.svg) -대조군이 관측과 귀속 사이에 있어서, 그 자리가 비면 같은 관측이 두 가지로 읽힌다. +대조군이 관측과 귀속 사이에 있어서, 그것이 없으면 같은 관측이 두 가지로 읽힌다. #### 두 시계에서 온 값을 빼면 안 된다 @@ -744,7 +744,7 @@ CT 로그가 자체 시계로 서명)가 보정한 훅 시각의 정확히 1초 아니라 지표 자체가 없어서다. 그래서 이 사실을 「스크린샷 누락」이 아니라 **측정된 공백**으로 기록했다. -#### 문서가 자기 증거와 어긋나는 자리 +#### 문서가 자기 증거와 어긋난 곳 기록을 다 쓴 뒤 증거와 하나씩 대조했더니 어긋난 곳이 여럿 나왔다. @@ -762,8 +762,8 @@ CT 로그가 자체 시계로 서명)가 보정한 훅 시각의 정확히 1초 ### 재현 가능성을 어떻게 보장했나 -절차를 명령으로 적는 것만으로는 부족했다. **측정 장치 자체가 산문인 자리**가 -남아 있었는데, 하필 그것들이 헤드라인 수치를 만든 바로 그 명령이었다. +절차를 명령으로 적는 것만으로는 부족했다. **측정 장치 자체를 산문으로 적어 둔 +곳**이 남아 있었는데, 하필 그것들이 헤드라인 수치를 만든 바로 그 명령이었다. | 어디 | 산문이던 것 | |---|---| @@ -796,7 +796,7 @@ CT 로그가 자체 시계로 서명)가 보정한 훅 시각의 정확히 1초 ![열린 질문 네 개가 닿은 곳](assets/open-questions-answered/open-questions-answered.svg) -네 질문이 공통 원인으로 모이면서, 저장소 선택만으로는 풀리지 않는 것들이 한자리에 드러난다. +네 질문이 공통 원인으로 모이면서, 저장소 선택만으로는 풀리지 않는 것들이 함께 드러난다. ### 이 기록이 적용되지 않는 조건 @@ -897,3 +897,162 @@ lint 가 잡아낸 것 중 사람이 놓치기 쉬운 것 둘을 적어 둔다. **Studio 기록은 아직 쓰지 않았다.** 이 문서까지가 SSOT 이고 `tech-log-studio/` 아래 글감 추출과 기록 작성은 다음 단계이기 때문이다. + +--- + +## 딥리서치 대기 목록 — 이 실험대에서 쓴 개념 전수 + +실험을 돌리면서 이름을 쓴 개념과, 그 개념을 실제로 설명한 곳을 대조했다. +**쓰기는 썼는데 설명이 어디에도 없는 것이 24개**였다. `conntrack` 은 실험 +문서에 35번 나오는데 무엇인지 설명한 곳이 없고, `refresh token rotation` 은 +17번, `JWKS` 와 `Liquibase` 는 각각 11번 나오는데 마찬가지다. + +이 목록은 그 대조 결과다. 채우기 전까지는 **이 기록이 이름만 알고 쓴 것**이 +어디인지 가리키는 표로 둔다. + +**상태 표기** + +| | 뜻 | +|---|---| +| 중앙 | [`session-lab-concepts.md`](../source/docs/session-lab-concepts.md) 에 절이 있다 | +| 분산 | 해당 실험 문서 안에만 「개념」 절이 있다 | +| **없음** | **쓰기만 하고 설명한 곳이 없다** | + +### 1. 리눅스 · systemd — 바닥 계층 + +이 실험대의 모든 것이 이 위에서 돈다. 그런데 **전부 「없음」이다.** 지금까지 +`systemctl` 을 명령으로만 여덟 번 썼고 무엇인지 설명한 적이 없다. + +| 개념 | 어디서 나왔나 | 상태 | 알아야 할 것 | +|---|---|---|---| +| systemd 유닛 파일 구조 | D-4 `certbot-renew.service` · 호스트 nginx | 없음 | `[Unit]`·`[Service]`·`[Install]` 의 역할, `systemctl cat` 과 `systemctl show` 의 차이(작성값 대 적용값) | +| `Type=forking` · `PIDFile` | 호스트 nginx | 없음 | systemd 가 데몬을 추적하는 방식, `simple`·`notify` 와의 차이, 왜 nginx 는 forking 인가 | +| `Restart=` 정책 | 호스트 nginx (`on-failure`) | 없음 | `no`·`always`·`on-failure`·`on-abnormal` 의 경계, `RestartSec` | +| `StartLimitBurst` 크래시 루프 상한 | 호스트 nginx (5회/10초) | 없음 | 상한에 걸리면 systemd 가 포기한다 — `reset-failed` 없이는 안 살아난다 | +| `KillMode` · `KillSignal` | 호스트 nginx (`mixed`/`SIGQUIT`) | 없음 | nginx 의 SIGQUIT 이 graceful 이라는 것과 D-4a 의 reload 무중단이 같은 성질인가 | +| cgroup v2 | `systemctl status` 의 `CGroup:` 블록 | 중앙(4회 언급) | `/sys/fs/cgroup` 실물, `pids.max`·`memory.max`·`cpu.stat`, 쿠버네티스 자원 제한과 같은 메커니즘인지 | +| systemd slice | `system.slice` | 없음 | slice·scope·service 의 관계, 자원 제한이 상속되는 방식 | +| journald | `systemctl status` 하단 로그 | 없음 | `-u` 필터·우선순위·보존 정책. **B-7 의 502 원인이 여기 있었는데 수집하지 않았다** | +| `PrivateTmp=true` | nginx · `certbot-renew.service` 양쪽 | 없음 | 왜 두 유닛 모두 켜져 있나, 네임스페이스 격리와의 관계 | +| PID 1 시그널 보호 | A-3 (`kill -9 1` 무시) | 분산(A-3) | 왜 자기 네임스페이스의 SIGKILL 을 무시하는가 | +| OOM killer · `oom_score` | 미측정 | 없음 | cgroup 메모리 상한과 OOM 의 관계 | + +### 2. 네트워크 · netfilter + +| 개념 | 어디서 나왔나 | 상태 | 알아야 할 것 | +|---|---|---|---| +| **conntrack** | A-1 (35회 언급) | **없음** | 연결 추적 상태 기계, `ESTABLISHED` 가 규칙 평가를 건너뛰는 이유, 표를 지우면 왜 다시 걸리는가 | +| netfilter 처리 순서 | A-5 | 분산(A-5) | `raw`→`mangle`→`nat`→`filter` 와 훅 지점, conntrack 이 어디서 개입하나 | +| iptables `raw` 테이블 | A-5 (12회) | 없음 | PREROUTING 에서 conntrack 보다 먼저 잡는다는 것의 정확한 의미 | +| kube-router 체인 재삽입 | A-5 | 없음 | 왜 `-I FORWARD 1` 이 무시되는가, 컨트롤러가 규칙을 되돌리는 주기 | +| flannel VXLAN | A-5 | 중앙 | 캡슐화 때문에 물리 인터페이스에서 파드 IP 가 안 보이는 것 | +| `tc` netem | A-6 | 중앙(2회) | qdisc 계층, 지연 주입이 어느 방향에만 걸리는가 | + +### 3. PostgreSQL · 영속성 + +| 개념 | 어디서 나왔나 | 상태 | 알아야 할 것 | +|---|---|---|---| +| `synchronous_commit` | A-3 (24회) | 언급만 | `on`·`off`·`local`·`remote_write` 의 차이, Keycloak 이 왜 트랜잭션마다 끄는가 | +| WAL · `wal_writer_delay` | A-3 | 언급만 | WAL 기록·플러시·체크포인트의 순서, 실측 200ms 가 어디서 나온 값인가 | +| fsync · 페이지 캐시 | A-3 | 언급만 | COMMIT 반환과 디스크 도달 사이에 무엇이 있나 | +| 낙관적 락 · `VERSION` 컬럼 | A-6 (충돌 0건) | 언급만 | 왜 로그인은 경합하지 않는가(INSERT 라서), 어떤 연산이 경합하나 | +| `FOR NO KEY UPDATE SKIP LOCKED` | A-6 | 없음 | 잠금 수준과 SKIP LOCKED 의 의미 | +| PostgreSQL 문장 로깅 | A-7a | 분산(A-7a) | `log_statement` 수준별 비용, 운영에서 켜도 되는가 | +| Liquibase `databasechangelog` | D-2 (11회) | 분산(D-2) | 체크섬이 계산되는 방식, 왜 옛 버전이 새 체크섬을 거부하나 | + +### 4. 쿠버네티스 + +| 개념 | 어디서 나왔나 | 상태 | 알아야 할 것 | +|---|---|---|---| +| readiness vs liveness · health group | A-2 · B-5 | 분산 | 프로브가 실패했을 때 각각 무슨 일이 일어나나 | +| `node-monitor-grace-period` | A-4 (40초) | 없음 | 컨트롤 플레인이 노드를 죽었다고 판정하는 절차 | +| `tolerationSeconds` | A-4 (300초) | 없음 | taint 기반 축출 타이머, 합쳐서 5분 40초가 되는 계산 | +| local-path PVC 노드 친화성 | A-4 | 중앙 | 왜 재배치가 불가능한가 | +| StatefulSet 재생성 규칙 | A-4 | 중앙 | Terminating 파드의 대체를 만들지 않는 이유 | +| NetworkPolicy 허용목록 | A-1 | 분산(A-1) | deny 규칙을 쓸 수 없는 구조 | +| `enableServiceLinks` | B-1 | 분산(B-1) | Docker link 시절 환경변수 주입이 남아 있는 이유 | +| Secret 과 etcd | D-3 | 중앙 | base64 가 인코딩인 것과 저장 시 암호화(EncryptionConfiguration)의 차이 | + +### 5. Keycloak · Infinispan + +| 개념 | 어디서 나왔나 | 상태 | 알아야 할 것 | +|---|---|---|---| +| `persistent-user-sessions` | A-7 · A-7a | 중앙 | 버전별 기본값 변화와 마이그레이션 경로 | +| 디스커버리 vs 트랜스포트 | A-1 | 중앙 | `JGROUPS_PING` 과 TCP 7800 이 나뉜 이유 | +| FD_SOCK2 · MERGE3 · GMS | A-1 · A-5 | 중앙 | 지표 이름에 그대로 나오는 프로토콜들 | +| **refresh token rotation** | B-3 (17회) | **없음** | `revokeRefreshToken`·`refreshTokenMaxReuse` 의 정확한 의미, 경쟁 시 client session 을 지우는 것이 규격인가 구현인가 | +| `sid` 와 세션 두 겹 | 전 실험 | 언급만 | SSO 세션·클라이언트 세션·`KEYCLOAK_IDENTITY`·`AUTH_SESSION_ID` 의 관계 | +| `CLIENT_SCOPE_CLIENT` · `DEFAULT_SCOPE` | A-7a | 분산(A-7a) | default 와 optional 스코프가 토큰 발급에서 갈리는 지점 | +| 백채널 로그아웃 | C-2 | 분산(C-1) | `backchannelLogoutUrl` 과 앱 엔드포인트 규격, `sid` 역인덱스 | + +### 6. Spring · 애플리케이션 저장소 + +| 개념 | 어디서 나왔나 | 상태 | 알아야 할 것 | +|---|---|---|---| +| Spring Session | B-1 | 언급만 | `SessionRepository` 추상화, Redis 구현의 키 구조와 만료 처리 | +| `OAuth2AuthorizedClientService` 계열 | B-0 · B-2 | 없음 | In-memory·JDBC 구현의 차이, `AuthenticatedPrincipal…Repository` 가 principal 로 찾는 이유 | +| **인가 클라이언트 `PRIMARY KEY`** | B-2 | **없음** | 기본 스키마가 세션 id 를 키에 넣지 않은 설계 의도, 바꿀 수 있는가 | +| Java 직렬화 `\xac\xed` | B-1 | 없음 | Redis 에 들어간 바이트가 무엇인지, JSON 직렬화로 바꿀 때의 대가 | +| agroal 커넥션 풀 | A-6 (9회) | 없음 | 획득 대기·최대 크기·검증 주기, `blocking_time` 지표의 정의 | + +### 7. TLS · 인증서 + +| 개념 | 어디서 나왔나 | 상태 | 알아야 할 것 | +|---|---|---|---| +| `fullchain.pem` vs `cert.pem` | D-4 | 분산(D-4) | 체인 단계와 클라이언트가 보완해 주는 경우 | +| ACME · HTTP-01 vs DNS-01 | 구축 | 중앙 | 어느 쪽이 어떤 제약을 푸는가 | +| certbot `renewal-hooks` | D-4a | 분산(D-4a) | `pre`·`deploy`·`post` 의 실행 조건 | +| **SCT · Certificate Transparency** | D-4a | **없음** | SCT 가 인증서에 박히는 절차, crt.sh 가 왜 색인하지 못했나 | +| **Let's Encrypt 백데이트** | D-4a | **없음** | `notBefore` 를 앞당기는 이유와 정확한 폭 | +| JWKS · `kid` | B-6 (11회) | 없음 | 키 회전 시 캐시 동작, 유예 구간이 없는 이유 | +| oauth2-proxy 티켓 구조 | B-7a | 분산(B-7a) | 세션 id 와 복호화 키가 함께 암호화되는 형식 | + +### 8. 측정 · 시계 + +| 개념 | 어디서 나왔나 | 상태 | 알아야 할 것 | +|---|---|---|---| +| **NTP 와 시계 왜곡** | D-4a (106초) | **없음** | `NTPSynchronized` 의 의미, 왜곡을 재는 올바른 방법, 왜 두 시계를 빼면 안 되는가 | +| Prometheus `up` 의 한계 | A-2 | 중앙 | 합성 지표가 무엇을 못 보는가 | +| exporter 패턴 · relabel | 구축 | 중앙 | 관측 대상에 Redis·BFF·PostgreSQL 이 빠진 이유 | + +### 분해 계약과의 대응 + +이 프로젝트에는 이미 주제 여섯과 글감 서른셋으로 분해 계약이 서 있다 +([`tech-log-studio/tech-log-tree.json`](../tech-log-studio/tech-log-tree.json)). +위 여덟 묶음은 그것과 나란한 별개 구조가 아니라, **각 주제가 서 있는 바닥을 +채우는 목록**이다. + +| 계약의 주제 | 그 아래에서 이름만 쓰고 넘어간 개념 | +|---|---| +| `session-custody-across-nodes` | 5번 — 디스커버리 대 트랜스포트, `sid` 와 세션 두 겹 | +| `losing-a-node-or-the-store` | 1번 전체 · 3번(WAL·fsync) · 4번(`node-monitor-grace-period`·`tolerationSeconds`) | +| `where-application-state-lives` | 6번 전체 · 5번(refresh token rotation) | +| `trust-handed-over-at-the-edge` | 2번(conntrack·netfilter) · 7번(oauth2-proxy 티켓) | +| `operations-that-report-success` | 1번(systemd 일체) · 7번(SCT·백데이트) · 3번(Liquibase) | +| `when-the-measurement-lies` | 8번 전체 | + +`operations-that-report-success` 의 글감 두 개(「새 인증서가 디스크에 있고 +38분 25초 동안 옛 인증서가 나갔다」, 「deploy 훅 하나가 그 공백을 1~2초로 +줄였다」)는 **systemd 를 설명하지 않고는 쓸 수 없다.** `Restart=` 도 +`journald` 도 `Type=forking` 도 이 목록에서 「없음」이기 때문이다. + +### 채우는 순서 + +바닥부터 올라간다. **1번(리눅스·systemd)** 이 먼저인 이유는 나머지 전부가 그 +위에서 돌기 때문이고, 실제로 이 목록에서 「없음」이 가장 많은 층이기도 하다. +그 다음은 **2번(네트워크)** 인데 A층 실험의 주입이 전부 거기서 이루어졌고 +아홉 번의 조용한 실패 중 넷이 그 층의 개념을 몰라서 생겼다. + +### 이 목록을 만들면서 하나 더 나왔다 + +`systemctl status nginx` 를 쳐 보면 저널이 이것을 보여 준다. + +``` +Sep 04 14:37:44 nginx[586]: [error] upstream sent too big header while reading + response header from upstream, ... request: "GET /oauth2/callback?state=..." +``` + +**B-7 이 502 의 원인으로 지목한 것을 호스트 nginx 가 문장으로 적어 두었다.** +B-7 은 계층을 나눠(traefik 을 직접 불러 nginx 를 우회) 원인을 좁혔다고 +기록했는데, 증거는 저널에 있었고 증거 파일 147개 중 이것을 담은 것은 없다. +`journald` 가 「없음」이었던 대가가 여기서 나타난다. diff --git a/docs/keycloak-session-store/tech-log-studio/tech-log-tree.json b/docs/keycloak-session-store/tech-log-studio/tech-log-tree.json index b8ca484..3244202 100644 --- a/docs/keycloak-session-store/tech-log-studio/tech-log-tree.json +++ b/docs/keycloak-session-store/tech-log-studio/tech-log-tree.json @@ -7,7 +7,7 @@ "revision": "cdac9b8178391311d8eca1ebc6cac15bb62d79af", "verified": "이 커밋이 A-1 부터 D-4a 까지 실험 전량을 병합한 tip 이고 저장소 HEAD 다. git branch --contains 가 experiment 브랜치 26 개를 모두 낸다 (2026-09-07 확인)" }, - "ssotSha256": "9d2acdd6d38074572d6707655ede4b6768a1e508345e458483da33539fe9e65c", + "ssotSha256": "e51464baa254783bb63463a5009c46f432dbf9c21ab75e67e4b8516038661851", "sourceRevision": "keycloak-session-lab@2026-09", "generatedAt": "2026-09-07", "candidateScope": {