Files
keycloak-pattern/docs/experiment-c1-multi-app-sso.md
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

10 KiB
Raw Permalink Blame History

C-1 — SSO 를 붙이면 무엇이 달라지는가

브랜치 feature/keycloak-c1-multi-app-sso · 증거 docs/evidence/c1-multi-app-sso/ · 2026-09-04 16:1516:30 KST

선행: B-2 (app1 = BFF) · B-7 (app2 = oauth2-proxy)

원래 질문"SSO 를 추가하게 되면 어떻게 달라지는지"


구조

C-1 구조 — user session 1 : client session N

다이어그램 규약은 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 온라인 세션: 4        ← 초기화가 안 먹었다
  app1 HTTP 200 / app2 HTTP 200

정정 — 이 문서는 처음에 이 값을 0 으로 인쇄했다. 증거 01-baseline.txt4 다. 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 가 로그인 없이 열린다

     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 세션이 없어도 앱은 동작한다

둘 다 로그인 화면 없이 그대로 열렸다.

왜 그런가 — 세 개의 독립된 수명

   ① 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


증거 파일

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

파일 종류
01-baseline.txt 터미널 원문
02-after-app1-login.txt 터미널 원문
03-after-app2-visit.txt 터미널 원문
04-sso-session-killed.txt 터미널 원문
c1-apps-alive-after-idp-logout.png 스크린샷
c1-sso-app2-no-login-screen.png 스크린샷

파일별 상세는 evidence/c1-multi-app-sso/README.md.

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 을 잃으면 전 앱이 끊긴다 — 백업 범위에 들어간다
운영 세 층의 수명을 맞추거나, 어긋날 때의 동작을 정의해야 한다