Files
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.

Follows the import procedure in README.md.

  source/     the originating repository verbatim — 78 documents, 28 SVGs,
              8 manifests, plus .source-revision recording the commit
  final/      the SSOT
    document.md   729 lines written from the 29 experiment documents, not
                  concatenated: what was predicted, what was measured, and
                  where the measurement itself was wrong
    evidence/raw    125 outputs, flattened to <experiment>__<file> because
                    the originals collided (01-baseline.txt appeared three
                    times) and the audit only globs the top level
    evidence/meta   one per raw file; command and exitCode are null and the
                    README says why rather than inventing them
    evidence/browser  22 captures
    assets/       three diagrams through techviz
    .techviz/     their VizSpecs

A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.

Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.

verify-pipeline.py passes. audit-records.py reports no issues.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 22:51:59 +09:00
..

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
raw — 명령 출력 원문 (정본)
===================

이 폴더가 증거의 정본이다. SVG 는 표현물이고 여기 원문 없이는 만들지 않는다.

파일 이름은 <실험 폴더>__<원본 파일명> 이다. 원본 저장소에서는 실험마다
폴더가 나뉘어 있었고 파일명이 겹쳤다(01-baseline.txt 등). 이 저장소의 감사
스크립트는 raw/ 최상위만 훑으므로 이름을 평평하게 펴고 접두사로 구분했다.

각 파일의 실행 메타는 ../meta/<같은 이름>.json 에 있다.
명령 전문은 그 json 의 sourceDoc 이 가리키는 해설 문서의 「재현 절차」 절에 있다.


[a1-jgroups-transport-block]
  a1-jgroups-transport-block__01-baseline-cluster.txt
      차단 전 — 양쪽이 뷰 ID 5·멤버 2로 일치, JGROUPS_PING 코디네이터 1명
  a1-jgroups-transport-block__02-control-before-block.txt
      대조군 — 차단 전 교차 노드 refresh 200, JGroups 지표 전부 0
  a1-jgroups-transport-block__03-block-applied.txt
      NetworkPolicy 적용. 빈 측정값을 "변화 감지"로 오판한 기록
  a1-jgroups-transport-block__04-after-block-state.txt
      차단 43초 후 — 지표 무변화, JGROUPS_PING 그대로
  a1-jgroups-transport-block__05-conntrack-problem.txt
      핵심 문제 — ESTABLISHED [ASSURED] 로 기존 연결이 살아 있음. FD_SOCK2 의 57800 포트도 함께 드러남
  a1-jgroups-transport-block__06-partition-observed.txt
      임시 curl 파드 폴링의 실패 — 빈 값·개수 불일치
  a1-jgroups-transport-block__07-cluster-size.txt
      vendor_cluster_size 가 25분 내내 2 — 분단이 일어나지 않았다는 결정적 증거
  a1-jgroups-transport-block__08-restart-forced-partition.txt
      재연결 강제 후 2 → 1
  a1-jgroups-transport-block__09-cross-node-under-partition.txt
      본 시험 — 교차 refresh 200(예측 적중), 로그아웃 후 재갱신 200(예측 빗나감)
  a1-jgroups-transport-block__10-logout-not-propagated.txt
      기제 확정 — DB 행 0건인데 keycloak-0 캐시에 1건, 헬스체크 cluster health: DOWN
  a1-jgroups-transport-block__11-service-impact.txt
      분단 노드가 Service 에서 빠짐. ready=[10.42.0.35] notReady=[10.42.1.67], 외부 로그인 200
  a1-jgroups-transport-block__12-recovery.txt
      90초 만에 자동 재형성, merge3_get_num_merge_events = 1, 코디네이터 재선출

[a2-database-loss]
  a2-database-loss__01-baseline.txt
      정지 전 — 양쪽 Ready, cluster_size=2
  a2-database-loss__02-setup-sessions.txt
      양쪽 노드에 세션 하나씩. 캐시는 각자 노드에만
  a2-database-loss__03-four-paths.txt
      네 경로 전부 500. 캐시를 가진 노드도 실패 — refresh 는 쓰기다
  a2-database-loss__04-health-and-service.txt
      전면 장애 증거 — Ready 파드 0개, ready 주소=[], 외부 503, database connections: DOWN. JWKS·.well-known 은 200
  a2-database-loss__05-recovery.txt
      up=1 인 채로 503. DB 복귀 15초 후 재시작 0회로 자동 회복, 세션 5건 생존

