Cross-checked each question's 남은 미지수, 다음 검증 and 제약 against the plan item by item. Adds B-0 (autoconfiguration actually chosen), B-6 (encryption key rotation) and B-7 (oauth2-proxy cookie secret rotation) as new experiments, plus lock-holder death, rotation-disabled comparison, partial-logout recovery, store latency and the Q4 design checklist. Restores the Redis persistence comparison and records the correct index URL. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
292 lines
12 KiB
Markdown
292 lines
12 KiB
Markdown
# 열린 질문 커버리지 — 이 실험대로 답할 수 있는가
|
|
|
|
공개 기록에 등록된 KeyCloak Patterns 열린 질문 네 개를, 이 실험대가 실제로
|
|
검증할 수 있는지 대조한 결과.
|
|
|
|
> **목록 경로는 [`/explore/questions`](https://hyeonworks.com/explore/questions)**
|
|
> 다. `/questions` 는 404 이고 개별 문서만 `/questions/<slug>` 로 열린다.
|
|
>
|
|
> **2026-09-04 재확인** — Playwright 로 네 문서를 전문 재독하고
|
|
> 「남은 미지수」·「다음 검증」·「제약」을 항목 단위로 대조한 결과
|
|
> **계획에 빠진 항목 9개**를 찾아 보강했다. 항목별 실험 번호 대조표는
|
|
> [`experiment-plan.md`](experiment-plan.md) B층 머리에 있다.
|
|
|
|
**결론 — 네 개 모두 이 실험대에서 재현 가능하다. 다만 로드맵에 빠진 항목이
|
|
있고, 순서가 한 곳 뒤집혀 있다.**
|
|
|
|
| # | 질문 | 게시 | 로드맵 커버 |
|
|
|---|---|---|---|
|
|
| Q1 | [서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가](https://hyeonworks.com/questions/server-session-pattern-multi-instance) | 2026.08.29 | **부분** |
|
|
| Q2 | [Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가](https://hyeonworks.com/questions/refresh-rotation-replica-contention) | 2026.08.26 | **부분** |
|
|
| Q3 | [BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가](https://hyeonworks.com/questions/bff-session-authorized-client-store) | 2026.08.30 | **부분** |
|
|
| Q4 | [Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가](https://hyeonworks.com/questions/edge-authorization-scope) | 2026.08.31 | **없음** |
|
|
|
|
---
|
|
|
|
## 발견한 구조적 문제
|
|
|
|
### 1. 순서가 뒤집혀 있다
|
|
|
|
Q2가 명시한다.
|
|
|
|
> 이 경쟁은 **저장소를 공유한 뒤에야 재현**되기 때문에 저장소 결정을 하고
|
|
> 나서 해당 문제를 이어서 풀어보자.
|
|
|
|
즉 **Q3(저장소 결정) → Q2(경쟁 재현)** 순서다. 그런데 로드맵은
|
|
`refresh-token-concurrency`를 `redis-app-session-store`보다 **앞**에 두었다.
|
|
|
|
**Q2를 먼저 시도하면 재현 자체가 불가능하다.** 저장소가 process-local이면
|
|
두 replica가 같은 refresh token 항목을 보지 않기 때문이다.
|
|
|
|
→ 로드맵 순서를 교정한다.
|
|
|
|
### 2. Session과 Authorized Client는 조회 키가 다르다
|
|
|
|
Q3의 핵심이며 로드맵에 이 구분이 없었다.
|
|
|
|
| 상태 | 조회 키 | 저장 위치(현재) |
|
|
|---|---|---|
|
|
| Application Session | **session ID** | 서블릿 컨테이너 in-memory |
|
|
| OAuth2AuthorizedClient | **client registration 이름 + principal name** | 자동구성 in-memory |
|
|
|
|
**`session ID`가 조회 키에 없다.** 그래서 같은 사용자가 두 브라우저에서
|
|
로그인하면 **동일한 authorized client 항목을 공유**한다.
|
|
|
|
Q1의 제약이 이를 그대로 지적한다.
|
|
|
|
> 여러 인스턴스가 같은 세션을 사용할 수 있도록 Session Store를 공유
|
|
> 저장소로 변경하는 것만으로는 **충분하지 않다.**
|
|
|
|
→ 실험을 "Redis 도입" 하나로 뭉뚱그리면 안 된다. **두 저장소를 각각 설계하고
|
|
각각 검증해야 한다.**
|
|
|
|
### 3. 이미 해결한 문제가 질문에도 있다
|
|
|
|
Q1의 제약:
|
|
|
|
> Resource Server의 8081이 host에도 열려 있어서 모든 client가 BFF만 거치도록
|
|
> **network에서 강제된 상태가 아니다.**
|
|
|
|
이는 2홉 헤더 실험에서 마주친 **프록시 우회 경로**와 같은 문제이며,
|
|
NetworkPolicy로 닫는 방법을 이미 확립했다
|
|
([`two-hop-proxy-header-contract.md`](two-hop-proxy-header-contract.md) 11절).
|
|
|
|
→ Q1에 답할 때 그 패턴을 그대로 재사용한다.
|
|
|
|
---
|
|
|
|
## Q1. 다중 인스턴스 운영
|
|
|
|
**질문이 요구하는 검증 5단계**
|
|
|
|
| # | 검증 | 실험대 가능 | 로드맵 |
|
|
|---|---|---|---|
|
|
| 1 | 한쪽에서 로그인 후 **다른 인스턴스로 요청 시 200 유지** | 가능 | 없음 |
|
|
| 2 | 한 인스턴스 재시작 후 **같은 session cookie로 상태 유지** | 가능 | 없음 |
|
|
| 3 | 같은 사용자 두 브라우저 → **authorized client 덮어쓰는가** | 가능 | **없음** |
|
|
| 4 | 한쪽 logout 후 **다른 쪽 요청** | 가능 | 부분 (백채널 로그아웃) |
|
|
| 5 | **session 만료 ≠ token 만료** 각 경우의 응답과 화면 | 가능 | **없음** |
|
|
|
|
**실험대 준비 상태** — BFF를 2 replica로 띄우면 전부 재현된다. 호스트 nginx의
|
|
`ip_hash` 주석을 켜고 끄면 **스티키 유무 비교**까지 같은 구성에서 된다.
|
|
|
|
**추가로 필요한 것**
|
|
|
|
- BFF 이미지 (아직 `bff/` 디렉터리에 소스 없음)
|
|
- 로그아웃 전파를 관찰할 두 번째 앱 (`app2.hyeonworks.com` 이름은 확보)
|
|
|
|
**3번이 특히 중요하다.** "Redis만 붙이면 해결"이라는 착각을 깨는 항목이고,
|
|
조회 키가 다르다는 사실의 실증이다.
|
|
|
|
---
|
|
|
|
## Q2. Refresh Token Rotation 경쟁
|
|
|
|
**질문이 요구하는 검증 5단계**
|
|
|
|
| # | 검증 | 실험대 가능 | 로드맵 |
|
|
|---|---|---|---|
|
|
| 1 | replica 두 대에서 **access token 만료 직후 동시 요청** | 가능 | 있음 |
|
|
| 2 | **이긴 쪽/지는 쪽 응답** 각각 기록 | 가능 | 부분 |
|
|
| 3 | 지는 쪽이 **저장된 새 token으로 재시도해 성공하는가** | 가능 | **없음** |
|
|
| 4 | **지는 쪽 사용자 화면**에 무엇이 보이는가 | 가능 | **없음** |
|
|
| 5 | **lock 유무를 같은 입력으로 비교** (실패율·지연) | 가능 | **없음** |
|
|
|
|
**5번이 결론을 내는 기준이다.**
|
|
|
|
> 실패가 사용자에게 노출되면 lock을 고르고, 노출되지 않으면 재시도로 둔다.
|
|
|
|
로드맵에 없던 항목인데, **이것이 없으면 질문에 답할 수 없다.**
|
|
|
|
**제약을 지켜야 한다**
|
|
|
|
- rotation + 재사용 0회는 **전제로 고정**한다. 바꾸지 않고 답한다
|
|
- 이미 발급된 access token은 만료 전까지 통하므로 **재현은 access token 만료
|
|
직후에 맞춰 실행**한다. 그렇지 않으면 실패가 화면에 보이지 않는다
|
|
|
|
**선행 조건** — Q3의 저장소 공유가 먼저다.
|
|
|
|
---
|
|
|
|
## Q3. BFF 저장소 결정
|
|
|
|
**질문이 요구하는 검증 5단계**
|
|
|
|
| # | 검증 | 실험대 가능 | 로드맵 |
|
|
|---|---|---|---|
|
|
| 1 | 인스턴스 두 대에서 **로그인 유지와 재시작 복구** | 가능 | 부분 |
|
|
| 2 | 저장소를 열어 **refresh token이 평문인가** | 가능 | **없음** |
|
|
| 3 | **session TTL ≠ token 만료** 그 순간의 응답과 화면 | 가능 | **없음** |
|
|
| 4 | logout 뒤 **두 store에 잔여 항목이 없는가** | 가능 | 부분 |
|
|
| 5 | **저장소를 끊은 상태**에서 로그인·API 호출 오류 | 가능 | 있음 |
|
|
|
|
**로드맵에 없던 큰 항목 — 후보 비교**
|
|
|
|
질문은 "Redis로 간다"가 아니라 **"Redis와 JDBC 중 무엇이 이 접근 패턴에
|
|
맞는가"** 를 묻는다.
|
|
|
|
> 요청마다 읽는 값과 가끔 읽는 값이 섞여 있다.
|
|
|
|
이 실험대에는 PostgreSQL이 이미 있으므로 **JDBC 후보를 같은 조건에서 비교할
|
|
수 있다.** Redis만 붙이면 질문의 절반만 답하는 셈이다.
|
|
|
|
**2번(평문 확인)의 실행 방법**
|
|
|
|
```bash
|
|
kubectl -n <ns> exec -it deploy/redis -- redis-cli --scan --pattern 'spring:session:*'
|
|
kubectl -n <ns> exec -it deploy/redis -- redis-cli GET <key>
|
|
```
|
|
|
|
저장소를 직접 열어 refresh token이 그대로 읽히는지 본다. 읽힌다면
|
|
암호화 설계가 필요하고, 그 key 교체 절차는 별도 과제다.
|
|
|
|
---
|
|
|
|
## Q4. Edge 인가 범위 — 로드맵에 전혀 없다
|
|
|
|
이 축을 A층(Keycloak)·B층(앱 세션) 중심으로 잡으면서 **AP4의 인가 범위
|
|
질문을 빠뜨렸다.**
|
|
|
|
**질문이 요구하는 검증**
|
|
|
|
| 검증 | 실험대 가능 |
|
|
|---|---|
|
|
| role을 헤더에 담고 **다중 값 구분자·escaping** 확인 | 가능 |
|
|
| **헤더 크기 상한** 초과 시 proxy가 자르는가 요청이 거부되는가 | 가능 |
|
|
| role 변경 후 **몇 번째 요청부터 반영되는가** | 가능 |
|
|
| upstream이 헤더 존재만 보는가 값과 service identity까지 보는가 | 가능 |
|
|
|
|
**이 실험대에서 특히 잘 맞는 이유**
|
|
|
|
nginx의 헤더 처리 특성을 이미 실측했다. 질문이 지적한
|
|
|
|
> Nginx는 client가 보낸 동명 헤더를 merge하지 않고 **덮어쓴다.**
|
|
|
|
는 2홉 헤더 실험에서 `proxy_set_header X-Forwarded-For $remote_addr`로
|
|
확인한 그 동작이다. **`X-Auth-Request-*`도 같은 규칙을 따르는지**를 같은
|
|
방법으로 검증할 수 있다.
|
|
|
|
그리고 질문의 제약
|
|
|
|
> internal token 검사가 controller 한 곳에만 있다. 헤더를 늘리기 전에 이
|
|
> 검사를 **공통 경계로 옮겨야** 된다.
|
|
|
|
는 코드 변경이므로 `backend/`에서 진행한다.
|
|
|
|
---
|
|
|
|
## 교정된 실험 순서
|
|
|
|
기존 로드맵의 순서를 질문의 의존 관계에 맞춰 조정한다.
|
|
|
|
```
|
|
✅ 환경 구축
|
|
✅ 2홉 프록시 헤더 계약
|
|
──────────────────────────────────────────────────────────
|
|
1. Keycloak 멀티노드 클러스터 형성 (선행 인프라)
|
|
2. persistent vs volatile 세션 (A층)
|
|
3. BFF 저장소 결정 → Q3 ★ Q2 의 선행 조건
|
|
4. 다중 인스턴스 운영 → Q1
|
|
5. Refresh Token 경쟁 → Q2 ★ 3 이후여야 재현됨
|
|
6. Edge 인가 범위 → Q4 ← 새로 추가
|
|
7. 장애 주입과 복구 (전 항목 공통)
|
|
```
|
|
|
|
**바뀐 점**
|
|
|
|
- `refresh-token-concurrency`가 `redis-app-session-store` **뒤로** 이동
|
|
- 저장소 결정이 **Redis 도입**이 아니라 **Redis vs JDBC 비교**로 확장
|
|
- **Edge 인가 범위(Q4)** 신규 추가
|
|
|
|
## 브랜치 매핑
|
|
|
|
| 실험 | 브랜치 | 상태 |
|
|
|---|---|---|
|
|
| 멀티노드 클러스터 | `feature/keycloak-multinode-cluster-jdbc-ping` | 존재 |
|
|
| persistent vs volatile | `feature/keycloak-persistent-vs-volatile-sessions` | 존재 |
|
|
| BFF 저장소 (Q3) | `feature/keycloak-redis-app-session-store` | 존재 — **범위 확장 필요** |
|
|
| 다중 인스턴스 (Q1) | — | **없음** |
|
|
| refresh 경쟁 (Q2) | `feature/keycloak-refresh-token-concurrency` | 존재 |
|
|
| Edge 인가 (Q4) | — | **없음** |
|
|
| 장애 주입 | `feature/keycloak-failure-injection-recovery` | 존재 |
|
|
|
|
**두 개를 새로 만들어야 한다.**
|
|
|
|
```bash
|
|
git checkout develop-keycloak-session-store
|
|
git checkout -b feature/keycloak-multi-instance-session-operation
|
|
git checkout -b feature/keycloak-edge-authorization-scope
|
|
```
|
|
|
|
## 공통 선행 조건 — BFF 구현은 이미 있다
|
|
|
|
세 질문(Q1·Q2·Q3)이 모두 **BFF를 2 replica로 띄우는 것**을 전제한다.
|
|
`develop-keycloak-session-store`의 `bff/`에는 빌드 산출물만 있지만,
|
|
**`develop-keycloak-pattern3`에 구현이 완성되어 있다.**
|
|
|
|
```
|
|
bff/Dockerfile
|
|
bff/pom.xml
|
|
bff/src/main/java/com/example/keycloakpattern/bff/
|
|
├ BffApplication.java
|
|
├ BffController.java
|
|
├ CsrfController.java
|
|
├ SecurityConfig.java
|
|
└ SpaCsrfTokenRequestHandler.java
|
|
bff/src/main/resources/application.yml
|
|
bff/src/main/resources/static/{index.html,app.js}
|
|
bff/src/test/java/.../BffControllerTest.java
|
|
```
|
|
|
|
→ 새로 구현할 필요가 없다. **AP3 브랜치에서 이 실험대로 가져온다.**
|
|
|
|
```bash
|
|
git checkout develop-keycloak-session-store
|
|
git checkout develop-keycloak-pattern3 -- bff/
|
|
```
|
|
|
|
가져온 뒤 확인할 것 — 질문들이 지목한 부분이 코드에 그대로 있는지.
|
|
|
|
| 확인 | 어디를 볼 것인가 |
|
|
|---|---|
|
|
| Session 저장소가 in-memory 자동구성인가 | `SecurityConfig.java`, `application.yml`에 Spring Session 설정 부재 |
|
|
| `OAuth2AuthorizedClientService`가 in-memory인가 | Bean 정의 부재 → 자동구성 결과 확인 필요 |
|
|
| authorized client 조회에 session ID가 없는가 | Spring Security 기본 계약 |
|
|
|
|
Q3가 "어떤 구현체가 실제로 쓰이는지는 자동구성 결과까지 확인해야 정확히
|
|
알 수 있다"고 남긴 미지수를, **기동 후 Bean을 실제로 조회해서** 확정할 수 있다.
|
|
|
|
```bash
|
|
kubectl -n <ns> exec deploy/bff -- \
|
|
curl -s localhost:8082/actuator/beans | grep -i authorizedClientService
|
|
```
|
|
|
|
## 참고
|
|
|
|
| 문서 | 관계 |
|
|
|---|---|
|
|
| [`session-store-lab-roadmap.md`](session-store-lab-roadmap.md) | 이 문서가 그 순서를 교정한다 |
|
|
| [`two-hop-proxy-header-contract.md`](two-hop-proxy-header-contract.md) | Q1의 우회 경로 제약, Q4의 헤더 덮어쓰기 근거 |
|
|
| [`session-lab-operations.md`](session-lab-operations.md) | 실행 도구와 명령 |
|
|
| [`four-pattern-tradeoff-matrix.md`](four-pattern-tradeoff-matrix.md) | Q4가 되돌아가는 선택지(BFF)의 비교표 |
|