docs: replace prose placeholders in reproduction steps with executable commands
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 "<sid 목록>" — 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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f3f3a8da46
commit
74c9b3cea7
@@ -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 이 그 차이를 보여준다 — 문법적으로는 멀쩡했고, 실행하면 조용히 실패했다.
|
||||
@@ -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 증거가 없는 이유가 확인됐다** — 관측 대상에 애초에 없다. 스크린샷이 없는 것이 아니라 지표가 없다.
|
||||
|
||||
@@ -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 = '<sid>';
|
||||
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://<pod-ip>: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"'
|
||||
```
|
||||
|
||||
@@ -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 <src> -d <dst> --sport <sp> --dport <dp>
|
||||
# 위 출력의 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 <k1ip> -d <k0ip> --sport <sp> --dport 7800'
|
||||
ssh kc-lab-2 'sudo conntrack -D -p tcp -s <k0ip> -d <k1ip> --sport 7800 --dport <sp>'
|
||||
# 노드마다 자기 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"
|
||||
|
||||
@@ -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://<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.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
|
||||
|
||||
@@ -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 (<sid 목록>)"
|
||||
# 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))"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -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 <new-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
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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://<pod>: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://<pod>: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'
|
||||
|
||||
@@ -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://<pod>:8080/realms/master/protocol/openid-connect/token \
|
||||
-d grant_type=password -d client_id=admin-cli -d username=admin -d password=<pw> > /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://<pod>: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='<sid>'"
|
||||
-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)'"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -105,7 +105,7 @@ Bad Gateway
|
||||
파드 안에서 직접 받아 해결했다. **alpine 기반 JRE 이미지에 `wget` 이 있다.**
|
||||
|
||||
```bash
|
||||
kubectl -n keycloak-lab exec <bff-pod> -- 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 <bff-pod> -- 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 <pw>
|
||||
--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 <pod> -- 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 <bff-pod> -- 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']]"
|
||||
```
|
||||
|
||||
@@ -122,7 +122,7 @@ value: http://echo.header-lab.svc:8081
|
||||
## 2. 자동구성이 실제로 바뀌었는가 — B-0 의 방법을 다시 쓴다
|
||||
|
||||
```bash
|
||||
kubectl -n keycloak-lab exec <bff-pod> -- 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 <bff-pod> -- 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:<id>"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli ttl "bff:session:sessions:<id>"
|
||||
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
|
||||
|
||||
@@ -345,7 +345,7 @@ Spring Security 는 DDL 을 두 벌 제공한다.
|
||||
# + spring-boot-starter-jdbc, postgresql 의존성
|
||||
|
||||
# 2. 스키마 — PostgreSQL 판본을 써야 한다
|
||||
kubectl -n keycloak-lab exec <bff-pod> -- 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
|
||||
|
||||
@@ -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 = '<sid>';
|
||||
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 = '<sid>'"
|
||||
from offline_user_session us where us.user_session_id = '$SID'"
|
||||
|
||||
# 6. 정책 비교 — revokeRefreshToken 과 refreshTokenMaxReuse 를 바꿔가며 3~5 반복
|
||||
```
|
||||
|
||||
@@ -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 <bff-pod> -- wget -qO- http://localhost:8083/actuator/health
|
||||
kubectl -n keycloak-lab exec <bff-pod> -- 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}'
|
||||
|
||||
# ③ 복구
|
||||
|
||||
@@ -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/<old-id> -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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -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/<user-id>/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/<user-id>/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. 두 앱을 다시 연다 — 그대로 열리면 앱 세션이 독립적이라는 뜻
|
||||
```
|
||||
|
||||
@@ -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/<id> -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/<id> -r keycloak-patterns \
|
||||
Redis: 1 키
|
||||
|
||||
=== IdP 로그아웃 ===
|
||||
kcadm.sh create users/<id>/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/<id> -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/<user-id>/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 로
|
||||
|
||||
@@ -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 <bff-pod> -- 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 <pod> -- 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 \
|
||||
|
||||
@@ -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/<id> -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
|
||||
```
|
||||
|
||||
### 결과 — 반영되지 않는다
|
||||
|
||||
Reference in New Issue
Block a user