[a3-database-crash]
  a3-database-crash__01-crash-injection.txt
      첫 시도 실패 — 파드 안 백그라운드 루프가 exec 종료와 함께 죽어 0건 수집
  a3-database-crash__02-design-check.txt
      핵심 설계 확인 — 로그인 트랜잭션도 SET LOCAL synchronous_commit TO OFF 로 커밋한다
  a3-database-crash__03-loss-measurement.txt
      --grace-period=0 --force 주입
  a3-database-crash__04-comparison.txt
      유실 0건 — 그러나 crash recovery 가 안 돌았다. 죽인 적이 없는 것
  a3-database-crash__05-true-crash.txt
      kill -9 1 시도 — 컨테이너 안에서 PID 1 은 SIGKILL 을 무시한다
  a3-database-crash__06-backend-kill-crash.txt
      성공한 주입 — 백엔드에 SIGKILL → not properly shut down / redo starts / redo done
  a3-database-crash__07-loss-result.txt
      결과: 153건 중 4건 유실. 토큰은 발급됐는데 세션 행이 없는 sid 목록
  a3-database-crash__08-wal-settings.txt
      (원본 README 에 설명이 없다 — 파일 머리말 참조)

[a4-node-loss]
  a4-node-loss__01-baseline.txt
      차단 전 — 양쪽 Ready, PVC 가 kc-lab-2 에 못박혀 있음(재배치 불가의 원인), 외부 200
  a4-node-loss__02-worker-node-killed.txt
      virsh destroy kc-lab-2 — 40초간 노드가 Ready 로 남아 있고 외부는 이미 000. 이후 503
  a4-node-loss__03-state-during-loss.txt
      죽은 파드가 ready=true, 산 파드가 ready=false. up 은 정확히 0. unreachable taint
  a4-node-loss__04-eviction-timing.txt
      tolerationSeconds=300 — 5분 뒤 축출, 새 postgres 는 Pending
  a4-node-loss__05-recovery.txt
      FailedScheduling: didn't match PersistentVolume's node affinity, StatefulSet DESIRED=2 CURRENT=1. 노드 복귀 후 60초
  a4-node-loss__06-control-plane-inventory.txt
      kc-lab-1 에 있는 것 목록 — Traefik replicas=1
  a4-node-loss__07-control-plane-loss.txt
      virsh destroy kc-lab-1 — 외부 000, kubectl 불통. 그런데 crictl ps 로 보면 keycloak-0 은 Running
  a4-node-loss__08-control-plane-recovery.txt
      60초 만에 복귀

[a5-asymmetric-partition]
  a5-asymmetric-partition__01-injection.txt
      첫 시도 — iptables -I FORWARD 1
  a5-asymmetric-partition__02-injection-verify.txt
      실패 확인 — kube-router 가 자기 체인을 위로 재삽입, DROP 규칙 0 패킷
  a5-asymmetric-partition__03-raw-table-injection.txt
      raw 테이블로 이동. 그래도 0 패킷
  a5-asymmetric-partition__04-correct-direction.txt
      연결 방향이 반대였다. 수신측 노드로 옮기니 19/21 패킷 차단
  a5-asymmetric-partition__05-reconnect-observed.txt
      핵심 — 연결이 열린 방향으로 뒤집혀 재연결. 뷰 변화 없음, suspected=0
  a5-asymmetric-partition__06-view-history-and-cleanup.txt
      뷰 ID 이력. 주입 이후 변화 없음 확인
  a5-asymmetric-partition__07-bidirectional-block.txt
      양방향 차단 → 뷰 14, 멤버 1개씩. 그런데 ready=[10.42.1.77] 로 한쪽은 남고 외부 200
  a5-asymmetric-partition__08-coordinator-and-recovery.txt
      15] (2)

[a6-latency-injection]
  a6-latency-injection__01-baseline.txt
      배치 설명(A/B 가 되는 이유), agroal_* 지표 목록, 기준선 70ms / 66ms
  a6-latency-injection__02-delay-injected.txt
      첫 시도 실패 — Cannot find device "eth0" (Debian 은 enp1s0)
  a6-latency-injection__03-flannel-injection.txt
      성공 — flannel.1 에 걸어야 파드 IP 가 보인다. netem Sent 150 pkt 로 검증. k0=41ms vs k1=1872ms
  a6-latency-injection__04-pool-under-load.txt
      동시 20건 — 응답이 1.9초에서 22.2초까지 계단. blocking_time_max=20000ms, max_used_count=19, readiness 프로브 타임아웃
  a6-latency-injection__05-recovery.txt
      해제 즉시 43ms / 51ms 회복. 낙관적 락 충돌 0건(예측 빗나감)

