Compare commits

...
Author SHA1 Message Date
DongHyeonkaandClaude Opus 5 8f6d67df35 docs: A-7 — three results invert when persistent sessions are turned off
Disabling persistent-user-sessions moves the session from PostgreSQL into the cluster, and the A-1 and A-8 outcomes flip to 400 Session not active while a new login during database loss starts working. The control group in each case still returns 200, so the injections cut only what they were meant to cut.

This is the pair that makes the A layer legible: the conventional wisdom that sessions ride TCP 7800 is correct for Keycloak 24 and earlier, and the mistake is applying it to 26 without checking the version.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 13:33:33 +09:00
9 changed files with 454 additions and 0 deletions
@@ -9,3 +9,76 @@
[ 20184ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 32473ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 50697ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 65961ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 85590ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 99644ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 115524ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 130773ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 134354ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 142549ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 151361ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 155246ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 165901ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 169379ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 180952ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 190372ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 193657ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 205737ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 220582ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 225496ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 240339ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 244029ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 264211ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 273424ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 284893ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 292571ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 305778ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 306804ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 324378ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 335169ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 337968ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 352967ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 364258ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 379409ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 399590ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 403681ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 406340ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 421033ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 428667ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 429483ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 437573ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 442286ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 445865ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 456312ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 464043ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 480382ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 491438ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 494608ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 508231ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 528305ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 529632ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 538033ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 550079ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 566347ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 586157ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 603877ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 615779ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 624657ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 629879ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 637255ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 643298ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 650047ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 668080ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 669816ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 675422ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 677001ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 686611ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 692655ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 713114ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 723787ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 730443ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 751427ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 757583ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 776007ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 792298ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
[ 803509ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
@@ -0,0 +1,18 @@
=== 비교를 위해 세션을 비운다 ===
DELETE 151
=== volatile 모드로 전환 ===
namespace/keycloak-lab unchanged
secret/keycloak-lab-secrets configured
persistentvolumeclaim/postgres-data unchanged
deployment.apps/postgres unchanged
service/postgres unchanged
statefulset.apps/keycloak configured
service/keycloak-headless unchanged
service/keycloak unchanged
ingress.networking.k8s.io/keycloak unchanged
Waiting for 1 pods to be ready...
partitioned roll out complete: 2 new pods have been updated...
=== [검증] 정말 꺼졌는가 ===
["start","--features-disabled=persistent-user-sessions"]
@@ -0,0 +1,15 @@
keycloak-0=10.42.1.94 keycloak-1=10.42.0.45
=== [A-0 재실행] keycloak-0 에만 로그인 5회 → 캐시가 어디에 담기는가 ===
로그인완료
keycloak-0 sessions 캐시 5.0 건
keycloak-1 sessions 캐시 0.0 건
=== DB 에는 들어갔는가 (persistent 였을 때는 5건이 들어갔다) ===
offline_flag | count
--------------+-------
(0 rows)
=== 교차 노드 세션은 되는가 ===
keycloak-0 로그인 → keycloak-1 에서 refresh HTTP 200
@@ -0,0 +1,13 @@
=== [A-8 재실행] 재시작 전 로그인 ===
sid = aVwYnzKZFFvMqD3bpSeiILuM
=== 롤링 재시작 ===
statefulset.apps/keycloak restarted
partitioned roll out complete: 2 new pods have been updated...
=== ★ 재시작 전 토큰이 아직 통하는가 (persistent 였을 때는 200) ===
keycloak-0 에서 refresh HTTP 400
--- 오류 본문 ---
{"error":"invalid_grant","error_description":"Session not active"}
=== 캐시 상태 ===
keycloak-1 sessions 캐시 1.0 건
@@ -0,0 +1,21 @@
=== [A-1 재실행] volatile 에서 7800 을 막으면 ===
keycloak-0=10.42.1.99 keycloak-1=10.42.0.46
[대조군] 차단 전 교차 노드 refresh
keycloak-1 에서 refresh HTTP 200
차단 적용 (A-5 에서 확인한 raw 테이블 방식, 양방향)
분단이 성립할 때까지 대기...
+25초 cluster_size(k0 k1) = [2.0 2.0 ]
+50초 cluster_size(k0 k1) = [1.0 ]
+75초 cluster_size(k0 k1) = [1.0 ]
+100초 cluster_size(k0 k1) = []
+125초 cluster_size(k0 k1) = [1.0 ]
+150초 cluster_size(k0 k1) = [1.0 ]
+175초 cluster_size(k0 k1) = [1.0 ]
+200초 cluster_size(k0 k1) = []
=== ★ 분단 상태에서 교차 노드 세션 (persistent 였을 때는 200) ===
keycloak-0 로그인 → keycloak-0 에서 refresh HTTP 200 ← 대조군
keycloak-0 로그인 → keycloak-1 에서 refresh HTTP 400 ← 시험군
--- 시험군 오류 본문 ---
{"error":"invalid_grant","error_description":"Session not active"}
@@ -0,0 +1,12 @@
차단 해제, 클러스터 재형성 대기...
=== [A-2 재실행] volatile 에서 DB 를 내리면 — 세션이 메모리에 있으니 살아남는가? ===
DB 정지 전 로그인 완료
deployment.apps/postgres scaled
postgres 정지
① 캐시를 가진 노드에서 refresh HTTP 500
② 새 로그인 HTTP 200
=== DB 복구 후 원복 ===
deployment.apps/postgres scaled
deployment "postgres" successfully rolled out
@@ -0,0 +1,12 @@
=== persistent 모드로 원복 ===
statefulset.apps/keycloak configured
partitioned roll out complete: 2 new pods have been updated...
=== [검증] persistent 로 돌아왔는가 — 로그인 후 DB 에 행이 생기는가 ===
["start"]
로그인
DB 온라인 세션: 1 건 (1 이면 persistent 복귀)
keycloak-0 1/1 Running 0 67s
keycloak-1 1/1 Running 0 89s
postgres-7b474b88c8-t6rrf 1/1 Running 0 2m8s
외부 진입점 HTTP 200
@@ -0,0 +1,23 @@
# A-7 — volatile 모드 비교 증거
2026-09-04 13:4514:15 KST
해설: [`docs/experiment-a7-volatile-comparison.md`](../../experiment-a7-volatile-comparison.md)
| 파일 | 무엇을 보여주는가 |
|---|---|
| `01-switch-to-volatile.txt` | `--features-disabled=persistent-user-sessions` 적용, args 확인 |
| `02-a0-rerun.txt` | **DB 0건**인데 교차 노드 refresh `200` — 경로가 DB 에서 클러스터로 바뀌었다 |
| `03-a8-rerun-restart.txt` | **롤링 재시작 후 `400 Session not active`** — persistent 에서는 `200` 이었다 |
| `04-a1-rerun-partition.txt` | **7800 차단 시 교차 노드 `400`** — persistent 에서는 `200`. 대조군(같은 노드)은 `200` 유지 |
| `05-a2-rerun-db-loss.txt` | DB 정지 중 **새 로그인 `200`**(persistent 에서는 500), refresh 는 `500` |
| `06-restore-persistent.txt` | 원복 확인 — `args: ["start"]`, 로그인 후 DB 1건, 외부 200 |
## 뒤집힌 결과
| 실험 | persistent | volatile |
|---|---|---|
| A-1 7800 차단 후 교차 refresh | `200` | **`400`** |
| A-8 롤링 재시작 후 refresh | `200` | **`400`** |
| A-2 DB 정지 중 새 로그인 | `500` | **`200`** |
**같은 주입, 같은 관측, 정반대 결과.** A층 전체가 버전 조건부임을 보여주는 대조군이다.
+267
View File
@@ -0,0 +1,267 @@
# A-7 — 옛 방식(volatile)이었다면 무엇이 달라지는가
브랜치 `feature/keycloak-a7-volatile-comparison` ·
증거 [`docs/evidence/a7-volatile-comparison/`](evidence/a7-volatile-comparison/) ·
2026-09-04 13:4514:15 KST
**A층의 결론 전체가 "Keycloak 26 기본값"이라는 전제 위에 있다.**
전제를 뒤집어 같은 실험을 반복한 것이 이 실험이다.
---
## 0. 결론부터 — 비교표
| 실험 | persistent (KC 26 기본) | **volatile (KC 24 이전 방식)** |
|---|---|---|
| **A-0** 세션 저장 위치 | DB 에 행이 생긴다 | **DB 0건.** 메모리에만 |
| **A-1** 7800 차단 후 교차 노드 refresh | **`200`** — 안 깨진다 | **`400 Session not active`** — 깨진다 |
| **A-8** 롤링 재시작 후 refresh | **`200`** — 세션 생존 | **`400 Session not active`** — 전원 로그아웃 |
| **A-2** DB 정지 중 새 로그인 | `500` | **`200`** — 된다 |
| A-2 DB 정지 중 refresh | `500` | `500` |
**세 개가 정반대로 뒤집혔다.** 예측한 그대로다.
> **"세션 공유는 7800 을 안 탄다"는 A-1 의 결론은 버전에 달린 사실이다.**
> 인터넷 자료 대부분이 24 이전 기준이므로 **거기서는 통념이 맞다.**
> 틀린 것은 자료가 아니라 **버전을 확인하지 않고 적용하는 것**이다.
---
## 1. 전환 방법
```bash
# 이 버전에서 끌 수 있는지부터 확인한다
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kc.sh build --help-all \
| tr ',' '\n' | grep -i persistent
# persistent-user-sessions[:v1] ← 목록에 있다
```
```yaml
# deploy/lab/k8s/keycloak-cluster.yaml
args: ["start", "--features-disabled=persistent-user-sessions"]
```
```bash
kubectl apply -f deploy/lab/k8s/keycloak-cluster.yaml
kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=500s
```
**빌드 옵션이므로 기동 시 재빌드가 일어나 평소보다 오래 걸린다.**
---
## 2. A-0 재실행 — 세션이 DB 에 안 들어간다
```
=== keycloak-0 에만 로그인 5회 ===
keycloak-0 sessions 캐시 5.0 건
keycloak-1 sessions 캐시 0.0 건
=== DB 에는 들어갔는가 (persistent 였을 때는 5건) ===
offline_flag | count
--------------+-------
(0 rows) ← 0 건
=== 교차 노드 세션은 되는가 ===
keycloak-0 로그인 → keycloak-1 에서 refresh HTTP 200
```
**DB 는 비어 있는데 교차 노드가 된다.** persistent 때와 겉보기 결과가 같지만
**경로가 완전히 다르다** — 이제는 DB 가 아니라 **클러스터를 타고** 있다.
> 캐시 엔트리 수가 `5 / 0` 인 것은 persistent 때와 같다.
> `approximate_entries_unique` 는 **그 노드가 소유한 엔트리**만 세므로,
> 백업본을 들고 있어도 0 으로 보인다. **이 지표만으로는 두 모드를 구분할 수
> 없다** — 구분하려면 7800 을 끊어봐야 한다. 그게 다음 절이다.
---
## 3. A-1 재실행 — **정반대다**
A-5 에서 확립한 `raw` 테이블 양방향 차단을 그대로 썼다.
```
[대조군] 차단 전 교차 노드 refresh HTTP 200
차단 적용 → 분단 성립
+25초 cluster_size(k0 k1) = [2.0 2.0]
+50초 cluster_size(k0 k1) = [1.0 ...] ← 갈라졌다
=== 분단 상태에서 ===
keycloak-0 로그인 → keycloak-0 에서 refresh HTTP 200 ← 대조군
keycloak-0 로그인 → keycloak-1 에서 refresh HTTP 400 ← 시험군
{"error":"invalid_grant","error_description":"Session not active"}
```
**대조군이 200 인 것이 중요하다.** 차단이 모든 것을 망가뜨린 게 아니라
**교차 노드만** 끊었다는 증거다.
```
persistent : 세션 ── PostgreSQL ──▶ 양쪽이 본다 7800 무관
volatile : 세션 ── 클러스터(7800) ─▶ 상대에게 간다 7800 필수
```
**같은 주입, 같은 관측, 정반대 결과.** 이 한 쌍이 A층 전체의 근거다.
---
## 4. A-8 재실행 — 배포가 곧 로그아웃
```
=== 재시작 전 로그인 ===
sid = aVwYnzKZFFvMqD3bpSeiILuM
=== 롤링 재시작 ===
partitioned roll out complete: 2 new pods have been updated...
=== ★ 재시작 전 토큰이 아직 통하는가 (persistent 였을 때는 200) ===
keycloak-0 에서 refresh HTTP 400
{"error":"invalid_grant","error_description":"Session not active"}
```
**배포할 때마다 전원 로그아웃된다.**
| | persistent | volatile |
|---|---|---|
| 배포 | 자유롭다 | **모든 사용자가 다시 로그인** |
| 파드 재시작 (OOM, 노드 교체) | 무해 | **그 노드가 처리하던 세션 소멸** |
| 무중단 여부 | 무중단 (A-8) | 접속은 되지만 **로그인 상태가 사라진다** |
> **A-8 에서 "이것이 persistent 를 켜는 진짜 이유"라고 썼는데, 여기서 증명된다.**
> 24 이전 버전을 쓰는 곳에서 "배포하면 로그아웃된다"가 당연하게 여겨졌던 이유다.
---
## 5. A-2 재실행 — 예상 못 한 비대칭
```
① 캐시를 가진 노드에서 refresh HTTP 500
② 새 로그인 HTTP 200 ← persistent 에서는 500 이었다
```
**새 로그인은 되는데 refresh 가 안 된다.** 순서가 거꾸로다.
### 왜 새 로그인이 되는가
```
로그인에 필요한 것
├─ realm 설정 → Infinispan `realms` 캐시에 있다
├─ 사용자 자격 → `users` 캐시에 있다
└─ 세션 저장 → volatile 이므로 메모리
→ DB 없이 완결된다
```
**A-2 에서 persistent 로 했을 때 로그인이 실패한 이유는 "세션을 DB 에 써야
해서"였다.** 그 쓰기가 없어지니 로그인이 통과한다.
### refresh 가 500 인 이유 — 가설
**측정은 확실하지만 원인은 확정하지 못했다.** 유력한 후보는
`REVOKED_TOKEN` 테이블이다 — refresh token 회전에서 **이미 쓴 토큰인지**
확인하려면 그 테이블을 봐야 하고, 그 경로는 캐시되지 않는다.
```
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c "\dt" | grep revoked
public | revoked_token | table | keycloak
```
**확정하려면 A-3 에서 쓴 문장 로깅을 켜고 다시 재현해야 한다.** 여기서는
**가설로 남긴다.**
> **volatile 이 "DB 없이 돌아간다"는 뜻은 아니다.**
> realm·사용자·클라이언트·취소 토큰은 **여전히 DB 에 있다.**
> 세션만 메모리로 옮긴 것이다.
---
## 6. 원복
```bash
# args 를 되돌린다
args: ["start"]
kubectl apply -f deploy/lab/k8s/keycloak-cluster.yaml
```
```
=== [검증] persistent 로 돌아왔는가 ===
["start"]
DB 온라인 세션: 1 건 (로그인 1회 후 → persistent 복귀 확인)
외부 진입점 HTTP 200
```
**전환 자체는 설정 한 줄이고 되돌리기도 한 줄이다.** 다만 전환 시점에
**기존 세션은 전부 사라진다** (저장 위치가 바뀌므로).
---
## 7. 개념
### 두 모드의 데이터 흐름
```
persistent (KC 25+, 26 기본)
로그인 ──▶ PostgreSQL (진실) + 로컬 캐시 (사본)
조회 ──▶ 캐시에 없으면 DB
공유 ──▶ 같은 DB 를 보는 것
volatile (KC 24 이전)
로그인 ──▶ Infinispan (진실)
조회 ──▶ 클러스터에서 찾는다
공유 ──▶ 7800 을 통한 복제
```
### 무엇을 맞바꾸는가
| | persistent | volatile |
|---|---|---|
| 재시작 내구성 | **있다** | 없다 |
| 7800 의존 | 낮다 (무효화만) | **높다 (세션 자체)** |
| DB 부하 | **로그인·refresh 마다 쓰기** | 세션 관련 없음 |
| 노드 확장 | DB 가 병목 | **복제 트래픽이 N² 로 증가** |
| 지연 민감도 | **DB 왕복에 민감** (A-6) | 클러스터 왕복에 민감 |
**26 이 기본을 바꾼 이유가 이 표에 있다** — 운영에서 가장 아픈 것이
"배포하면 로그아웃"이었기 때문이다.
---
## 8. 재현 절차 (명령어)
```bash
# 1. 끌 수 있는지 확인
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kc.sh build --help-all \
| tr ',' '\n' | grep -i persistent
# 2. 세션을 비우고 전환 (비교 기준을 맞추기 위해)
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "delete from offline_user_session"
# args: ["start", "--features-disabled=persistent-user-sessions"]
kubectl apply -f deploy/lab/k8s/keycloak-cluster.yaml
kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=500s
# 3. volatile 확인 — 로그인 후 DB 가 비어 있어야 한다
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "select offline_flag, count(*) from offline_user_session group by offline_flag"
# 4. A-1 / A-8 을 그대로 반복한다 (증거 파일 참조)
# 5. 원복
# args: ["start"]
kubectl apply -f deploy/lab/k8s/keycloak-cluster.yaml
```
---
## 9. 이 실험이 A층에 남기는 것
| | |
|---|---|
| **A-0~A-6 의 결론은 모두 "26 기본값" 조건부다** | 버전이 다르면 답이 다르다 |
| **A-1 이 통념과 어긋난 이유가 확정됐다** | 통념은 24 이전에서 맞다 |
| **버전 확인이 1순위** | `kc.sh --version` 을 먼저 본다 |
| **volatile 이 DB 독립을 뜻하지 않는다** | realm·사용자·취소 토큰은 여전히 DB |
### 미해결로 남긴 것
**volatile 에서 refresh 만 500 이 되는 이유**`REVOKED_TOKEN` 조회 가설을
세웠지만 확정하지 못했다. A-3 의 문장 로깅 기법으로 재현하면 답이 나온다.