docs(keycloak-session-store): list every concept this lab used but never explained

The lab wrote 26 experiments on top of names it never defined. Comparing the
terms used against the places that actually explain them: 24 have no
explanation anywhere. conntrack appears 35 times across the experiment
documents, refresh token rotation 17, JWKS and Liquibase 11 each.

The list groups them in eight layers with, for each, where it came up,
whether it is explained centrally, only inside one experiment, or nowhere,
and what specifically needs to be learned. It is mapped onto the six topics
of the decomposition contract rather than standing beside it — two glam of
operations-that-report-success cannot be written without systemd, and
Restart=, journald and Type=forking are all in the "nowhere" column.

The bottom layer is the worst of it. systemctl has been typed eight times
as a command and never explained, and the unit file, cgroup, restart policy
and crash-loop limit under it are unwritten.

Building the list turned up a piece of evidence the lab missed. B-7 recorded
that it narrowed the 502 by splitting layers and calling traefik directly to
bypass nginx. The host nginx journal had already written the cause in a
sentence — "upstream sent too big header ... /oauth2/callback" — and none of
the 147 evidence files contain it. That is what journald being in the
"nowhere" column cost.

Also fixes ten spatial-metaphor errors the tightened check_prose now
catches, nine of which predate this section.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-09-07 15:17:32 +09:00
co-authored by Claude Opus 5
parent 1f04117bbf
commit 6955611439
2 changed files with 170 additions and 11 deletions
+169 -10
View File
@@ -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` 가 「없음」이었던 대가가 여기서 나타난다.