[a7-volatile-comparison]
  a7-volatile-comparison__01-switch-to-volatile.txt
      --features-disabled=persistent-user-sessions 적용, args 확인
  a7-volatile-comparison__02-a0-rerun.txt
      DB 0건인데 교차 노드 refresh 200 — 경로가 DB 에서 클러스터로 바뀌었다
  a7-volatile-comparison__03-a8-rerun-restart.txt
      롤링 재시작 후 400 Session not active — persistent 에서는 200 이었다
  a7-volatile-comparison__04-a1-rerun-partition.txt
      7800 차단 시 교차 노드 400 — persistent 에서는 200. 대조군(같은 노드)은 200 유지
  a7-volatile-comparison__05-a2-rerun-db-loss.txt
      DB 정지 중 새 로그인 200(persistent 에서는 500), refresh 는 500
  a7-volatile-comparison__06-restore-persistent.txt
      원복 확인 — args: ["start"], 로그인 후 DB 1건, 외부 200

[a7a-volatile-cause]
  a7a-volatile-cause__01-cause-determined.txt
      가설이 틀렸다. REVOKED_TOKEN 이 아니라 CLIENT_SCOPE_CLIENT 조회다. 문장 로깅으로 잡고, 실패 로그가 그 SQL 을 직접 지목한다. 그리고 같은 설정에서 캐시 온도만으로 400/500/200 셋이 나온다

[a8-rolling-restart]
  a8-rolling-restart__01-restart-availability.txt
      재시작 전 로그인(sid 기록), 롤링 재시작 중 외부 진입점 9회 모두 200
  a8-rolling-restart__02-session-survival.txt
      재시작 전 토큰으로 refresh 200, DB 행 생존(last_session_refresh 갱신 확인), 세션 수 151 → 151, 캐시 0으로 초기화, 파드 나이 44초/66초

[b0-bff-redis-deploy]
  b0-bff-redis-deploy__01-deploy.txt
      배포 전 자원, Redis·BFF 롤아웃, 두 노드에 하나씩 배치됨
  b0-bff-redis-deploy__02-autoconfiguration.txt
      첫 조회 시도(파싱 실패)와 외부 진입점 HTTP 200
  b0-bff-redis-deploy__03-beans-analysis.txt
      B-0 의 답 — InMemoryOAuth2AuthorizedClientService, AuthenticatedPrincipalOAuth2AuthorizedClientRepository, Redis·Spring Session 없음

[b1-redis-session-store]
  b1-redis-session-store__01-servicelinks-trap.txt
      enableServiceLinks: false 적용 후 롤아웃 성공 — 쿠버네티스가 주입한 REDIS_PORT=tcp://... 가 설정을 덮어쓴 문제
  b1-redis-session-store__02-autoconfig-after.txt
      핵심 — 빈 321→402(+81). sessionRepository → RedisSessionRepository 로 바뀌었지만 authorizedClientService 는 InMemory 그대로
  b1-redis-session-store__03-redis-contents.txt
      Redis 키 1개, 필드는 SPRING_SECURITY_CONTEXT 뿐. 토큰 없음. Java 직렬화(\xac\xed), TTL 1772초

[b2-multi-instance-session]
  b2-multi-instance-session__01-jdbc-store-deploy.txt
      JDBC 저장소로 배포. 테이블이 조용히 안 만들어졌다
  b2-multi-instance-session__02-schema.txt
      원인 — 기본 DDL 은 blob(PostgreSQL 에 없음), -postgres.sql 판본이 따로 있다. PRIMARY KEY (client_registration_id, principal_name) — 조회 키 문제가 DDL 에 박혀 있다
  b2-multi-instance-session__03-plaintext-tokens.txt
      Q3 검증 2번 — bytea 안이 JWT 문자열 그대로. 디코드하면 {"alg":"HS512",...}
  b2-multi-instance-session__04-overwrite-test.txt
      Q1 검증 3번 — 같은 사용자 재로그인 시 행 수 1 그대로, issued_at 과 md5 만 바뀜 = UPDATE(덮어쓰기)
  b2-multi-instance-session__05-logout-cleanup.txt
      Q1 검증 4번 — Redis 0키 / PostgreSQL 1행 잔존 / Keycloak SSO 2세션 잔존

