The forward upgrade to 26.7.3 was zero downtime across 87 samples, and since databasechangelog stayed at 210 the rollback to 26.7.0 also succeeded, which narrows D-2's conclusion: rolling back fails when the schema moved, not because of the version number. The row count is the check. Role changes never reach the upstream through request repetition; the session is a snapshot taken at login and only a new session picks up the new claim. Auditing the docs also surfaced that Prometheus scrapes only keycloak, kubelet, node-exporter and itself, so the B-layer experiments have no metrics to screenshot rather than missing screenshots. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9.2 KiB
C-1 — SSO 를 붙이면 무엇이 달라지는가
브랜치 feature/keycloak-c1-multi-app-sso ·
증거 docs/evidence/c1-multi-app-sso/ ·
2026-09-04 16:15–16:30 KST
선행: B-2 (app1 = BFF) ·
B-7 (app2 = oauth2-proxy)
원래 질문 — "SSO 를 추가하게 되면 어떻게 달라지는지"
구조
다이어그램 규약은
diagrams/_style.md. 실험대 전체 구조는diagrams/lab-topology.svg.
0. 결론부터
| 물음 | 답 |
|---|---|
| 두 번째 앱에서 로그인 화면이 뜨는가 | 안 뜬다. SSO 가 동작한다 |
| Keycloak 안의 구조는 | user session 1개 · client session 2개 |
| IdP 세션을 죽이면 두 앱이 끊기는가 | ★ 안 끊긴다. 두 앱 모두 그대로 동작한다 |
"SSO 를 붙이면 IdP 가 단일 장애점이 된다" 는 절반만 맞다. 로그인할 때는 그렇고, 이미 로그인한 뒤에는 아니다.
1. 구성 — 서로 다른 구조의 두 앱
app1.hyeonworks.com → BFF 서버 세션 (Redis) + 토큰 (PostgreSQL)
app2.hyeonworks.com → oauth2-proxy 쿠키 티켓 + 세션 (Redis)
둘 다 realm keycloak-patterns
우연히 좋은 실험대가 됐다. B-2 와 B-7 에서 만든 두 앱이 같은 IdP 를 쓰지만 세션을 완전히 다르게 다룬다.
깨끗한 상태에서 시작한다
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "delete from offline_client_session" -c "delete from offline_user_session"
kubectl -n keycloak-lab exec deploy/redis -- redis-cli flushall
kubectl -n keycloak-lab rollout restart statefulset/keycloak
kcadm create realms/.../logout-all 은 듣지 않았다. 세션이 그대로 남아
DB 를 직접 지우고 Keycloak 을 재시작해야 했다 — 캐시 때문이다
(A-1 에서 확인한 대로, DB 를 직접 지워도 캐시는 남는다).
Keycloak 온라인 세션: 0
app1 HTTP 200 / app2 HTTP 200
2. SSO 가 동작한다
app1 로그인 — 로그인 화면이 나온다
https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-connect/auth
?client_id=bff-confidential&...
→ Sign in to keycloak-patterns
로그인 후
user_session_id | client_sessions
--------------------------+-----------------
oqOjHekin4JU-BZjgQLjUByW | 1
Redis: bff:session:sessions:6e0d9af4-...
PostgreSQL authorized client: 1 행
app2 방문 — 로그인 화면 없이 통과한다
user_session_id | client_sessions
--------------------------+-----------------
oqOjHekin4JU-BZjgQLjUByW | 2 ← 1 → 2
client_id | name
--------------------------------------+------------------
9055fa46-6abb-4d6d-a339-8a9183bbf26d | bff-confidential
80431dbc-af81-4673-9790-ad06d1570b2e | oauth2-proxy
Redis:
bff:session:sessions:6e0d9af4-... ← app1 의 세션
_oauth2_proxy-6b028a70f... ← app2 의 세션
개념 — SSO 의 데이터 구조
user session (사용자 · 브라우저 하나당 하나)
├─ client session : bff-confidential
└─ client session : oauth2-proxy
A-3 과 B-3 에서 봤던 그 구조가 여기서 의미를 갖는다.
- A-3 — 크래시로
user_session행이 통째로 사라지면 모든 앱이 끊긴다 - B-3 — 재사용 탐지가
client_session만 제거하면 그 앱만 끊긴다
두 층이 나뉘어 있는 이유가 SSO 다. 앱 하나의 문제가 다른 앱에 번지지 않도록 하려면 client session 이 따로 있어야 한다.
3. ★ IdP 세션을 죽여도 두 앱은 살아 있다
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
create users/<user-id>/logout -r keycloak-patterns
남은 keycloak-patterns 세션: 0 (master realm 의 admin 세션만 남음)
Redis:
_oauth2_proxy-6b028a70f... ← 남아 있다
bff:session:sessions:6e0d9af4-... ← 남아 있다
PostgreSQL authorized client: 1 행 ← 남아 있다
브라우저로 두 앱을 다시 열었다.
둘 다 로그인 화면 없이 그대로 열렸다.
왜 그런가 — 세 개의 독립된 수명
① IdP 세션 (Keycloak) ssoSessionIdleTimeout 1800초
② 앱 세션 (BFF / oauth2-proxy) 각자 30분 / 1시간
③ access token 60초
①을 지워도 ②는 자기 수명을 산다
로그아웃은 ①만 지운 것이고, ②는 아무도 안 건드렸다.
| 언제 끊기는가 | |
|---|---|
| BFF | access token 이 만료되어 refresh 를 시도할 때 → Session not active |
| oauth2-proxy | 쿠키 만료(1시간) 또는 토큰 갱신 시도할 때 |
즉시가 아니라 지연되어 끊긴다. 최대 지연은 access token 수명(60초)이 아니라 앱이 다음에 IdP 를 부를 때까지다.
이것이 B-2 에서 "로그아웃했는데 다시 들어가진다" 를 겪은 것의 반대편이다. 거기서는 앱 세션을 지웠는데 IdP 세션이 남아 재로그인이 됐고, 여기서는 IdP 세션을 지웠는데 앱 세션이 남아 계속 들어가진다.
두 방향 모두 "한쪽만 지우면 다른 쪽이 남는다" 이다.
4. 그래서 SSO 의 대가는 무엇인가
원래 질문 "SSO 를 추가하면 어떻게 달라지는가" 에 대한 답이다.
| 앱이 하나일 때 | SSO 일 때 | |
|---|---|---|
| 로그인 | 앱마다 | 한 번 |
| IdP 가 죽으면 | 그 앱만 로그인 불가 | 모든 앱이 로그인 불가 |
| 이미 로그인한 사용자 | — | ★ 영향 없다 (앱 세션이 살아 있으므로) |
| 로그아웃 | 그 앱만 | 전 앱을 끊으려면 백채널 로그아웃이 필요 |
| 세션 수명 | 하나 | 세 층이 각자 — 어긋나면 예측이 어렵다 |
IdP 는 "로그인 경로"의 단일 장애점이지 "이미 로그인한 사용자"의 단일 장애점이 아니다. A-2(DB 상실)에서 본 것과 합치면
Keycloak DB 죽음 → 새 로그인 불가 (전 앱)
→ 이미 로그인한 사용자는 앱 세션 수명 동안 계속 쓴다
→ 그 뒤 갱신 시점에 한꺼번에 끊긴다
장애가 즉시 전면화되지 않고 "앱 세션 수명만큼 지연되어 몰려온다." 이것이 SSO 구조의 장애 모양이며, 모니터링이 어려운 이유다.
5. 겪은 문제
| 문제 | |
|---|---|
kcadm create realms/<r>/logout-all 이 안 먹었다 |
세션 수가 그대로였다. DB 를 직접 지우고 Keycloak 재시작해야 했다 (A-1 의 캐시 문제) |
kcadm delete sessions/<id> 도 안 먹었다 |
오류 없이 아무 일도 안 일어났다. users/<id>/logout 은 동작했다 |
| 세션 수를 셀 때 realm 을 안 봤다 | master realm 의 admin 세션이 섞여 "안 지워졌다" 로 오독할 뻔했다 |
세 번째가 특히 위험했다 — realm 을 join 해서 보고 나서야 알았다.
select us.user_session_id, r.name as realm, ...
from offline_user_session us join realm r on r.id = us.realm_id
6. 재현 절차 (명령어)
# 1. 깨끗한 상태 — logout-all 은 안 먹으므로 DB + 재시작
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "delete from offline_client_session" -c "delete from offline_user_session"
kubectl -n keycloak-lab exec deploy/redis -- redis-cli flushall
kubectl -n keycloak-lab rollout restart statefulset/keycloak
# 2. app1 로그인 → app2 방문 (로그인 화면이 안 떠야 SSO)
# 3. 구조 확인 — user session 1 에 client session 2
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c \
"select us.user_session_id, r.name as realm,
(select count(*) from offline_client_session cs
where cs.user_session_id=us.user_session_id) as clients
from offline_user_session us join realm r on r.id=us.realm_id
where us.offline_flag='0'"
# 4. IdP 세션만 죽인다
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
create users/<user-id>/logout -r keycloak-patterns
# 5. 두 앱을 다시 연다 — 그대로 열리면 앱 세션이 독립적이라는 뜻
7. 다음 실험에 남기는 것
| 실험 | 이 실험이 준 것 |
|---|---|
| C-2 백채널 로그아웃 | 이 실험이 C-2 가 왜 필요한지 보여준다 — IdP 로그아웃이 앱에 전파되지 않는다 |
| D-1 백업·복구 | user session 을 잃으면 전 앱이 끊긴다 — 백업 범위에 들어간다 |
| 운영 | 세 층의 수명을 맞추거나, 어긋날 때의 동작을 정의해야 한다 |

