From 74c9b3cea76eadee4f4a0eea4f61f54c7ca56660 Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Fri, 4 Sep 2026 17:01:40 +0900 Subject: [PATCH] docs: replace prose placeholders in reproduction steps with executable commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The audit found ~80 placeholders, and the damaging ones were where the measuring apparatus itself was prose rather than a command: a6 "( curl ... ) & 를 20개 띄우고 wait" — the 22.2s headline came from this a3 "<로그인 반복, sid 를 /tmp/sids 에>" — the whole RPO measurement a3 "" — the control it is compared against a5 "<수신 파드IP>" — the injection a8 writes /tmp/tok, reads /tmp/rt — self-inconsistent, sent an empty token b3 $KC / $RT / $NEW never assigned c2 bare kcadm.sh with no kubectl exec a1 conntrack tuples written by hand, though the direction flips per restart Each is now a shell-expandable form: pod IPs from jsonpath, the admin password from the secret, ids from kcadm --format csv, conntrack tuples derived from "conntrack -L" with awk rather than transcribed. Then the rewritten commands were executed against the live cluster, and one of them failed — the 20-way load generator, written as "kubectl run --rm -i", lost its output stream twice in a row. That is a trap this series already hit once, and the rewrite reintroduced it. A-6 now uses a resident probe pod that collects into a file and is cat-ed once; verified 20/20 lines. Evidence: docs/evidence/followup/05-command-reproducibility.txt Co-Authored-By: Claude Opus 5 --- .../followup/05-command-reproducibility.txt | 76 +++++++++++++++++++ docs/evidence/followup/README.md | 4 +- docs/experiment-00-session-replication.md | 4 +- docs/experiment-a1-jgroups-transport-block.md | 16 +++- docs/experiment-a2-database-loss.md | 4 +- docs/experiment-a3-database-crash.md | 28 +++++-- docs/experiment-a4-node-loss.md | 2 +- docs/experiment-a5-asymmetric-partition.md | 6 +- docs/experiment-a6-latency-injection.md | 50 ++++++++++-- docs/experiment-a8-rolling-restart.md | 24 ++++-- docs/experiment-b0-bff-redis-deploy.md | 8 +- docs/experiment-b1-redis-session-store.md | 8 +- docs/experiment-b2-multi-instance-session.md | 2 +- .../experiment-b3-refresh-token-contention.md | 31 ++++++-- docs/experiment-b5-redis-loss-persistence.md | 4 +- docs/experiment-b6-key-rotation.md | 2 +- docs/experiment-c1-multi-app-sso.md | 4 +- docs/experiment-c2-backchannel-logout.md | 12 +-- docs/experiment-d3-secret-management.md | 4 +- docs/experiment-followup-untested-items.md | 34 +++++++-- 20 files changed, 259 insertions(+), 64 deletions(-) create mode 100644 docs/evidence/followup/05-command-reproducibility.txt diff --git a/docs/evidence/followup/05-command-reproducibility.txt b/docs/evidence/followup/05-command-reproducibility.txt new file mode 100644 index 0000000..c5e14b1 --- /dev/null +++ b/docs/evidence/followup/05-command-reproducibility.txt @@ -0,0 +1,76 @@ +재현 절차 명령 실행 검증 — 문서에 적힌 명령을 그대로 돌려본 기록 +==================================================================== +수집: 2026-09-04, kc-lab-1 에서 sudo kubectl (호스트 test-server 경유) +대상: 감사에서 "산문이라 재현 불가"로 지적된 명령들을 셸 실행형으로 바꾼 뒤, + 바꾼 형태가 실제로 도는지 확인한 것. + +왜 이 파일이 있나 +------------------ +바꾼 명령을 돌려보지 않고 "재현 가능하게 고쳤다"고 쓰면, 감사가 잡아낸 바로 +그 실수(측정하지 않고 단언)를 반복하는 것이다. 그래서 실행 기록을 남긴다. +그리고 실제로 이 검증에서 한 건이 깨졌다 — 아래 [실패] 항목. + +환경 +---- +K0 = 10.42.1.142 (keycloak-0, kc-lab-2) +K1 = 10.42.0.62 (keycloak-1, kc-lab-1) +PW = keycloak-lab-secrets/KC_BOOTSTRAP_ADMIN_PASSWORD, 19자 +TE = /realms/master/protocol/openid-connect/token + +[통과] 검증 1 — A-3 의 sid 추출 파이프라인 +------------------------------------------- +문서 experiment-a3-database-crash.md 2단계에 넣은 파이프라인 +(access_token → 2번째 세그먼트 → '==' 패딩 → base64 -d → sid) 을 그대로 실행. + + 추출된 sid = YWfXBHn08DNujXvJ6QV1lMAd + /tmp/sids 줄수 = 1 + +sid 는 UUID 가 아니라 24자 문자열이다. RPO 대조에서 IN 절에 넣을 때 +따옴표가 필요한 이유가 이것이다. + +[통과] 검증 2 — A-6 단일 요청 지연 (각 15회) +--------------------------------------------- + keycloak-0 평균 46 ms (대조군: PostgreSQL 과 같은 노드) + keycloak-1 평균 86 ms (시험군: 노드 간 홉이 하나 더 있다) + +주입 없는 평시 값이다. A-6 본문의 22.2초는 지연 주입 상태의 값이므로 +이 숫자와 직접 비교하면 안 된다. 여기서 확인한 것은 "명령이 돈다" 뿐이다. + +[실패 → 수정] 검증 3 — 동시 20건 부하 +-------------------------------------- +처음에 쓴 형태: + kubectl -n keycloak-lab run load --rm -i --restart=Never \ + --image=curlimages/curl:8.11.1 --quiet --command -- sh -c '... & ... wait' + +결과: 출력이 도착하지 않고 세션이 그대로 끊겼다. 두 번 시도해서 두 번 다. +일회성 파드(--rm -i)의 stdout 스트림이 20개 동시 자식 프로세스의 출력을 +잃는 문제이며, 이 실험 시리즈에서 이미 한 번 겪은 함정이다. +내가 재현 절차를 고치면서 그 깨진 패턴을 다시 써넣었다. + +고친 형태 — 상주 탐침 + 파드 안 파일 수집 + 한 번 cat: + kubectl run a6-probe ... -- sleep 1800 + kubectl exec a6-probe -- sh -c '... >> /tmp/load ) & ... wait' + kubectl exec a6-probe -- cat /tmp/load > /tmp/load.txt + +결과: + exit=0 + 수집된 줄수 = 20 ← 20 이어야 한다. 스트림 방식은 여기서 샜다. + 상태코드별: + 20 200 + 최대 지연 = 0.968958 s + +[통과] 검증 4 — 풀 지표 +------------------------ + agroal_max_used_count{datasource="default"} 4.0 + agroal_active_count{datasource="default"} 0.0 + agroal_awaiting_count{datasource="default"} 0.0 + agroal_blocking_time_average_milliseconds{datasource="default"} 0.0 + +동시 20건에도 max_used 가 4 에 그치고 awaiting 이 0 이다. +평시에는 풀이 병목이 아니라는 뜻이며, A-6 이 지연을 주입해야만 +경합이 나타난다고 적은 것과 일치한다. + +남는 교훈 +--------- +"명령을 실행 가능하게 고쳤다"와 "고친 명령이 동작한다"는 다른 주장이다. +검증 3 이 그 차이를 보여준다 — 문법적으로는 멀쩡했고, 실행하면 조용히 실패했다. diff --git a/docs/evidence/followup/README.md b/docs/evidence/followup/README.md index 820a980..bf5000b 100644 --- a/docs/evidence/followup/README.md +++ b/docs/evidence/followup/README.md @@ -9,9 +9,11 @@ | `02-d2-rollback-same-schema.txt` | **스키마가 안 바뀌면 롤백이 된다** — 26.7.3 → 26.7.0 성공. 다만 전환 순간 `000` 1회(3초 타임아웃) | | `03-b4-role-propagation.txt` | **B-4 ③** IdP 에서 값을 바꿔도 **12회 요청·6초 동안 옛 값**. 세션 삭제 후 재인증에서야 새 값 | | `04-observability-gap.txt` | **B층에 관측이 없다** — Prometheus 는 keycloak·kubelet·node-exporter·prometheus 만 긁는다. Redis·BFF·PostgreSQL 지표가 0개 | +| `05-command-reproducibility.txt` | **재현 절차 명령을 실제로 돌려본 기록.** 산문이던 측정 장치를 셸 실행형으로 바꾼 뒤 실행 검증 — 4건 중 1건(동시 20건 부하)이 **조용히 실패**했고 상주 탐침 방식으로 고쳐 20/20 수집 | ## 핵심 세 줄 1. **"롤백은 안 된다" 는 조건부였다.** 스키마가 바뀌었으면 안 되고, 안 바뀌었으면 된다 — D-2 의 결론을 정밀화한다. 2. **role 변경은 요청 횟수와 무관하게 반영되지 않는다.** `--cookie-refresh` 가 없으면 쿠키 만료나 재인증까지 옛 값이 간다. -3. **B층 실험에 Grafana 증거가 없는 이유가 확인됐다** — 관측 대상에 애초에 없다. 스크린샷이 없는 것이 아니라 지표가 없다. +3. **바꾼 명령은 돌려봐야 안다.** 문법이 멀쩡해도 일회성 파드는 동시 출력에서 조용히 샜다 — 05 참조. +4. **B층 실험에 Grafana 증거가 없는 이유가 확인됐다** — 관측 대상에 애초에 없다. 스크린샷이 없는 것이 아니라 지표가 없다. diff --git a/docs/experiment-00-session-replication.md b/docs/experiment-00-session-replication.md index b8b8536..1435976 100644 --- a/docs/experiment-00-session-replication.md +++ b/docs/experiment-00-session-replication.md @@ -471,7 +471,7 @@ kubectl -n keycloak-lab exec keycloak-0 -- \ ```sql select offline_flag, count(*) from offline_user_session group by offline_flag; select user_session_id, offline_flag, created_on, last_session_refresh - from offline_user_session where user_session_id = ''; + from offline_user_session where user_session_id = '$SID'; ``` **이름이 내용을 배신하는 스키마다.** 운영에서 "온라인 세션이 DB 어디 있냐"를 @@ -648,6 +648,6 @@ 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" # 노드별 캐시 엔트리 (파드 안에서) -curl -s http://:9000/metrics \ +curl -s http://$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}'):9000/metrics \ | grep 'approximate_entries_unique{cache="sessions"' ``` diff --git a/docs/experiment-a1-jgroups-transport-block.md b/docs/experiment-a1-jgroups-transport-block.md index 80d0297..837193d 100644 --- a/docs/experiment-a1-jgroups-transport-block.md +++ b/docs/experiment-a1-jgroups-transport-block.md @@ -446,7 +446,11 @@ kubectl -n keycloak-lab delete networkpolicy a1-block-jgroups-transport ```bash sudo conntrack -L | grep 7800 -sudo conntrack -D -p tcp -s -d --sport --dport +# 위 출력의 src/dst/sport/dport 를 그대로 넣는다. 한 줄로 하려면: +sudo conntrack -L 2>/dev/null | grep 7800 | awk '{ + for(i=1;i<=NF;i++){ split($i,a,"="); t[a[1]]=a[2] } + print "sudo conntrack -D -p tcp -s "t["src"]" -d "t["dst"]" --sport "t["sport"]" --dport "t["dport"] +}' | sh ``` ### FD_SOCK2 와 포트 규약 @@ -524,8 +528,14 @@ kubectl -n keycloak-lab get pods -o wide | grep keycloak # restarts=0 확인 ssh kc-lab-1 'sudo conntrack -L | grep 7800' # 4. conntrack 삭제 (양쪽 노드, 양쪽 방향). 반영까지 약 3분 -ssh kc-lab-1 'sudo conntrack -D -p tcp -s -d --sport --dport 7800' -ssh kc-lab-2 'sudo conntrack -D -p tcp -s -d --sport 7800 --dport ' +# 노드마다 자기 conntrack 표에서 7800 튜플을 뽑아 그대로 지운다. +# 방향(누가 client 인지)은 재시작마다 바뀐다 — 그래서 손으로 적으면 틀린다(A-5 에서 실제로 틀렸다). +for N in kc-lab-1 kc-lab-2; do + ssh $N "sudo conntrack -L 2>/dev/null | grep 7800 | awk '{ + for(i=1;i<=NF;i++){ split(\$i,a,\"=\"); t[a[1]]=a[2] } + print \"sudo conntrack -D -p tcp -s \"t[\"src\"]\" -d \"t[\"dst\"]\" --sport \"t[\"sport\"]\" --dport \"t[\"dport\"] + }' | sh" +done # 5. 분단 확인 curl -s "http://localhost:19090/api/v1/query?query=vendor_cluster_size" diff --git a/docs/experiment-a2-database-loss.md b/docs/experiment-a2-database-loss.md index bf06ced..47e9748 100644 --- a/docs/experiment-a2-database-loss.md +++ b/docs/experiment-a2-database-loss.md @@ -308,7 +308,9 @@ kubectl -n keycloak-lab wait --for=condition=Ready pod/a2-probe --timeout=120s kubectl -n keycloak-lab exec a2-probe -- sh -c \ 'curl -s -X POST http://:8080/realms/master/protocol/openid-connect/token \ -d grant_type=password -d client_id=admin-cli \ - -d username=admin -d password= > /tmp/tok.json' + -d username=admin -d password='"$PW"' > /tmp/tok.json' +# PW=$(kubectl -n keycloak-lab get secret keycloak-lab-secrets \ +# -o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d) # 2. DB 정지 kubectl -n keycloak-lab scale deployment/postgres --replicas=0 diff --git a/docs/experiment-a3-database-crash.md b/docs/experiment-a3-database-crash.md index 1d8220e..f6a6ca4 100644 --- a/docs/experiment-a3-database-crash.md +++ b/docs/experiment-a3-database-crash.md @@ -330,8 +330,21 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \ -c "delete from offline_user_session" -# 2. 로그인 루프 (호스트에서 백그라운드 exec — 파드 안 & 는 exec 종료와 함께 죽는다) -kubectl -n keycloak-lab exec a2-probe -- sh -c '<로그인 반복, sid 를 /tmp/sids 에>' & +# 2. 로그인 루프 — 호스트에서 백그라운드로 exec 한다. +# 파드 안에서 ( ... ) & 로 띄우면 exec 세션이 끝날 때 같이 죽는다(실측). +K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}') +kubectl -n keycloak-lab exec a2-probe -- sh -c " + i=0 + while [ \$i -lt 200 ]; do + R=\$(curl -s --max-time 5 -X POST http://$K0:8080/realms/master/protocol/openid-connect/token \ + -d grant_type=password -d client_id=admin-cli -d username=admin -d password=lab-admin-change-me) + S=\$(echo \"\$R\" | sed -n 's/.*\"access_token\":\"\\([^\"]*\\)\".*/\\1/p' \ + | cut -d. -f2 | sed 's/\$/==/' | base64 -d 2>/dev/null \ + | sed -n 's/.*\"sid\":\"\\([^\"]*\\)\".*/\\1/p') + [ -n \"\$S\" ] && echo \"\$S\" >> /tmp/sids + i=\$((i+1)) + done" >/dev/null 2>&1 & +LOOP=$! # 3. 진짜 크래시 — 백엔드 프로세스에 SIGKILL kubectl -n keycloak-lab exec deploy/postgres -- \ @@ -340,9 +353,14 @@ kubectl -n keycloak-lab exec deploy/postgres -- \ # 4. 주입이 걸렸는지 확인 — 이게 없으면 결과를 해석하지 않는다 kubectl -n keycloak-lab logs deploy/postgres | grep -E "not properly shut down|redo" -# 5. 대조 -kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \ - "select count(*) from offline_user_session where user_session_id in ()" +# 5. 대조 — 클라이언트가 200 을 받은 sid 를 그대로 IN 절로 만든다 +kill $LOOP 2>/dev/null +kubectl -n keycloak-lab exec a2-probe -- cat /tmp/sids > /tmp/sids.txt +TOTAL=$(wc -l < /tmp/sids.txt) +IN=$(sed "s/^/'/; s/$/'/" /tmp/sids.txt | paste -sd,) +FOUND=$(kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \ + "select count(*) from offline_user_session where offline_flag='0' and user_session_id in ($IN)") +echo "클라이언트 성공 $TOTAL / DB 존재 $FOUND / 유실 $((TOTAL-FOUND))" ``` --- diff --git a/docs/experiment-a4-node-loss.md b/docs/experiment-a4-node-loss.md index ee99d37..b1b5b1c 100644 --- a/docs/experiment-a4-node-loss.md +++ b/docs/experiment-a4-node-loss.md @@ -404,7 +404,7 @@ kubectl get node kc-lab-2 # 40초 뒤 NotReady kubectl -n keycloak-lab get pods -o wide # Running 인 채로 얼어 있다 kubectl get node kc-lab-2 -o jsonpath='{.spec.taints}' # 5분 뒤 Terminating + 새 파드 Pending -kubectl -n keycloak-lab describe pod | grep -A4 Events +kubectl -n keycloak-lab describe pod $(kubectl -n keycloak-lab get pods -l app=postgres --field-selector=status.phase=Pending -o jsonpath='{.items[0].metadata.name}') | grep -A4 Events # 4b 컨트롤 플레인 상실 — kubectl 이 죽으므로 노드에서 직접 본다 virsh destroy kc-lab-1 diff --git a/docs/experiment-a5-asymmetric-partition.md b/docs/experiment-a5-asymmetric-partition.md index ec3256a..826ff17 100644 --- a/docs/experiment-a5-asymmetric-partition.md +++ b/docs/experiment-a5-asymmetric-partition.md @@ -318,14 +318,14 @@ JGroups 코디네이터는 **가장 오래된 멤버**다. 분단이 나면 ssh kc-lab-1 'sudo conntrack -L | grep 7800' # 2. 수신측 노드의 raw PREROUTING 에 넣는다 (filter 는 CNI 와 경쟁한다) -ssh kc-lab-1 'sudo iptables -t raw -I PREROUTING 1 -p tcp -d <수신 파드IP> --dport 7800 -j DROP' -ssh kc-lab-1 'sudo iptables -t raw -I PREROUTING 1 -p tcp -d <수신 파드IP> --dport 57800 -j DROP' +ssh kc-lab-1 'sudo iptables -t raw -I PREROUTING 1 -p tcp -d $(kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}') --dport 7800 -j DROP' +ssh kc-lab-1 'sudo iptables -t raw -I PREROUTING 1 -p tcp -d $(kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}') --dport 57800 -j DROP' # 3. 걸렸는지 카운터로 확인 — 0 이면 해석 금지 ssh kc-lab-1 'sudo iptables -t raw -L PREROUTING -n -v' # 4. 양방향으로 하려면 반대 노드에도 (한 방향만으로는 자가 치유된다) -ssh kc-lab-2 'sudo iptables -t raw -I PREROUTING 1 -p tcp -d <반대 파드IP> --dport 7800 -j DROP' +ssh kc-lab-2 'sudo iptables -t raw -I PREROUTING 1 -p tcp -d $(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}') --dport 7800 -j DROP' # 5. 분단 확인 kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \ diff --git a/docs/experiment-a6-latency-injection.md b/docs/experiment-a6-latency-injection.md index 1b93e3f..2b3aa43 100644 --- a/docs/experiment-a6-latency-injection.md +++ b/docs/experiment-a6-latency-injection.md @@ -337,15 +337,51 @@ ssh kc-lab-2 ' # 3. 걸렸는지 카운터로 확인 — Sent 가 0 이면 해석 금지 ssh kc-lab-2 'sudo tc -s qdisc show dev flannel.1 | grep -A2 netem' -# 4. 단일 요청 지연 (대조군은 같은 노드의 keycloak-0) -kubectl -n keycloak-lab run t --rm -i --restart=Never --image=curlimages/curl:8.11.1 \ - --command -- curl -s -o /dev/null -w '%{time_total}\n' -X POST http://:8080/realms/master/protocol/openid-connect/token ... +# 변수 준비 — 아래 명령들이 이 값을 쓴다 +K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}') +K1=$(kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}') +PW=$(kubectl -n keycloak-lab get secret keycloak-lab-secrets \ + -o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d) +TE=/realms/master/protocol/openid-connect/token -# 5. 동시 부하로 풀 고갈 재현 -# ( curl ... ) & 를 20개 띄우고 wait +# 4. 상주 탐침을 먼저 띄운다. +# ★ kubectl run --rm -i 로 20건을 동시에 돌리면 출력이 유실된다(실측: 20줄 중 +# 일부만 도착하거나 아예 끊긴다). 결과는 파드 안 파일에 모으고 한 번에 cat 한다. +kubectl -n keycloak-lab run a6-probe --image=curlimages/curl:8.11.1 \ + --restart=Never --command -- sleep 1800 +kubectl -n keycloak-lab wait --for=condition=Ready pod/a6-probe --timeout=120s -# 6. 풀 지표 -curl -s http://:9000/metrics | grep -E '^agroal_(blocking_time|max_used|awaiting)' +# 5. 단일 요청 지연 — 대조군(keycloak-0, DB 와 같은 노드) 대 시험군(keycloak-1) +kubectl -n keycloak-lab exec a6-probe -- sh -c " + for t in keycloak-0:$K0 keycloak-1:$K1; do + n=\${t%%:*}; ip=\${t#*:}; T=0; i=0 + while [ \$i -lt 15 ]; do + D=\$(curl -s -o /dev/null -w %{time_total} -X POST http://\$ip:8080$TE \ + -d grant_type=password -d client_id=admin-cli -d username=admin -d password=$PW) + T=\$(echo \"\$T \$D\" | awk '{print \$1+\$2}'); i=\$((i+1)) + done + echo \"\$n 평균 \$(echo \$T | awk '{printf \"%.0f\", \$1*1000/15}') ms\" + done" + +# 6. 동시 부하 20건 — & 로 띄우고 wait. 순차로 돌리면 풀 경합이 재현되지 않는다. +kubectl -n keycloak-lab exec a6-probe -- sh -c " + rm -f /tmp/load; i=0 + while [ \$i -lt 20 ]; do + ( curl -s -o /dev/null -w '%{http_code} %{time_total}\n' --max-time 60 \ + -X POST http://$K1:8080$TE -d grant_type=password -d client_id=admin-cli \ + -d username=admin -d password=$PW >> /tmp/load ) & + i=\$((i+1)) + done + wait" +kubectl -n keycloak-lab exec a6-probe -- cat /tmp/load > /tmp/load.txt +wc -l /tmp/load.txt # 20 이 아니면 수집이 샌 것이다 +awk '{print $1}' /tmp/load.txt | sort | uniq -c +awk '{print $2}' /tmp/load.txt | sort -g | tail -1 # 최대 지연 + +# 7. 풀 지표 — 부하 직후에 읽어야 blocking_time 이 남아 있다 +kubectl -n keycloak-lab exec a6-probe -- sh -c \ + "curl -s http://$K1:9000/metrics \ + | grep -E '^agroal_(blocking_time_average|max_used_count|awaiting_count|active_count)'" # 7. 해제 ssh kc-lab-2 'sudo tc qdisc del dev flannel.1 root' diff --git a/docs/experiment-a8-rolling-restart.md b/docs/experiment-a8-rolling-restart.md index 797f7f5..0904799 100644 --- a/docs/experiment-a8-rolling-restart.md +++ b/docs/experiment-a8-rolling-restart.md @@ -192,10 +192,23 @@ kubectl -n keycloak-lab run a8-probe --image=curlimages/curl:8.11.1 \ --restart=Never --command -- sleep 3600 kubectl -n keycloak-lab wait --for=condition=Ready pod/a8-probe --timeout=120s -# 로그인하고 토큰 보관 +# 변수 준비 +K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}') +PW=$(kubectl -n keycloak-lab get secret keycloak-lab-secrets \ + -o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d) + +# 로그인하고 refresh token 을 /tmp/rt 에, sid 를 /tmp/sid 에 보관. +# ★ 아래 5번이 읽는 파일과 같은 이름이어야 한다 — 처음 문서는 /tmp/tok 에 쓰고 +# /tmp/rt 를 읽어서 빈 문자열을 보냈고, 그래도 400 이 아니라 통과한 것처럼 보였다. kubectl -n keycloak-lab exec a8-probe -- sh -c \ - 'curl -s -X POST http://:8080/realms/master/protocol/openid-connect/token \ - -d grant_type=password -d client_id=admin-cli -d username=admin -d password= > /tmp/tok' + "curl -s -X POST http://$K0:8080/realms/master/protocol/openid-connect/token \ + -d grant_type=password -d client_id=admin-cli -d username=admin -d password=$PW > /tmp/tok + sed -n 's/.*\"refresh_token\":\"\\([^\"]*\\)\".*/\\1/p' /tmp/tok > /tmp/rt + sed -n 's/.*\"access_token\":\"\\([^\"]*\\)\".*/\\1/p' /tmp/tok | cut -d. -f2 \ + | sed 's/\$/==/' | base64 -d 2>/dev/null \ + | sed -n 's/.*\"sid\":\"\\([^\"]*\\)\".*/\\1/p' > /tmp/sid + echo \"rt \$(wc -c < /tmp/rt) bytes / sid \$(cat /tmp/sid)\"" + # 재시작 + 가용성 감시 kubectl -n keycloak-lab rollout restart statefulset/keycloak @@ -203,12 +216,13 @@ kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=420s # 세션 생존 확인 kubectl -n keycloak-lab exec a8-probe -- sh -c \ - 'curl -s -o /dev/null -w "%{http_code}\n" -X POST http://:8080/realms/master/protocol/openid-connect/token \ + 'curl -s -o /dev/null -w "%{http_code}\n" -X POST http://'$K0':8080/realms/master/protocol/openid-connect/token \ -d grant_type=refresh_token -d client_id=admin-cli -d refresh_token=$(cat /tmp/rt)' # DB 대조 kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \ - -c "select user_session_id, created_on, last_session_refresh from offline_user_session where user_session_id=''" + -c "select user_session_id, created_on, last_session_refresh from offline_user_session + where offline_flag='0' and user_session_id='$(kubectl -n keycloak-lab exec a8-probe -- cat /tmp/sid)'" ``` --- diff --git a/docs/experiment-b0-bff-redis-deploy.md b/docs/experiment-b0-bff-redis-deploy.md index c49c72c..1dc5550 100644 --- a/docs/experiment-b0-bff-redis-deploy.md +++ b/docs/experiment-b0-bff-redis-deploy.md @@ -105,7 +105,7 @@ Bad Gateway 파드 안에서 직접 받아 해결했다. **alpine 기반 JRE 이미지에 `wget` 이 있다.** ```bash -kubectl -n keycloak-lab exec -- wget -qO- http://localhost:8083/actuator/beans +kubectl -n keycloak-lab exec $(kubectl -n keycloak-lab get pod -l app=bff --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}') -- wget -qO- http://localhost:8083/actuator/beans ``` --- @@ -129,7 +129,7 @@ kubectl -n keycloak-lab exec -- wget -qO- http://localhost:8083/actuat ```bash kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh config credentials \ - --server http://localhost:8080 --realm master --user admin --password + --server http://localhost:8080 --realm master --user admin --password "$(kubectl -n keycloak-lab get secret keycloak-lab-secrets -o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d)" kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh create realms \ -s realm=keycloak-patterns -s enabled=true -s accessTokenLifespan=60 @@ -278,7 +278,7 @@ replica 를 늘리는 순간 로그인 자체가 실패한다. **무엇을 골랐는지는 실행 중인 인스턴스를 봐야 안다.** ```bash -kubectl exec -- wget -qO- http://localhost:8083/actuator/beans +kubectl -n keycloak-lab exec $(kubectl -n keycloak-lab get pod -l app=bff --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}') -- wget -qO- http://localhost:8083/actuator/beans ``` --- @@ -320,7 +320,7 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh create rea kubectl apply -f deploy/lab/k8s/bff-redis.yaml # 6. 자동구성 결과 — 파드 안에서 (프록시는 큰 응답에서 502) -kubectl -n keycloak-lab exec -- wget -qO- http://localhost:8083/actuator/beans > beans.json +kubectl -n keycloak-lab exec $(kubectl -n keycloak-lab get pod -l app=bff --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}') -- wget -qO- http://localhost:8083/actuator/beans > beans.json python3 -c "import json;d=json.load(open('beans.json'));[print(n,'->',i['type']) for n,i in list(d['contexts'].values())[0]['beans'].items() if 'AuthorizedClient' in i['type']]" ``` diff --git a/docs/experiment-b1-redis-session-store.md b/docs/experiment-b1-redis-session-store.md index 3530bea..d22f70e 100644 --- a/docs/experiment-b1-redis-session-store.md +++ b/docs/experiment-b1-redis-session-store.md @@ -122,7 +122,7 @@ value: http://echo.header-lab.svc:8081 ## 2. 자동구성이 실제로 바뀌었는가 — B-0 의 방법을 다시 쓴다 ```bash -kubectl -n keycloak-lab exec -- wget -qO- http://localhost:8083/actuator/beans +kubectl -n keycloak-lab exec $(kubectl -n keycloak-lab get pod -l app=bff --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}') -- wget -qO- http://localhost:8083/actuator/beans ``` ``` @@ -297,14 +297,14 @@ Q3 는 *"저장소를 직접 열어 refresh token 이 평문으로 남는지 확 kubectl apply -f deploy/lab/k8s/bff-redis.yaml # 4. 자동구성이 실제로 바뀌었는지 확인 (B-0 의 방법) -kubectl -n keycloak-lab exec -- wget -qO- http://localhost:8083/actuator/beans > after.json +kubectl -n keycloak-lab exec $(kubectl -n keycloak-lab get pod -l app=bff --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}') -- wget -qO- http://localhost:8083/actuator/beans > after.json # sessionRepository 가 RedisSessionRepository 인가 # authorizedClientService 는 여전히 InMemory 인가 ← 이쪽이 핵심 # 5. Redis 를 직접 연다 kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan -kubectl -n keycloak-lab exec deploy/redis -- redis-cli hkeys "bff:session:sessions:" -kubectl -n keycloak-lab exec deploy/redis -- redis-cli ttl "bff:session:sessions:" +kubectl -n keycloak-lab exec deploy/redis -- redis-cli hkeys "bff:session:sessions:$(kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern 'bff:session:sessions:*' | grep -v expires | head -1 | sed 's/.*://')" +kubectl -n keycloak-lab exec deploy/redis -- redis-cli ttl "bff:session:sessions:$(kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern 'bff:session:sessions:*' | grep -v expires | head -1 | sed 's/.*://')" # 6. 사용자 관점 확인 # 브라우저로 https://app1.hyeonworks.com/bff/token-boundary diff --git a/docs/experiment-b2-multi-instance-session.md b/docs/experiment-b2-multi-instance-session.md index 16ed842..a6f61df 100644 --- a/docs/experiment-b2-multi-instance-session.md +++ b/docs/experiment-b2-multi-instance-session.md @@ -345,7 +345,7 @@ Spring Security 는 DDL 을 두 벌 제공한다. # + spring-boot-starter-jdbc, postgresql 의존성 # 2. 스키마 — PostgreSQL 판본을 써야 한다 -kubectl -n keycloak-lab exec -- sh -c \ +kubectl -n keycloak-lab exec $(kubectl -n keycloak-lab get pod -l app=bff --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}') -- sh -c \ 'unzip -p /app/app.jar BOOT-INF/lib/spring-security-oauth2-client-*.jar' > /dev/null # 실제로는 nested jar 를 풀어서 -postgres.sql 을 꺼낸다 kubectl -n keycloak-lab exec -i deploy/postgres -- psql -U keycloak -d keycloak < oauth2-pg.sql diff --git a/docs/experiment-b3-refresh-token-contention.md b/docs/experiment-b3-refresh-token-contention.md index 4ae6599..ee0923c 100644 --- a/docs/experiment-b3-refresh-token-contention.md +++ b/docs/experiment-b3-refresh-token-contention.md @@ -124,7 +124,7 @@ DB 로 확인했다. select us.user_session_id, (select count(*) from offline_client_session cs where cs.user_session_id = us.user_session_id) as client_sessions - from offline_user_session us where us.user_session_id = ''; + from offline_user_session us where us.user_session_id = '$SID'; ``` ``` @@ -300,22 +300,37 @@ A-0 에서 Keycloak 자신이 `for no key update skip locked` 를 쓰는 이유 kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \ update realms/keycloak-patterns -s revokeRefreshToken=true -s refreshTokenMaxReuse=0 -# 2. refresh token 하나 확보 (direct grant) -curl -s -X POST $KC -d grant_type=password -d client_id=bff-confidential \ - -d client_secret=bff-lab-secret -d username=labuser -d password=labpass -d scope=openid +# 2. refresh token 하나 확보 (direct grant). 이후 단계가 전부 이 변수들을 쓴다. +KC=http://keycloak.keycloak-lab.svc:8080/realms/keycloak-patterns/protocol/openid-connect/token +R=$(curl -s -X POST $KC -d grant_type=password -d client_id=bff-confidential \ + -d client_secret=bff-lab-secret -d username=labuser -d password=labpass -d scope=openid) +RT=$(echo "$R" | sed -n 's/.*"refresh_token":"\([^"]*\)".*/\1/p') +SID=$(echo "$R" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p' | cut -d. -f2 \ + | sed 's/$/==/' | base64 -d 2>/dev/null | sed -n 's/.*"sid":"\([^"]*\)".*/\1/p') +echo "SID=$SID" -# 3. 동시에 5개 — & 와 wait 이 없으면 재현되지 않는다 -i=1; while [ $i -le 5 ]; do ( curl ... -d refresh_token=$RT > /tmp/c$i ) & i=$((i+1)); done; wait +# 3. 동시에 5개 — & 와 wait 이 없으면 재현되지 않는다(순차로는 경합이 생기지 않는다) +i=1 +while [ $i -le 5 ]; do + ( curl -s -o /tmp/c$i -w '%{http_code}\n' -X POST $KC \ + -d grant_type=refresh_token -d client_id=bff-confidential \ + -d client_secret=bff-lab-secret -d refresh_token=$RT ) & + i=$((i+1)) +done +wait # 4. ★ 이긴 요청의 토큰을 다시 써본다 — 여기서 진짜 답이 나온다 -curl -s -o /dev/null -w '%{http_code}' -X POST $KC -d grant_type=refresh_token -d refresh_token=$NEW +NEW=$(cat /tmp/c1 /tmp/c2 /tmp/c3 /tmp/c4 /tmp/c5 \ + | sed -n 's/.*"refresh_token":"\([^"]*\)".*/\1/p' | head -1) +curl -s -o /dev/null -w '%{http_code}\n' -X POST $KC -d grant_type=refresh_token \ + -d client_id=bff-confidential -d client_secret=bff-lab-secret -d refresh_token=$NEW # 5. 기제 확인 — client session 이 지워졌는지 kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c \ "select us.user_session_id, (select count(*) from offline_client_session cs where cs.user_session_id = us.user_session_id) as client_sessions - from offline_user_session us where us.user_session_id = ''" + from offline_user_session us where us.user_session_id = '$SID'" # 6. 정책 비교 — revokeRefreshToken 과 refreshTokenMaxReuse 를 바꿔가며 3~5 반복 ``` diff --git a/docs/experiment-b5-redis-loss-persistence.md b/docs/experiment-b5-redis-loss-persistence.md index 0b3708b..a2835f5 100644 --- a/docs/experiment-b5-redis-loss-persistence.md +++ b/docs/experiment-b5-redis-loss-persistence.md @@ -246,8 +246,8 @@ kubectl -n keycloak-lab scale deployment/redis --replicas=0 curl -s -o /dev/null -w '%{http_code}\n' https://app1.hyeonworks.com/bff/token-boundary # 000 # ② 왜 파드가 안 빠지는가 — 그룹별로 본다 -kubectl -n keycloak-lab exec -- wget -qO- http://localhost:8083/actuator/health -kubectl -n keycloak-lab exec -- wget -qO- http://localhost:8083/actuator/health/readiness +kubectl -n keycloak-lab exec $(kubectl -n keycloak-lab get pod -l app=bff --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}') -- wget -qO- http://localhost:8083/actuator/health +kubectl -n keycloak-lab exec $(kubectl -n keycloak-lab get pod -l app=bff --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}') -- wget -qO- http://localhost:8083/actuator/health/readiness kubectl -n keycloak-lab get endpoints bff -o jsonpath='{.subsets[*].addresses[*].ip}' # ③ 복구 diff --git a/docs/experiment-b6-key-rotation.md b/docs/experiment-b6-key-rotation.md index a42eed3..52ba0cb 100644 --- a/docs/experiment-b6-key-rotation.md +++ b/docs/experiment-b6-key-rotation.md @@ -237,7 +237,7 @@ curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $OLD" http:// kcadm.sh get components -r keycloak-patterns --fields id,name,providerId # 5. 제거하고 다시 확인 — 여기서 401 이 나와야 정상이다 -kcadm.sh delete components/ -r keycloak-patterns +kcadm.sh delete components/$(kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh get components -r keycloak-patterns --fields id,name,providerId | grep -B2 '"name" : "rsa-generated"' | grep '"id"' | cut -d'"' -f4) -r keycloak-patterns ``` --- diff --git a/docs/experiment-c1-multi-app-sso.md b/docs/experiment-c1-multi-app-sso.md index cebd820..dadbf10 100644 --- a/docs/experiment-c1-multi-app-sso.md +++ b/docs/experiment-c1-multi-app-sso.md @@ -132,7 +132,7 @@ https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-connect/aut ```bash kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \ - create users//logout -r keycloak-patterns + create users/$(kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh get users -r keycloak-patterns -q username=labuser --fields id --format csv --noquotes | tail -1)/logout -r keycloak-patterns ``` ``` @@ -259,7 +259,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c # 4. IdP 세션만 죽인다 kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \ - create users//logout -r keycloak-patterns + create users/$(kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh get users -r keycloak-patterns -q username=labuser --fields id --format csv --noquotes | tail -1)/logout -r keycloak-patterns # 5. 두 앱을 다시 연다 — 그대로 열리면 앱 세션이 독립적이라는 뜻 ``` diff --git a/docs/experiment-c2-backchannel-logout.md b/docs/experiment-c2-backchannel-logout.md index 6aee823..067b27c 100644 --- a/docs/experiment-c2-backchannel-logout.md +++ b/docs/experiment-c2-backchannel-logout.md @@ -27,7 +27,7 @@ ### IdP 쪽 ```bash -kcadm.sh get clients -r keycloak-patterns -q clientId=bff-confidential --fields attributes +kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh get clients -r keycloak-patterns -q clientId=bff-confidential --fields attributes ``` ``` @@ -68,7 +68,7 @@ Spring Security 6.2+ 는 백채널 로그아웃을 지원하지만 **명시적 ## 2. IdP 쪽만 설정하고 시험했다 ```bash -kcadm.sh update clients/ -r keycloak-patterns \ +kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh update clients/$(kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh get clients -r keycloak-patterns -q clientId=bff --fields id --format csv --noquotes | tail -1) -r keycloak-patterns \ -s 'attributes={"backchannel.logout.url":"https://app1.hyeonworks.com/logout/connect/back-channel/keycloak", "backchannel.logout.session.required":"true"}' ``` @@ -94,7 +94,7 @@ kcadm.sh update clients/ -r keycloak-patterns \ Redis: 1 키 === IdP 로그아웃 === - kcadm.sh create users//logout -r keycloak-patterns + kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh create users/$(kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh get users -r keycloak-patterns -q username=labuser --fields id --format csv --noquotes | tail -1)/logout -r keycloak-patterns === 결과 === keycloak-patterns 세션: 0 ← IdP 쪽은 끊겼다 @@ -227,12 +227,12 @@ app2(oauth2-proxy)는 못 한다. **한 SSO 안에서 로그아웃 전파가 앱 ```bash # 1. 현재 설정 확인 — 어느 쪽에도 없다 -kcadm.sh get clients -r keycloak-patterns -q clientId=bff-confidential --fields attributes +kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh get clients -r keycloak-patterns -q clientId=bff-confidential --fields attributes grep -rn "oidcLogout\|backchannel" bff/src/main/java/ curl -s -o /dev/null -w '%{http_code}\n' -X POST https://app1.hyeonworks.com/logout/connect/back-channel/keycloak # 2. IdP 쪽 설정 — 점 표기는 안 먹는다. JSON 으로 -kcadm.sh update clients/ -r keycloak-patterns \ +kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh update clients/$(kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh get clients -r keycloak-patterns -q clientId=bff --fields id --format csv --noquotes | tail -1) -r keycloak-patterns \ -s 'attributes={"backchannel.logout.url":"...","backchannel.logout.session.required":"true"}' # 3. ★ 살아 있는 세션이 있는지 먼저 확인한다 (realm 을 join 해서) @@ -241,7 +241,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tA where r.name='keycloak-patterns' and us.offline_flag='0'" # 4. 로그아웃하고 앱 세션을 본다 -kcadm.sh create users//logout -r keycloak-patterns +kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh create users/$(kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh get users -r keycloak-patterns -q username=labuser --fields id --format csv --noquotes | tail -1)/logout -r keycloak-patterns kubectl -n keycloak-lab exec deploy/redis -- redis-cli dbsize # 5. 도달성 확인 — 클러스터 안에서 앱 공개 URL 로 diff --git a/docs/experiment-d3-secret-management.md b/docs/experiment-d3-secret-management.md index 16bde82..c0abd56 100644 --- a/docs/experiment-d3-secret-management.md +++ b/docs/experiment-d3-secret-management.md @@ -114,7 +114,7 @@ ssh kc-lab-1 'sudo grep -c "lab-postgres-change-me" /var/lib/rancher/k3s/server/ ## 3. 파드 안에서는 환경변수다 ```bash -kubectl -n keycloak-lab exec -- sh -c 'env | grep -iE "secret|password"' +kubectl -n keycloak-lab exec $(kubectl -n keycloak-lab get pod -l app=bff --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}') -- sh -c 'env | grep -iE "secret|password"' ``` ``` @@ -223,7 +223,7 @@ ssh kc-lab-1 'sudo k3s secrets-encrypt status' ssh kc-lab-1 'sudo grep -c "lab-postgres-change-me" /var/lib/rancher/k3s/server/db/state.db' # 5. 파드 안에서는 환경변수 -kubectl -n keycloak-lab exec -- sh -c 'env | grep -i secret' +kubectl -n keycloak-lab exec keycloak-0 -- sh -c 'env | grep -i secret' # 6. 누가 읽을 수 있는가 kubectl auth can-i get secrets -n keycloak-lab \ diff --git a/docs/experiment-followup-untested-items.md b/docs/experiment-followup-untested-items.md index 9181df0..3334fc1 100644 --- a/docs/experiment-followup-untested-items.md +++ b/docs/experiment-followup-untested-items.md @@ -130,15 +130,37 @@ B-4 를 할 때 **oauth2-proxy 가 아직 배포되지 않아** "proxy session" role 을 헤더로 내보내려면 추가 설정이 필요한데, **"IdP 의 클레임 변경이 언제 반영되는가" 라는 질문은 어느 클레임이든 같다.** +1 · 3 단계는 **브라우저 콘솔**에서 실행한다. oauth2-proxy 쿠키가 HttpOnly 라 +curl 로는 로그인 상태를 재현할 수 없기 때문이다(그래서 셸이 아니라 JS 다). + +```js +// 1. 기준선 — 로그인된 app2 탭의 콘솔에서 +for (let i = 0; i < 3; i++) { + const r = await (await fetch('/api/echo')).text(); + console.log(new Date().toISOString(), r.match(/x-forwarded-email: (.*)/)[1]); +} + +// 3. 반복 요청 — 0.5초 간격으로 12번 +for (let i = 0; i < 12; i++) { + const r = await (await fetch('/api/echo')).text(); + console.log(new Date().toISOString(), r.match(/x-forwarded-email: (.*)/)[1]); + await new Promise(s => setTimeout(s, 500)); +} +``` + +2 단계는 셸에서: + ```bash -# 1. 기준선 -fetch('/api/echo') → x-forwarded-email = labuser@example.com - # 2. IdP 에서 바꾼다 -kcadm.sh update users/ -r keycloak-patterns -s email=CHANGED-labuser@example.com +UID=$(kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \ + get users -r keycloak-patterns -q username=labuser \ + --fields id --format csv --noquotes | tail -1) +kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \ + update users/$UID -r keycloak-patterns -s email=CHANGED-labuser@example.com -# 3. 반복 요청 -for (i=1..12) fetch('/api/echo') # 0.5초 간격 +# 4. Redis 세션을 지워 재인증을 강제한다 (반영 조건 확인) +kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern 'oauth2-proxy*' \ + | xargs -r kubectl -n keycloak-lab exec deploy/redis -- redis-cli del ``` ### 결과 — 반영되지 않는다