[b3-refresh-contention]
  b3-refresh-contention__01-concurrent-refresh.txt
      같은 토큰으로 동시 5회 — 1개만 200, 나머지는 Maximum allowed refresh token reuse exceeded 와 Session doesn't have required client 두 종류 오류
  b3-refresh-contention__02-session-impact.txt
      ★ 이긴 요청의 새 토큰조차 400. user_session 행은 남아 있고 revoked_token 은 0건
  b3-refresh-contention__03-client-session-removed.txt
      기제 확정 (대조군 포함) — 경쟁 세션 client_sessions=0, 정상 세션 client_sessions=1
  b3-refresh-contention__04-policy-comparison.txt
      정책 3종 비교 — rotation OFF 는 5/5 성공·세션 생존, maxReuse=1 은 여전히 세션 파괴

[b4-edge-authorization]
  b4-edge-authorization__01-header-handling.txt
      ① 동명 헤더가 둘 다 도착(['admin','editor'])하고 값 안의 쉼표를 구분자와 구별할 수 없다 · ② 8KB 에서 Tomcat 400, 16KB 에서 연결 끊김 — 자르지 않고 거부 · ④ 위조 신원 헤더가 그대로 도착, JWT 경로는 401

[b5-redis-loss]
  b5-redis-loss__01-baseline.txt
      정지 전 — Redis 1키, PostgreSQL 1행, save/appendonly no, 외부 200
  b5-redis-loss__02-redis-down.txt
      정지 후 — /bff/token-boundary HTTP 000(멈춤), /actuator/health 503, 파드는 1/1 Ready 유지, Lettuce 재연결 스택
  b5-redis-loss__03-health-groups.txt
      핵심 — /actuator/health 503 인데 /actuator/health/readiness 는 {"status":"UP"}. Service 엔드포인트에 두 파드 모두 남아 있다
  b5-redis-loss__04-persistence.txt
      복구는 자동(재시작 0회) · AOF 를 켰는데 파드 삭제 후 dbsize 0 · PVC 를 붙인 뒤 written-on-pvc 생존

[b6-key-rotation]
  b6-key-rotation__01-before-rotation.txt
      회전 전 — 토큰 kid=OY-caYDN..., JWKS RS256 1개, /api/me 200
  b6-key-rotation__02-rotation.txt
      우선순위 200 공급자 추가 → JWKS RS256 2개, 새 토큰은 새 kid, 옛 토큰도 새 토큰도 200 (무중단)
  b6-key-rotation__03-old-key-removed.txt
      옛 공급자 제거 → JWKS 1개, 옛 토큰 즉시 401. 리소스 서버 재시작 후에도 동일

[b7-cookie-secret]
  b7-cookie-secret__01-deploy.txt
      양 노드에 replica 하나씩. / 302, /ping 200
  b7-cookie-secret__02-cookie-portability.txt
      curl 로 흐름을 완주하려던 시도 — 파드 IP 는 호스트에서 안 닿는다
  b7-cookie-secret__03-rotation.txt
      --cookie-secret string 단수 확인 · 교체 후 session ticket cookie failed validation · Error removing session · Redis 에 고아 세션 2개

[b7a-orphan-session]
  b7a-orphan-session__01-orphan-lifecycle.txt
      회전 2회로 고아가 누적하는 것 · TTL 이 1초/초로 줄고 요청으로 갱신되지 않는 것 · Redis 만으로는 구분 불가(이름·타입·크기 동일, 값 암호화) · redis-cli del 로 지워도 산 세션은 200 · TTL 역산 정리 규칙이 1초 오차로 맞는 것

[c1-multi-app-sso]
  c1-multi-app-sso__01-baseline.txt
      초기화 시도 — logout-all 이 안 먹어 세션 4개가 남았다
  c1-multi-app-sso__02-after-app1-login.txt
      app1 로그인 후 — user session 1 · client session 1 · Redis 1 · DB 1행
  c1-multi-app-sso__03-after-app2-visit.txt
      app2 방문 후 client session 1 → 2, bff-confidential 과 oauth2-proxy 가 같은 user session 에 붙음. Redis 에 두 종류 세션
  c1-multi-app-sso__04-sso-session-killed.txt
      IdP 세션 삭제 후 — 앱 세션 셋 다 남아 있다. realm 을 join 해 보고서야 남은 것이 master 세션임을 확인

