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>
254 lines
9.2 KiB
Markdown
254 lines
9.2 KiB
Markdown
# C-1 — SSO 를 붙이면 무엇이 달라지는가
|
||
|
||
브랜치 `feature/keycloak-c1-multi-app-sso` ·
|
||
증거 [`docs/evidence/c1-multi-app-sso/`](evidence/c1-multi-app-sso/) ·
|
||
2026-09-04 16:15–16:30 KST
|
||
|
||
선행: [`B-2`](experiment-b2-multi-instance-session.md) (app1 = BFF) ·
|
||
[`B-7`](experiment-b7-cookie-secret-rotation.md) (app2 = oauth2-proxy)
|
||
|
||
**원래 질문** — *"SSO 를 추가하게 되면 어떻게 달라지는지"*
|
||
|
||
---
|
||
|
||
## 구조
|
||
|
||

|
||
|
||
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
|
||
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](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 를 쓰지만 세션을 완전히 다르게 다룬다.**
|
||
|
||
### 깨끗한 상태에서 시작한다
|
||
|
||
```bash
|
||
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 세션을 죽여도 두 앱은 살아 있다
|
||
|
||
```bash
|
||
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 해서 보고 나서야 알았다.
|
||
|
||
```sql
|
||
select us.user_session_id, r.name as realm, ...
|
||
from offline_user_session us join realm r on r.id = us.realm_id
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 재현 절차 (명령어)
|
||
|
||
```bash
|
||
# 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 을 잃으면 전 앱이 끊긴다 — 백업 범위에 들어간다 |
|
||
| 운영 | **세 층의 수명을 맞추거나, 어긋날 때의 동작을 정의해야 한다** |
|