Files
keycloak-pattern/docs/experiment-c2-backchannel-logout.md
T
DongHyeonkaandClaude Opus 5 cdac9b8178 docs: give the twelve experiments that had no architecture diagram one
An audit against the standard the series set — concepts, procedure,
commands, architecture diagram, evidence table, terminal output — found the
three new experiments met it while twelve of the original ones had no
diagram at all: A-0, A-1, A-3, A-4, A-5, A-6, A-8, B-0, B-2, B-7, C-2, D-2.

Each now has one drawn from what that experiment actually found, not filler:
A-0 shows sharing going through PostgreSQL rather than between the caches;
A-3 the gap between the 200 and the WAL flush, with both failed injections;
A-5 the three silent injection failures; A-6 the two places latency is
multiplied; B-0 the repository keyed by principal with no session id; B-2
the primary key that causes the overwrite; D-2 why the rolling update
stopped the accident halfway.

Also corrected the index's stale claim of 11 experiments without a
screenshot — it is 14, and the reason is recorded: those experiments were
measured from terminals, the database and logs, and the observability stack
does not scrape Redis, the BFF or PostgreSQL, so there is no console to
photograph.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 21:44:36 +09:00

11 KiB
Raw Blame History

C-2 — 로그아웃이 전 앱에 퍼지는가 (백채널 로그아웃)

브랜치 feature/keycloak-c2-backchannel-logout · 증거 docs/evidence/c2-backchannel-logout/ · 2026-09-04 16:3016:55 KST

선행: C-1IdP 로그아웃이 앱에 전파되지 않는다를 관측했다


구조

C-2 — 백채널 로그아웃에 필요한 양쪽

다이어그램 규약은 diagrams/_style.md. 실험대 전체 구조는 diagrams/lab-topology.svg.


0. 결론부터

확인 결과
백채널 로그아웃이 설정되어 있었는가 아니다. 두 클라이언트 모두 backchannelLogoutUrl 없음
BFF 에 그 엔드포인트가 있는가 아니다. 소스에 oidcLogout 설정이 없다
IdP 쪽만 설정하면 되는가 ★ 안 된다. 앱 세션이 그대로 남았다
Keycloak 이 앱 URL 에 닿기는 하는가 닿는다 (HTTP 200) — 네트워크 문제가 아니다

C-1 이 관측한 "로그아웃이 안 퍼진다" 의 원인은 단순했다 — 아무도 구현하지 않았다.


1. 현재 상태 — 어느 쪽에도 없다

IdP 쪽

kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh get clients -r keycloak-patterns -q clientId=bff-confidential --fields attributes
  frontchannelLogout : false
  (backchannel.logout.url 없음)

oauth2-proxy 클라이언트도 마찬가지였다.

앱 쪽

grep -rn "oidcLogout\|backchannel" bff/src/main/java/

아무것도 안 나온다.

Spring Security 6.2+ 는 백채널 로그아웃을 지원하지만 명시적으로 켜야 한다.

.oidcLogout(oidc -> oidc.backChannel(Customizer.withDefaults()))

이 설정이 없으면 /logout/connect/back-channel/{registrationId} 경로가 생기지 않는다.

  /logout/connect/back-channel/keycloak      HTTP 302   ← 로그인으로 리다이렉트
  /backchannel-logout                        HTTP 302
  /oauth2/sign_out                           HTTP 302

302 는 "그런 핸들러가 없어서 인증 요구로 떨어졌다"는 뜻이다. 엔드포인트가 있었다면 POST 를 받아 200 이나 400 을 돌려줬을 것이다.


2. IdP 쪽만 설정하고 시험했다

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"}'
  backchannel.logout.session.required = true
  backchannel.logout.url = https://app1.hyeonworks.com/logout/connect/back-channel/keycloak

출처 주의 — 위 확인 출력은 02-configure-idp.txt 가 아니라 그 뒤 별도로 실행한 조회에서 나온 것이다. 그 파일에는 command terminated with exit code 1 이 남아 있다 — -s "attributes.backchannel.logout.url=..." 의 점 표기가 실패한 첫 시도이며, JSON 으로 다시 넣어 성공했다. 실패한 시도의 파일에 성공 출력을 붙여 인쇄한 것은 잘못이었다.

살아 있는 세션에 로그아웃을 걸었다

=== 로그아웃 전 ===
  keycloak-patterns 세션: 1
  Redis: 1 키

=== IdP 로그아웃 ===
  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 쪽은 끊겼다
  Redis: 1 키                       ← ★ 앱 세션은 그대로다

IdP 세션만 사라지고 앱 세션은 남았다. C-1 과 같은 결과다.

네트워크 문제가 아님을 확인했다

=== Keycloak 파드가 app1.hyeonworks.com 에 닿는가 ===
  DNS 해석:  Address: 100.83.212.4
  HTTPS 도달: HTTP 200

클러스터 안에서 공개 이름으로 앱에 닿는다. 이 실험대는 tailnet + split DNS 구성이라 헤어핀이 되는데, 운영에서는 안 되는 경우가 흔하다.

백채널 로그아웃의 숨은 전제 — IdP 가 앱의 공개 URL 로 서버에서 서버로 요청을 보낼 수 있어야 한다. 앱이 사설망에 있고 IdP 가 밖에 있으면 설정을 해도 도달하지 못한다. 그때는 로그도 안 남고 조용히 실패한다.