[c2-backchannel-logout]
  c2-backchannel-logout__01-current-state.txt
      두 클라이언트 모두 backchannelLogoutUrl 없음 · BFF 소스에 oidcLogout 없음 · 후보 경로 셋 다 302(핸들러 없음)
  c2-backchannel-logout__02-configure-idp.txt
      IdP 쪽에만 backchannel.logout.url 설정 (점 표기는 실패, JSON 으로 성공)
  c2-backchannel-logout__03-logout-attempt.txt
      살아 있는 세션(1)에 로그아웃 → IdP 세션 0, Redis 세션은 1 그대로. Keycloak·BFF 로그에 흔적 없음
  c2-backchannel-logout__04-reachability.txt
      Keycloak 파드가 app1.hyeonworks.com 에 HTTP 200 으로 닿는다 — 네트워크 문제가 아님

[d1-backup-restore]
  d1-backup-restore__01-backup.txt
      pg_dump --clean --if-exists — 395KB · 101 테이블 · 세션 데이터 포함
  d1-backup-restore__02-destruction.txt
      DROP SCHEMA public CASCADE → 테이블 0개. 그런데 외부는 HTTP 200 — Keycloak 이 realm 캐시로 서빙한다
  d1-backup-restore__03-restore.txt
      깨지는 것과 안 깨지는 것(certs 200 / well-known 500 / 토큰 400) · 복구 1초 · 오류 0건 · 데이터 완전 일치 · 재시작 0회

[d2-version-upgrade]
  d2-version-upgrade__01-pre-upgrade.txt
      백업 396KB · 이미지 26.7.0 · 마이그레이션 210건 · 세션 4
  d2-version-upgrade__02-rollback-attempt.txt
      26.0 으로 내리자 Running(0/1) → Error → CrashLoopBackOff. liquibase.exception.ValidationFailedException
  d2-version-upgrade__03-roll-forward.txt
      서비스는 HTTP 200 유지(ready 주소 1개) · 오류 원인 1 changesets check sum · 26.7.0 복귀 후 마이그레이션 210·세션 4 그대로

[d3-secret-management]
  d3-secret-management__01-base64-not-encryption.txt
      실험대의 모든 비밀이 명령 네 줄로 평문 출력. describe 는 14 bytes 만 보여줘 착각을 준다
  d3-secret-management__02-at-rest.txt
      Encryption Status: Disabled · state.db 안에 비밀번호 평문 2회 일치 · 파드 안에서는 KEYCLOAK_CLIENT_SECRET=bff-lab-secret 환경변수 · default SA 는 읽을 수 없음

[d4-certificate-renewal]
  d4-certificate-renewal__01-certificate-state.txt
      SAN 3개(와일드카드 아님) · 체인 4단계, Verify return code: 0 · certbot-renew.timer enabled·active, 11시간 전 실행 · 88일 남음 · sudo: a password is required 로 강제 갱신 불가
  d4-certificate-renewal__05-control-no-injection.txt
      대조군 1 — 잡음 바닥. 0.2초 × 900회 / 180초 동안 900 전부 200, 오류 0. 중앙 98ms · p95 195ms. TLS 핸드셰이크 900/900 = 매 요청이 새 연결
  d4-certificate-renewal__06-inflight-control.txt
      대조군 2 — '진행 중이던 요청' 장치. 845KB 를 --limit-rate 20k 로 받아 요청을 42초간 살려 둔다. 무주입 시 200 · 845361바이트 · 연결수 1
  d4-certificate-renewal__07-renewal-hook-missing.txt
      nginx 는 reload 된 적이 없다 — 마스터 585·워커 586 이 같은 시각 기동, 22.4시간째. 유닛은 ExecStart=certbot -q renew 가 전부. crt.sh 는 SCT 가 박힌 인증서를 0건으로 답한다
  d4-certificate-renewal__08-inflight-artifact.txt
      76건 실패는 서버 탓이 아니다 — 같은 순간 폴링 49건 전부 200, 연결수=0, 50µs, 재현 0/100. 대조군이 오보를 막았다
  d4-certificate-renewal__09-serial-timeline.txt
      일련번호 564표본. 08:10:51 ~ 08:58:47 옛 것 → 08:58:52 새 것
  d4-certificate-renewal__10-reload-poll-window.txt
      reload 전후 60초 새 연결 원문 — 비200 0건, 최대 373ms
  d4-certificate-renewal__11-inflight-full.txt
      in-flight 전체 50건. 08:58:40 시작 요청이 08:58:52 reload 를 관통해 845361바이트 전량 수신
  d4-certificate-renewal__12-certbot-state.txt
      cert2.pem 09-04 17:22:13 기록됨 · renewal-hooks/{deploy,post,pre}/ 셋 다 비었음 · 플러그인 목록에 nginx 없음
  d4-certificate-renewal__13-verdict.txt
      판정 전문 — 38분 25초 공백(428회 관측) + reload 무중단(8856건 0실패)

