# 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 를 추가하게 되면 어떻게 달라지는지"* --- ## 구조 ![C-1 구조 — user session 1 : client session N](diagrams/c1-sso-structure.svg) > 다이어그램 규약은 [`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 온라인 세션: 4 ← 초기화가 안 먹었다 app1 HTTP 200 / app2 HTTP 200 ``` > **정정** — 이 문서는 처음에 이 값을 `0` 으로 인쇄했다. 증거 > [`01-baseline.txt`](evidence/c1-multi-app-sso/01-baseline.txt) 는 `4` 다. > `logout-all` 이 듣지 않아 세션이 남아 있었고, 그래서 아래 절차(DB 직접 삭제 + > Keycloak 재시작)가 필요했다. **`0` 은 그 다음 단계의 값이었다.** --- ## 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 방문 — **로그인 화면 없이 통과한다** ![app2 가 로그인 없이 열린다](evidence/c1-multi-app-sso/c1-sso-app2-no-login-screen.png) ``` 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/$(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 (master realm 의 admin 세션만 남음) Redis: _oauth2_proxy-6b028a70f... ← 남아 있다 bff:session:sessions:6e0d9af4-... ← 남아 있다 PostgreSQL authorized client: 1 행 ← 남아 있다 ``` 브라우저로 두 앱을 다시 열었다. ![IdP 세션이 없어도 앱은 동작한다](evidence/c1-multi-app-sso/c1-apps-alive-after-idp-logout.png) **둘 다 로그인 화면 없이 그대로 열렸다.** ### 왜 그런가 — 세 개의 독립된 수명 ``` ① 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//logout-all` 이 안 먹었다 | 세션 수가 그대로였다. DB 를 직접 지우고 **Keycloak 재시작**해야 했다 (A-1 의 캐시 문제) | | `kcadm delete sessions/` 도 안 먹었다 | 오류 없이 아무 일도 안 일어났다. `users//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 ``` --- --- ## 증거 파일 **증거 수집 시각: 2026-09-04 14:44 – 14:48 KST** (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.) | 파일 | 종류 | |---|---| | [`01-baseline.txt`](evidence/c1-multi-app-sso/01-baseline.txt) | 터미널 원문 | | [`02-after-app1-login.txt`](evidence/c1-multi-app-sso/02-after-app1-login.txt) | 터미널 원문 | | [`03-after-app2-visit.txt`](evidence/c1-multi-app-sso/03-after-app2-visit.txt) | 터미널 원문 | | [`04-sso-session-killed.txt`](evidence/c1-multi-app-sso/04-sso-session-killed.txt) | 터미널 원문 | | [`c1-apps-alive-after-idp-logout.png`](evidence/c1-multi-app-sso/c1-apps-alive-after-idp-logout.png) | 스크린샷 | | [`c1-sso-app2-no-login-screen.png`](evidence/c1-multi-app-sso/c1-sso-app2-no-login-screen.png) | 스크린샷 | 파일별 상세는 [`evidence/c1-multi-app-sso/README.md`](evidence/c1-multi-app-sso/README.md). ## 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/$(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. 두 앱을 다시 연다 — 그대로 열리면 앱 세션이 독립적이라는 뜻 ``` --- ## 7. 다음 실험에 남기는 것 | 실험 | 이 실험이 준 것 | |---|---| | **C-2** 백채널 로그아웃 | **이 실험이 C-2 가 왜 필요한지 보여준다** — IdP 로그아웃이 앱에 전파되지 않는다 | | **D-1** 백업·복구 | user session 을 잃으면 전 앱이 끊긴다 — 백업 범위에 들어간다 | | 운영 | **세 층의 수명을 맞추거나, 어긋날 때의 동작을 정의해야 한다** |