refactor: 문서 개선 중

This commit is contained in:
donghyeon-ka
2026-09-21 14:30:55 +09:00
parent c93cdea150
commit 805a18f486
1497 changed files with 525837 additions and 59152 deletions
+543
View File
@@ -0,0 +1,543 @@
# keycloak 재검토 리뷰 — 2026-09-19
## 판정
**keycloak = 아직 더 봐야댐**
직전 상세 리뷰의 K01~K06은 상당 부분 반영되었다. 그러나 K03과 K04는 의미상 완전히 닫히지 않았고, 같은 기술 정확성 기준으로 전체 문맥을 다시 읽는 과정에서 Authorization Endpoint의 Referrer 설명이 지나치게 일반화된 부분도 추가로 확인했다.
자동 검증은 모두 통과했다. 이번 판정이 남아 있는 이유는 빌드/파이프라인 실패가 아니라 **기술 의미와 문서 내부 정의의 일관성** 때문이다.
현재 source repository:
`/home/donghyeon/workspace/keycloak-pattern`
는 현재 머신에 존재하지 않는다. 따라서 live source reconciliation은 계속 **UNVERIFIABLE**이다. 이를 PASS로 간주하지 않는다.
---
# 1. 이번 재검토 기준
이번 검토는 직전 리뷰의 K01~K06 문자열만 없어졌는지 확인하는 방식으로 하지 않았다.
동일한 기준을 세 층으로 유지했다.
## 1.1 정본 정확성
`final/document.md` 자체가 OAuth/OIDC, Keycloak, Spring Security의 의미를 기술적으로 정확하게 설명하는지 본다.
확인 기준:
- 일반 표준 규칙과 이 프로젝트의 구현 사실을 구분하는가
- 사실, 추론, 미확인을 섞지 않는가
- 현재 구현에서 확인하지 않은 것을 확인한 것처럼 쓰지 않는가
- 특정 구현의 우연한 배치를 OAuth/OIDC 일반 규칙으로 확대하지 않는가
## 1.2 SSOT → Record 의미 보존
Concept / Reference / Case / Question이 SSOT를 파생하면서 다음을 바꾸지 않는지 본다.
- fact
- scope
- certainty
- causality
- condition
즉 문장이 자연스러워졌는지가 아니라 **정본이 말한 범위와 확실성이 그대로 유지됐는지**를 본다.
## 1.3 문서 시스템 무결성
의미가 맞더라도 문서 시스템에서 깨진 것이 없는지 본다.
- Natural prose
- Voice
- Tech Log Tree
- Project Layout
- Figure Text
- Figure Provenance
- Figure Overlap
- Required Content
- SSOT Facts
- Command Pedagogy
- git diff --check
- standalone pipeline
- full unittest regression
그림은 자동 PASS만 보지 않고 SSOT가 바뀐 뒤 context/spec이 여전히 같은 주장을 그리는지도 직접 본다.
### 맥락 유지 원칙
이번 재검토에서 **새 평가 축을 임의로 추가하지 않았다.**
새로 발견된 항목이 있더라도 그것은 기존 기준인:
1. 기술 정확성
2. 사실/추론/미확인 구분
3. SSOT ↔ Record semantic consistency
4. 설명 범위의 과도한 일반화 방지
중 하나로 설명될 때만 finding으로 잡았다.
---
# 2. 직전 K01~K06 반영 상태
| ID | 직전 finding | 현재 상태 | 판정 |
|---|---|---|---|
| K01 | AP2/AP3 PKCE scope 충돌 | AP2라는 주어가 명시되고 AP2 PKCE S256 미확인이 분리됨 | **CLOSED** |
| K02 | refreshTokenMaxReuse를 시간 창처럼 설명 | reuse count와 lifespan을 분리하고 runtime 경쟁은 미검증 유지 | **CLOSED** |
| K03 | client type이 token endpoint caller를 결정한다는 인과 | 본문은 수정됐으나 섹션 제목이 여전히 과도한 일반화 | **PARTIAL** |
| K04 | Public/Confidential 정의를 client secret 하나로 축소 | Reference 본문은 개선됐으나 서두·SSOT·Case에 축약 정의 잔존 | **PARTIAL** |
| K05 | “남는 선택지는 하나” 일반화 | AP1~AP4 네 패턴 범위로 제한됨 | **CLOSED** |
| K06 | CSRF token = 사용자 의도 증명 | cross-site forged request를 구분하는 anti-CSRF 검증으로 수정됨 | **CLOSED** |
따라서 K01~K06 전체가 닫힌 것은 아니다.
---
# 3. 남은 Findings
## R01 — HIGH — K04가 문서 전체에서는 아직 닫히지 않았다
직전 리뷰에서 요구한 핵심은:
> Public/Confidential client를 shared client secret 하나로 정의하지 말고,
> client credential의 기밀성 및 신뢰할 수 있는 client authentication 능력으로 설명한다.
Reference 본문은 이 방향으로 잘 수정됐다.
현재 올바르게 수정된 부분:
`reference-public-confidential-client.md:41`
> OAuth client type은 authorization server에 대해 client credential의 기밀성을 유지하고 신뢰할 수 있는 client authentication을 수행할 수 있는지로 구분한다.
`reference-public-confidential-client.md:53`
> Shared secret은 한 방식이고 private key나 mTLS 같은 다른 client authentication 방식도 가능하다.
문제는 이보다 앞의 문장과 다른 파생 문서가 여전히 예전 정의를 그대로 사용한다는 점이다.
### 잔여 1 — Reference 첫 정의
파일:
`docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/reference/reference-public-confidential-client.md`
23행:
> OAuth 클라이언트의 종류는 클라이언트 시크릿을 안전하게 보관할 수 있는지로 정한다.
41행에서 더 정확한 정의를 다시 제시하므로 **한 문서 안에서 정의가 두 개**가 된다.
### 잔여 2 — SSOT
파일:
`docs/keycloak/final/document.md`
183행:
> Public client는 브라우저처럼 client secret을 안전하게 숨길 수 없는 애플리케이션입니다.
210행:
> Confidential client는 client secret을 server에서 보관할 수 있는 애플리케이션입니다.
이 문장들은 “이 프로젝트가 shared secret을 사용한다”는 범위를 붙이면 설명용 단축 표현으로 사용할 수 있다.
하지만 현재 문장 구조는 OAuth client type의 일반 정의처럼 읽힌다.
### 잔여 3 — Case
파일:
`case-browser-credential-boundary.md`
101행:
> public client는 브라우저처럼 client secret을 안전하게 숨길 수 없는 애플리케이션이다.
파일:
`case-ap2-split-custody.md`
116행:
> confidential client는 client secret을 서버에 두고 자기를 인증할 수 있는 애플리케이션이다.
Case가 프로젝트 구현을 설명하는 문서인 만큼 shared secret 자체를 언급하는 것은 문제가 아니다. 다만 일반 정의처럼 시작하지 말고 프로젝트 scope를 먼저 걸어야 한다.
### 왜 중요한가
RFC 6749의 public/confidential 구분은 단순히 shared secret을 숨길 수 있는가만으로 정의되지 않는다. 핵심은 client credentials의 confidentiality를 유지할 수 있는지와 authorization server에 대해 secure client authentication을 수행할 수 있는지다.
따라서 Reference 본문만 수정하고 SSOT/Case의 정의형 문장을 남기면 semantic consistency가 깨진다.
### 수정 방향
Reference 첫 문장:
> OAuth client type은 authorization server에 대해 client credential의 기밀성을 유지하고 신뢰할 수 있는 client authentication을 수행할 수 있는지로 구분한다.
SSOT AP1:
> 이 프로젝트의 AP1 SPA는 사용자 기기에서 실행되어 장기 client credential의 기밀성을 유지하기 어려우므로 public client로 구성했습니다.
SSOT AP2:
> 이 프로젝트의 AP2 mediator는 server-side에서 shared client secret을 보호하고 `client_secret_basic`으로 인증하므로 confidential client로 구성했습니다.
Case도 같은 방식으로 **“일반 정의”가 아니라 “이 프로젝트에서 왜 그렇게 등록했는지”**로 바꾼다.
### 완료 조건
다음 패턴의 일반 정의형 문장이 0건이어야 한다.
- “public client는 client secret을 숨길 수 없는 애플리케이션이다”
- “confidential client는 client secret을 서버에 보관하는 애플리케이션이다”
- “client type은 client secret 보관 가능 여부로 정한다”
프로젝트에서 실제로 `client_secret_basic`을 쓴 사실은 삭제하지 않는다.
---
## R02 — MEDIUM — K03 본문은 고쳤지만 섹션 제목이 이전 의미를 남긴다
파일:
`docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/reference/reference-authorization-code-endpoints.md`
현재 제목:
> ### 2. Token Endpoint에서 비로소 클라이언트를 인증한다
그 아래 본문은 이미 다음처럼 정확하게 바뀌었다.
> client authentication도 이 요청에서 수행할 수 있다.
그리고:
> 이 프로젝트의 confidential client들은 `client_secret_basic`을 사용하므로 server-side component가 client secret으로 자신을 인증한다.
즉 본문은:
- public client가 항상 client authentication을 하는 것은 아님
- confidential client는 이 프로젝트에서 token endpoint에서 인증함
으로 고쳐졌다.
그러나 제목은 여전히 **Authorization Code Flow의 모든 client가 token endpoint에서 “클라이언트를 인증한다”**는 의미로 읽힌다.
### 수정 방향
예:
> ### 2. Client authentication이 필요한 경우 Token Endpoint에서 수행한다
또는 프로젝트 중심으로:
> ### 2. 이 프로젝트의 confidential client는 Token Endpoint에서 자신을 인증한다
첫 번째가 Reference 문서 성격에는 더 적절하다.
### 완료 조건
제목과 본문의 범위가 같아야 한다.
---
## R03 — MEDIUM — Authorization Endpoint URL이 “referrer에 남는다”는 표현이 과도하게 단정적이다
파일:
`docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/reference/reference-authorization-code-endpoints.md`
39행:
> URL이 히스토리와 서버 로그, referrer에 남고
46행:
> 링크를 타고 온 경우에는 referrer에도 남는다.
주소창, 브라우저 history, Authorization Server access log에 authorization request URL이 남을 수 있다는 설명은 타당하다.
문제는 **Referrer는 Referrer-Policy에 따라 전달 범위가 달라진다**는 점이다.
현대 브라우저의 기본 정책인 `strict-origin-when-cross-origin`에서는 일반적인 cross-origin 이동에서 전체 path/query가 아니라 origin만 보내며, downgrade에서는 referrer를 보내지 않는다. 같은 origin이거나 더 허용적인 정책에서는 path/query가 전달될 수 있다.
따라서 “authorization request URL이 referrer에 남는다”를 일반 규칙처럼 쓰면 scope가 넓다.
### 수정 방향
39행:
> Authorization Endpoint 요청 URL은 주소창과 브라우저 history, Authorization Server access log에 남을 수 있다. Referer 전달 범위는 브라우저의 Referrer-Policy와 요청 관계에 따라 달라진다.
46행:
> 같은 origin이거나 더 허용적인 Referrer-Policy에서는 path/query가 Referer에 포함될 수 있으므로, authorization request URL에 비밀값을 싣지 않는다.
이 문서의 핵심 결론인 “authorization endpoint query에 client secret을 넣지 않는다”는 유지한다.
### 완료 조건
- referrer에 full URL/query가 항상 남는다는 인상 제거
- Referrer-Policy에 따라 범위가 달라진다는 조건 명시
- 주소창/history/access log와 Referer를 같은 확실성으로 묶지 않음
---
## R04 — LOW — Figure 자동 검증은 PASS지만 context 검토 상태가 완전히 닫히지 않았다
현재 자동 결과:
`PROJECT LAYOUT: PASS — error 0 · warn 10`
keycloak warning:
- 8건: techviz 도구 부재로 전체 SSOT hash 대조 불가, context snapshot은 일치
- 2건: SSOT 문맥이 바뀐 뒤 그림을 다시 보지 않았다고 기록됨
- `final/.techviz/ap1-direct-architecture`
- `final/.techviz/login-api-phase-split`
이번 재검토에서 위 두 spec/context를 직접 읽었다.
### 직접 판독 결과
`ap1-direct-architecture`
- AP1 public SPA
- PKCE S256
- browser JS memory token custody
- Resource Server issuer/time/audience 검증
으로 현재 SSOT와 의미가 맞는다.
`login-api-phase-split`
- AP2 mediator가 token을 받음
- AP2 browser가 실제 API caller
- AP3는 BFF가 login/API 모두 담당
- AP4는 oauth2-proxy와 Nginx 책임 분리
- PKCE verifier 목록은 AP1/AP3/AP4이며 AP2를 억지로 포함하지 않음
으로 K01 수정과도 충돌하지 않는다.
따라서 **그림 내용 자체의 오류는 발견하지 않았다.**
다만 completion contract를 엄격하게 적용한다면 techviz가 남긴 stale-review 상태는 적절한 techviz review/regeneration 경로로 닫는 편이 맞다.
### 주의
- hash를 손으로 맞추지 않는다.
- SVG를 임의 수작업으로 고치지 않는다.
- techviz 정본 → spec/context → SVG 경로를 유지한다.
---
# 4. 직전 Findings 중 제대로 닫힌 항목
## K01 — CLOSED
AP3 설명 뒤에 AP2라는 주어 없이 붙어 있던 PKCE 미검증 문장이 다음처럼 수정됐다.
> 반면 AP2의 `token-mediating-confidential`은 ...
이제 다음 범위가 분명하다.
- AP3 = PKCE S256 확인
- AP2 = Authorization Code confidential client 확인, PKCE S256 미확인
SSOT와 Record의 certainty가 일치한다.
---
## K02 — CLOSED
`refreshTokenMaxReuse`가 시간 창이 아니라 reuse count라는 점이 명시됐다.
또:
- rotation
- max reuse count
- refresh token lifespan
을 서로 다른 설정으로 분리했다.
replica 동시 refresh의 실제 결과도 여전히 미검증으로 유지했다.
---
## K05 — CLOSED
기존:
> 남는 선택지는 하나다.
에서:
> 이 문서에서 비교하는 AP1~AP4 네 패턴만 놓고 보면 ...
으로 범위가 제한됐다.
OAuth/OIDC 전체 설계 공간의 유일한 답처럼 보이던 문제가 사라졌다.
---
## K06 — CLOSED
SSOT와 관련 Record에서:
> 사용자가 의도한 요청인지 확인
이라는 표현을 제거하고:
> cookie가 자동 첨부되는 state-changing request에 별도 anti-CSRF 검증 값을 요구해 cross-site forged request를 구분
하는 의미로 정리됐다.
SameSite / CSRF token / same-origin XSS의 역할도 기존 문맥과 충돌하지 않는다.
---
# 5. 자동 검증 결과
이번 재검토에서 현재 변경 상태 그대로 다시 실행했다.
| 항목 | 결과 |
|---|---|
| Tech Log Tree | PASS — records 24, topics 1, nodes 24, written 24, unwritten 0 |
| Project Layout | PASS — error 0, warn 10 |
| Record Audit | PASS — 문제 0 |
| Figure Text | PASS — SVG 10, sentence error 0 |
| Figure Provenance | PASS — error 0 |
| Figure Overlap | PASS — overlap 0 |
| Required Content | PASS — records 24, error 0 |
| SSOT Facts | PASS — error 0 |
| Natural Prose | PASS — 24/24 |
| Voice | PASS — 24/24 |
| Command Pedagogy | PASS — records 24, findings 0 |
| git diff --check | PASS |
| standalone verify-pipeline.py | PASS — exit 0 |
| full unittest regression | PASS — 391 tests, skipped 14 |
| live source reconciliation | **UNVERIFIABLE** |
pipeline과 unittest는 병렬로 돌리지 않았다.
---
# 6. 왜 자동 PASS인데 아직 완료가 아닌가
현재 자동 검사는 구조적으로 깨진 문서, tree, figure provenance, prose hard error, pipeline regression을 잘 잡는다.
하지만 다음은 자동 검사기가 보장하지 않는다.
- 한 문서의 첫 정의와 본문 정의가 의미상 서로 다른지
- 제목이 본문보다 범위를 넓히는지
- 프로젝트 구현 사실을 OAuth 일반 규칙으로 오해하게 만드는지
- Referrer-Policy 같은 조건을 생략해 문장이 과도하게 단정적인지
실제로 `check-ssot-facts.py` 자체도:
> 적힌 것이 맞는지만 본다. SSOT가 빠뜨린 finding은 찾지 못한다.
는 한계를 출력한다.
따라서 자동 PASS만으로 완료 판정을 내리지 않는다.
---
# 7. 수정 우선순위
## 1순위 — K04 완전 종료
다음 문서의 public/confidential 정의형 문장을 같은 scope로 맞춘다.
1. `final/document.md`
2. `reference-public-confidential-client.md`
3. `case-browser-credential-boundary.md`
4. `case-ap2-split-custody.md`
일반 정의와 프로젝트의 `client_secret_basic` 구현 사실을 분리한다.
## 2순위 — K03 제목 정리
`reference-authorization-code-endpoints.md`
> Token Endpoint에서 비로소 클라이언트를 인증한다
를 conditional/confidential scope가 드러나는 제목으로 바꾼다.
## 3순위 — Referrer scope
같은 Reference의 두 문장을 Referrer-Policy 조건을 포함하도록 수정한다.
## 4순위 — figure review 상태
내용은 직접 판독상 문제 없지만, 가능하면 techviz 정식 review 경로로 stale context 상태를 닫는다.
---
# 8. 수정 후 재검증 순서
1. stale client-type definition 검색
2. token endpoint heading/context 직접 판독
3. referrer 일반화 검색
4. SSOT ↔ Record semantic consistency
5. Natural prose
6. Voice
7. Tree
8. Layout
9. Figure Text / Provenance / Overlap
10. Required Content / SSOT Facts
11. Command Pedagogy
12. `git diff --check`
13. standalone `verify-pipeline.py`
14. full unittest regression
standalone pipeline과 unittest는 순차 실행한다.
---
# 9. 기술 근거
- RFC 6749 §2.1 — Client Types
- public/confidential은 client credential confidentiality 및 secure client authentication 능력 기준
- RFC 6749 Token Endpoint
- confidential client 또는 client credentials가 발급된 client가 token endpoint에서 인증
- 모든 public client가 token endpoint에서 client authentication을 하는 것은 아님
- RFC 9700 — OAuth 2.0 Security Best Current Practice
- public client PKCE 요구
- confidential client에도 PKCE 권고
- asymmetric client authentication 권고
- Keycloak RealmRepresentation
- `refreshTokenMaxReuse`는 Integer
- lifespan 계열 필드와 별도 속성
- MDN Referrer-Policy
- 기본 `strict-origin-when-cross-origin`
- same-origin과 cross-origin에서 Referer 전송 범위가 다름
---
# 최종 판정
**keycloak = 아직 더 봐야댐**
직전 리뷰를 무시하거나 새로운 방향으로 리뷰를 바꾼 것이 아니다.
오히려 직전 K01~K06을 같은 기준으로 다시 추적했을 때:
- K01 CLOSED
- K02 CLOSED
- K03 PARTIAL
- K04 PARTIAL
- K05 CLOSED
- K06 CLOSED
라는 상태다.
남은 핵심은 **public/confidential client 정의의 scope를 SSOT와 Record 전체에서 통일하는 것**, **token endpoint client authentication 제목을 본문 범위와 맞추는 것**, **Referrer-Policy 조건을 반영하는 것**이다.
이 세 축이 닫히고 동일한 전체 회귀가 다시 통과하면 그때 완료 여부를 다시 판정한다.