3. 그래서 왜 안 퍼졌는가

   IdP 로그아웃
     ├─ ① Keycloak 이 backchannel.logout.url 로 POST 를 보낸다   (설정함)
     ├─ ② 앱이 그 POST 를 받는 엔드포인트를 갖고 있다             ★ 없다
     └─ ③ 앱이 logout token 을 검증하고 sid 로 세션을 찾아 지운다  ★ 없다

②와 ③이 없다. ①만 설정해도 받을 사람이 없다.

Keycloak 로그에 backchannel 문자열이 0줄이었다 — 다만 이것만으로 "보내지 않았다"고 단정할 수는 없다 (DEBUG 레벨일 수 있다). 확실한 것은 앱 세션이 남았다는 관측이다.


4. 개념 — 백채널 로그아웃의 구조

   사용자가 어느 앱에서든 로그아웃
        │
        ▼
   Keycloak 이 SSO 세션에 붙은 client session 목록을 본다   (C-1 의 그 구조)
        │
        ├──POST──▶ app1 의 backchannel.logout.url
        └──POST──▶ app2 의 backchannel.logout.url
                     본문: logout_token (JWT)
                       { "sid": "...", "sub": "...", "events": {...} }

sid 가 여기서 쓰인다

A-0 에서 확인한 그 sid 다 — JWT·DB·관리 API 에서 같은 문자열이었던.

   logout_token 의 sid  →  앱이 "그 sid 로 만든 내 세션"을 찾아 지운다

그래서 앱은 sid → 자기 세션 ID 역인덱스를 갖고 있어야 한다. Spring Security 는 이를 위해 OidcSessionRegistry 를 쓰며, 여러 인스턴스가 있으면 그 레지스트리도 공유 저장소여야 한다 — B-1·B-2 에서 겪은 것과 같은 문제가 한 겹 더 있다.

부분 실패는 어떻게 되는가

   app1 로그아웃 성공, app2 는 응답 없음
     └─ Keycloak 은 재시도하는가? 얼마나?
     └─ 사용자는 app2 에서 여전히 로그인 상태다

로그아웃은 원자적이지 않다. 앱이 늘어날수록 "일부만 로그아웃된 상태"가 생길 확률이 올라간다.


5. 겪은 문제

문제
kcadm -s "attributes.backchannel.logout.url=..." 이 exit 1 점 표기가 안 먹는다. JSON 으로 통째로 줘야 한다
첫 시험이 무의미했다 로그아웃 전 IdP 세션이 이미 0 이었다. 끊을 대상이 없었다
세션 수를 realm 없이 셌다 C-1 과 같은 실수 — master 의 admin 세션이 섞인다
Keycloak 재시작 후 로그인 폼이 안 넘어갔다 인증 세션 쿠키가 무효화된 상태에서 폼을 재사용했다

두 번째가 A층에서 반복한 교훈이다주입 대상이 실제로 존재하는지 먼저 확인한다. 세션이 없는 상태에서 로그아웃을 걸고 "전파가 안 된다"고 결론지을 뻔했다.


6. 구현하려면 무엇이 필요한가

계층 할 일
IdP 클라이언트마다 backchannel.logout.url 설정 (완료)
.oidcLogout(oidc -> oidc.backChannel(...)) 활성화
OidcSessionRegistry공유 저장소로 (인스턴스가 여럿이므로)
네트워크 IdP → 앱 공개 URL 도달 (이 실험대는 됨, 운영은 확인 필요)
oauth2-proxy 지원하지 않는다. 별도 방안이 필요하다

마지막이 C-1 과 맞물린다 — app1(BFF)은 구현할 수 있지만 app2(oauth2-proxy)는 못 한다. 한 SSO 안에서 로그아웃 전파가 앱마다 다르게 동작하게 된다.



증거 파일

증거 수집 시각: 2026-09-04 14:50 14:53 KST (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)

파일 종류
01-current-state.txt 터미널 원문
02-configure-idp.txt 터미널 원문
03-logout-attempt.txt 터미널 원문
04-reachability.txt 터미널 원문

파일별 상세는 evidence/c2-backchannel-logout/README.md.

7. 재현 절차 (명령어)

# 1. 현재 설정 확인 — 어느 쪽에도 없다
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 으로
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 해서)
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
  "select count(*) from offline_user_session us join realm r on r.id=us.realm_id
    where r.name='keycloak-patterns' and us.offline_flag='0'"

# 4. 로그아웃하고 앱 세션을 본다
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 로
kubectl -n keycloak-lab run t --rm -i --restart=Never --image=curlimages/curl:8.11.1 \
  --command -- curl -s -o /dev/null -w '%{http_code}\n' https://app1.hyeonworks.com/

8. 다음에 남기는 것

구현 .oidcLogout() 활성화 + OidcSessionRegistry 공유
oauth2-proxy 백채널 로그아웃 미지원 — SSO 안에서 앱마다 동작이 갈린다
운영 IdP → 앱 도달성이 전제다. 안 되면 조용히 실패한다
B-2 와 연결 로그아웃이 지우는 것은 지금도 세 곳 중 하나뿐이다