[d4a-deploy-hook]
  d4a-deploy-hook__01-hook-verified.txt
      판정 전문. 훅 실행 로그 · 워커 PID 교체 · 시계 보정과 SCT 교차검증 · D-4 와의 대조
  d4a-deploy-hook__02-certbot-with-hook.txt
      certbot renew --force-renewal 원문. Hook 'deploy-hook' ran 과 all renewals succeeded
  d4a-deploy-hook__03-after-state.txt
      실행 후 nginx 프로세스와 서빙 인증서

[followup]
  followup__01-d2-forward-upgrade.txt
      D-2 정방향 26.7.0 → 26.7.3. 백업 396KB · 87회 요청 전부 200(무중단) · 마이그레이션 210 → 210(스키마 변경 없음) · 세션 3 유지 · Infinispan 16.0.12 → 16.0.14
  followup__02-d2-rollback-same-schema.txt
      스키마가 안 바뀌면 롤백이 된다 — 26.7.3 → 26.7.0 성공. 다만 전환 순간 000 1회(3초 타임아웃)
  followup__03-b4-role-propagation.txt
      B-4 ③ IdP 에서 값을 바꿔도 12회 요청·6초 동안 옛 값. 세션 삭제 후 재인증에서야 새 값
  followup__04-observability-gap.txt
      B층에 관측이 없다 — Prometheus 는 keycloak·kubelet·node-exporter·prometheus 만 긁는다. Redis·BFF·PostgreSQL 지표가 0개
  followup__05-command-reproducibility.txt
      재현 절차 명령을 실제로 돌려본 기록. 산문이던 측정 장치를 셸 실행형으로 바꾼 뒤 실행 검증 — 4건 중 1건(동시 20건 부하)이 조용히 실패했고 상주 탐침 방식으로 고쳐 20/20 수집

[keycloak-multinode-cluster]
  keycloak-multinode-cluster__01-cluster-formed.txt
      파드 배치·클러스터 뷰 로그·JGROUPS_PING·OIDC discovery·자원

[session-replication]
  session-replication__01-cross-node-session.txt
      클러스터 2멤버 확인 → keycloak-0 로그인 → 같은 sid 가 양쪽에서 보임 → keycloak-1 이 refresh 성공(200) → keycloak-1 로그아웃 → keycloak-0 갱신 실패(400) → DB 행 삭제 확인
  session-replication__02-cache-delta.txt
      로그인 하나를 사이에 둔 양쪽 노드의 캐시 계수기. keycloak-1 은 전부 +0
  session-replication__03-cache-ownership.txt
      로그인을 반대편에 몰아준 결과. 요청을 받은 노드에서만 엔트리가 는다. 캐시 합 7+5 = DB 12
  session-replication__04-read-path-sql.txt
      PostgreSQL 문장 로깅으로 잡은 keycloak-1 이 실제로 날린 SQL. SELECT ... FROM OFFLINE_USER_SESSION 로 남의 세션을 읽고 UPDATE ... where VERSION=$5 로 쓴다. 같은 트랜잭션에 SET LOCAL synchronous_commit TO OFF 가 들어 있다

[two-hop-proxy-headers]
  two-hop-proxy-headers__01-environment.txt
      수정 전 세 계층의 설정 스냅샷
  two-hop-proxy-headers__02-measurements.txt
      수정 전 측정 · 대조 실험 · 위조 테스트 · 분배
  two-hop-proxy-headers__04-after-fix.txt
      최종 측정 · 위조 테스트 · 분배
  two-hop-proxy-headers__05-networkpolicy.txt
      (원본 README 에 설명이 없다 — 파일 머리말 참조)