Files
keycloak-pattern/docs/experiment-c2-backchannel-logout.md
T
DongHyeonkaandClaude Opus 5 e0d27d47ce docs: correct the places where documents contradicted their own evidence
An independent audit found ten documents printing values their evidence files do not contain. C-1 printed a session count of 0 where the evidence says 4, C-2 printed a success readback for a command that exited 1, and A-1 credited the conntrack flush with a split that the timestamps attribute to a pod restart four seconds earlier.

Also measured wal_writer_delay, which A-3 had asserted as matching without ever querying it, relabelled the A-6 control that moved 41 percent, noted A-8's nine-sample resolution, corrected D-1's RTO to the 41 seconds its own timeline shows, and added a correction banner to D-2. Every experiment document now links its evidence files with their real collection times, and the duplicate screenshots are documented as duplicates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 16:35:49 +09:00

262 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# C-2 — 로그아웃이 전 앱에 퍼지는가 (백채널 로그아웃)
브랜치 `feature/keycloak-c2-backchannel-logout` ·
증거 [`docs/evidence/c2-backchannel-logout/`](evidence/c2-backchannel-logout/) ·
2026-09-04 16:3016:55 KST
선행: [`C-1`](experiment-c1-multi-app-sso.md) — **IdP 로그아웃이 앱에 전파되지 않는다**를 관측했다
---
## 0. 결론부터
| 확인 | 결과 |
|---|---|
| 백채널 로그아웃이 설정되어 있었는가 | **아니다.** 두 클라이언트 모두 `backchannelLogoutUrl` 없음 |
| BFF 에 그 엔드포인트가 있는가 | **아니다.** 소스에 `oidcLogout` 설정이 없다 |
| IdP 쪽만 설정하면 되는가 | **★ 안 된다.** 앱 세션이 그대로 남았다 |
| Keycloak 이 앱 URL 에 닿기는 하는가 | **닿는다** (`HTTP 200`) — 네트워크 문제가 아니다 |
**C-1 이 관측한 "로그아웃이 안 퍼진다" 의 원인은 단순했다 —
아무도 구현하지 않았다.**
---
## 1. 현재 상태 — 어느 쪽에도 없다
### IdP 쪽
```bash
kcadm.sh get clients -r keycloak-patterns -q clientId=bff-confidential --fields attributes
```
```
frontchannelLogout : false
(backchannel.logout.url 없음)
```
`oauth2-proxy` 클라이언트도 마찬가지였다.
### 앱 쪽
```bash
grep -rn "oidcLogout\|backchannel" bff/src/main/java/
```
**아무것도 안 나온다.**
Spring Security 6.2+ 는 백채널 로그아웃을 지원하지만 **명시적으로 켜야 한다.**
```java
.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 쪽만 설정하고 시험했다
```bash
kcadm.sh update clients/<id> -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`](evidence/c2-backchannel-logout/02-configure-idp.txt) 가
> 아니라 그 뒤 별도로 실행한 조회에서 나온 것이다. 그 파일에는
> **`command terminated with exit code 1`** 이 남아 있다 —
> `-s "attributes.backchannel.logout.url=..."` 의 점 표기가 실패한 첫 시도이며,
> JSON 으로 다시 넣어 성공했다. **실패한 시도의 파일에 성공 출력을 붙여
> 인쇄한 것은 잘못이었다.**
### 살아 있는 세션에 로그아웃을 걸었다
```
=== 로그아웃 전 ===
keycloak-patterns 세션: 1
Redis: 1 키
=== IdP 로그아웃 ===
kcadm.sh create users/<id>/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`](evidence/c2-backchannel-logout/01-current-state.txt) | 터미널 원문 |
| [`02-configure-idp.txt`](evidence/c2-backchannel-logout/02-configure-idp.txt) | 터미널 원문 |
| [`03-logout-attempt.txt`](evidence/c2-backchannel-logout/03-logout-attempt.txt) | 터미널 원문 |
| [`04-reachability.txt`](evidence/c2-backchannel-logout/04-reachability.txt) | 터미널 원문 |
파일별 상세는 [`evidence/c2-backchannel-logout/README.md`](evidence/c2-backchannel-logout/README.md).
## 7. 재현 절차 (명령어)
```bash
# 1. 현재 설정 확인 — 어느 쪽에도 없다
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 \
-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. 로그아웃하고 앱 세션을 본다
kcadm.sh create users/<user-id>/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 와 연결** | 로그아웃이 지우는 것은 지금도 세 곳 중 하나뿐이다 |