329 lines
52 KiB
Markdown
329 lines
52 KiB
Markdown
---
|
|
title: branch / feature-keycloak-oauth2-proxy-oidc-flow (P1A — oauth2-proxy 구성과 OIDC 흐름)
|
|
source_type: branch-note
|
|
status: raw
|
|
id: BR-KEYCLOAK-PATTERNS-OVERVIEW-012
|
|
kind: project-work-item
|
|
project: keycloak-patterns-overview
|
|
work_item: WI-KEYCLOAK-PATTERNS-OVERVIEW-012
|
|
inherits: [DEC-KEYCLOAK-PATTERNS-OVERVIEW-EDGE-FORWARDAUTH-001@1, DEC-KEYCLOAK-PATTERNS-OVERVIEW-ACCEPTANCE-001@1]
|
|
refines: []
|
|
overrides: []
|
|
depends_on: [WI-KEYCLOAK-PATTERNS-OVERVIEW-002]
|
|
contract_packet: 1
|
|
branch: feature-keycloak-oauth2-proxy-oidc-flow
|
|
parent_branch:
|
|
related_projects: [keycloak-patterns]
|
|
tags: [branch, keycloak-patterns, p1a, oauth2-proxy, oidc, cookie-session]
|
|
created: 2026-05-25
|
|
target_merge:
|
|
status_label: in-progress
|
|
contract_packet_sha256: 844b40d60f42d3186b5952aa27da4febf3da180c1f9fe017400afa625e0d7a36
|
|
---
|
|
|
|
# branch: feature-keycloak-oauth2-proxy-oidc-flow (P1A — oauth2-proxy 구성과 OIDC 흐름)
|
|
|
|
> Layer: `raw/branch-notes/` — [[raw/project-notes/keycloak-patterns-overview]] Work Item의 직접 자식 branch.
|
|
> oauth2-proxy 단독 컴포넌트의 구성과 OIDC 흐름을 **단계별**로 분해. nginx 통합은 별도 sub-sub 에서 다룬다.
|
|
> 본 sub-sub-branch는 **문서까지만** (`documented-only`). 실 구성/시연 대상 아님.
|
|
> `status_label`: `in-progress`
|
|
|
|
<!-- section-id: branch-parent -->
|
|
## 부모 (필수)
|
|
|
|
[[raw/project-notes/keycloak-patterns-overview]]
|
|
|
|
<!-- GENERATED: branch-contract:start -->
|
|
<!-- section-id: branch-contract-packet -->
|
|
## 브랜치 계약 패킷
|
|
|
|
- **생성 시 프로젝트 개정**: `1`
|
|
- **패킷 스키마**: `contract_packet: 1`
|
|
- **완료 조건**: unauthenticated redirect와 login 후 X-Forwarded-User backend 200이 재현된다
|
|
|
|
<!-- section-id: inherited-project-decisions -->
|
|
### 상속한 프로젝트 결정
|
|
|
|
| Decision Ref | Project Summary | Branch Application | Source |
|
|
|---|---|---|---|
|
|
| `DEC-KEYCLOAK-PATTERNS-OVERVIEW-EDGE-FORWARDAUTH-001@1` | AP4는 oauth2-proxy ForwardAuth를 사용한다 | oauth2-proxy OIDC flow와 forwarded-user 전달 경계에 적용한다 | [[raw/project-notes/keycloak-patterns-overview]] |
|
|
| `DEC-KEYCLOAK-PATTERNS-OVERVIEW-ACCEPTANCE-001@1` | done-bar는 E2E success와 signature security failure 재현·해결 evidence다 | unauthenticated redirect와 login 후 backend 200 재현 증거에 적용한다 | [[raw/project-notes/keycloak-patterns-overview]] |
|
|
|
|
<!-- section-id: branch-local-decisions -->
|
|
### 브랜치 지역 결정
|
|
|
|
> 기존 branch-local 결정은 아래 `## Decision Evidence Map`의 D-row가 소유하며 이 packet에서 복제하지 않는다.
|
|
|
|
| Decision ID | Decision | Relation | Supporting Claims | Status |
|
|
|---|---|---|---|---|
|
|
|
|
<!-- section-id: declared-overrides -->
|
|
### 선언한 예외
|
|
|
|
| Override ID | Overrides | Reason | Approval | Status |
|
|
|---|---|---|---|---|
|
|
|
|
없음.
|
|
<!-- GENERATED: branch-contract:end -->
|
|
|
|
<!-- section-id: branch-goal -->
|
|
## 목표
|
|
|
|
P1A 패턴의 핵심 컴포넌트인 **oauth2-proxy 자체의 설정과 OIDC handshake**를 단계별로 이해. 부모 sub-branch가 다이어그램/sequence 수준을 정리했다면, 본 문서는 **`provider=keycloak-oidc` 설정 + cookie session + OIDC discovery + JWT bearer 검증 경로** 각각이 어디서 동작하고 어떤 함정이 있는지를 분리해서 본다.
|
|
|
|
핵심 질문:
|
|
1. `provider=keycloak-oidc`와 `provider=oidc`(generic)의 실질 차이는? → role/group claim 매핑 + Keycloak userinfo endpoint 처리.
|
|
2. cookie domain · cookie secret · `--whitelist-domain`은 각각 어떤 공격 surface를 막는가?
|
|
3. cookie 없이 Bearer JWT를 검증하는 경로는 언제 쓰며, 실제 검증 메커니즘은 무엇인가? (RFC 7662 introspection 호출로 부르지 않음)
|
|
4. 로그아웃 시 Keycloak 세션까지 끊기 위한 흐름은? (RP-Initiated Logout)
|
|
|
|
- 이슈:
|
|
- PR:
|
|
|
|
<!-- section-id: branch-scope -->
|
|
## 범위
|
|
|
|
### 포함 범위
|
|
|
|
- `provider=keycloak-oidc` 설정 (`--client-id`, `--client-secret`, `--oidc-issuer-url`, `--redirect-url`)
|
|
- OIDC discovery / scopes / cookie 설정 (`--cookie-secret`, `--cookie-domain`, `--cookie-secure`, `--cookie-samesite`, `--cookie-expire`)
|
|
- JWT bearer 검증 경로 vs cookie session 경로 (`--skip-jwt-bearer-tokens`; RFC 7662 introspection과 구분)
|
|
- 로그아웃 흐름 (RP-Initiated Logout)
|
|
- role/group claim 매핑
|
|
|
|
### 제외 범위
|
|
|
|
- nginx 통합 (별도 sub-sub `feature-keycloak-nginx-auth-request-integration`)
|
|
- 헤더 spoofing 방어 (별도 sub-sub `feature-keycloak-header-spoofing-defense`)
|
|
- Traefik ForwardAuth 대안 비교 (별도 sub-sub `feature-keycloak-traefik-forwardauth-alternative`)
|
|
- 실 환경 구성 (P3A 한정, 본 sub-sub는 문서까지만)
|
|
|
|
## 근거 (필수, 최소 1개+)
|
|
|
|
> 상단 2개는 부모 sub-branch에서 인용한 외부 자료를 재참조. 나머지 5개는 **2026-07-17 `/branch-spec` 자동조사**로 본 sub-sub-branch 에서 신규 보존 — D5(cookie/session storage) · D6(logout) · D7(whitelist-domain) · D8(JWT bearer 분기) · D9(discovery) 의 UNSUPPORTED 해소용.
|
|
|
|
| Source | 정당화하는 결정 |
|
|
|---|---|
|
|
| [[raw/official-docs/oauth2-proxy-overview-config-official]] | oauth2-proxy 공식 (Reverse proxy + auth provider integration) — `provider=keycloak-oidc` 채택 근거 |
|
|
| [[raw/official-docs/oauth2-proxy-keycloak-oidc-provider-official]] | oauth2-proxy ↔ Keycloak OIDC 연동 — cookie session / scope mapping 결정 근거 |
|
|
| [[raw/official-docs/oauth2-proxy-cookie-redirect-flags-official]] | cookie 설정 표준화(D5: `--cookie-secret`/`--cookie-domain`/`--cookie-secure`/`--cookie-samesite`/`--cookie-expire`/`--cookie-refresh`) + open redirect 방어(`--whitelist-domain`) + OIDC discovery 우회(`--skip-oidc-discovery`) 근거 |
|
|
| [[raw/official-docs/oauth2-proxy-session-storage-official]] | D5 — session storage 백엔드(cookie vs redis) 선택의 공식 메커니즘 근거 (stateless cookie 저장, 세션 lock 부재, Redis ticket/SETEX 메커니즘, `--session-store-type`/`--redis-connection-url`/Sentinel·Cluster 플래그) |
|
|
| [[raw/official-docs/oauth2-proxy-endpoints-signout-official]] | `/oauth2/sign_out` 기본 동작(로컬 cookie만 삭제) + `rd` query parameter/`{id_token}` placeholder 로 Keycloak `end_session_endpoint` 트리거하는 메커니즘 — D6 (RP-Initiated Logout) 근거 |
|
|
| [[raw/official-docs/oauth2-proxy-behaviour-cookie-vs-bearer-official]] | oauth2-proxy 요청 검증 분기(opportunistic cookie/JWT 검증, invalid JWT fallback, 401/403/redirect 조건) 근거 — Bearer 경로를 RFC 7662 introspection으로 부를 근거는 없으며, 로컬 JWKS 검증 여부는 별도 확인 필요 |
|
|
| [[raw/official-docs/keycloak-oidc-logout-endpoint-official]] | Keycloak 측 RP-Initiated Logout 요구사항 — `end_session_endpoint`(`/realms/{realm}/protocol/openid-connect/logout`) + `id_token_hint` + `post_logout_redirect_uri` 파라미터 명세 + Backchannel Logout URL client 설정 — D6 (RP-Initiated Logout) 의 Keycloak 측 근거 (oauth2-proxy 측은 위 `oauth2-proxy-endpoints-signout-official` 가 커버) |
|
|
|
|
## TODO
|
|
|
|
각 항목 옆에 증거 등급 표기.
|
|
|
|
- [ ] `provider=keycloak-oidc` 설정 정리 (`--client-id`, `--client-secret`, `--oidc-issuer-url`, `--redirect-url`) — 등급: `planned`
|
|
- [ ] OIDC discovery (`.well-known/openid-configuration`) 호출 시점과 cache 정책 정리 — 등급: `planned`
|
|
- [ ] OIDC scopes 정리 (`openid profile email`, `groups`, `offline_access`) + Keycloak client scope 매핑 — 등급: `planned`
|
|
- [ ] cookie 설정 정리: `--cookie-secret` 생성(32byte), `--cookie-domain`, `--cookie-secure`, `--cookie-samesite=lax|strict`, `--cookie-expire` — 등급: `planned`
|
|
- [ ] cookie session 모드 vs Redis session store 모드 비교 — 등급: `planned`
|
|
- [ ] `--whitelist-domain` 옵션의 역할 (open redirect 방지) 정리 — 등급: `planned`
|
|
- [ ] JWT bearer 검증 경로 (`--skip-jwt-bearer-tokens`, `--extra-jwt-issuers`)의 의미와 실제 검증 메커니즘 정리 — RFC 7662 introspection으로 단정하지 않음 — 등급: `planned`
|
|
- [ ] 로그아웃 흐름 정리: `/oauth2/sign_out` + Keycloak RP-Initiated Logout (`end_session_endpoint`) 연계 — 등급: `planned`
|
|
- [ ] role/group claim 매핑: oauth2-proxy `--allowed-group` + Keycloak `groups` client scope mapper — 등급: `planned`
|
|
- [ ] 학습 시연 단계 정리 (실행 안 함, 문서상의 가상 단계만) — 등급: `planned`
|
|
|
|
## 진행 중 메모
|
|
|
|
> 작업하며 떠오른 메모.
|
|
|
|
- `provider=oidc` (generic)도 Keycloak에 동작하지만, `keycloak-oidc`는 group/role 매핑이 native라 `--allowed-group` 같은 옵션이 자연스럽게 동작.
|
|
- cookie session 모드는 access_token 자체를 cookie에 넣을 수 있어 nginx 헤더 4kb 한도 함정과 직결 (sub-sub `feature-keycloak-nginx-auth-request-integration`에서 다룸).
|
|
|
|
## 결정 사항 (decisions)
|
|
|
|
- **2026-05-25**: 본 sub-sub는 oauth2-proxy 단독 컴포넌트 학습으로 한정. nginx 통합·헤더 spoofing 방어·Traefik 대안은 형제 sub-sub 에서. 분리 이유 = 각 토픽의 함정이 서로 독립적이라 한 문서에 합치면 비교가 흐려짐.
|
|
- **2026-07-17** (`/branch-spec` 자동조사): D5(cookie 속성/session storage) · D6(RP-Initiated Logout) 의 `UNSUPPORTED_DECISION` 해소. 공식 문서 5건을 신규 보존해 D5a/D5b/D5c 로 분해하고, D6 을 oauth2-proxy 측(`O2PE-C1`~`C4`) + Keycloak 측(`KC-LOGOUT-C1`~`C6`) 양측 근거로 승격. 추가로 D7(`--whitelist-domain`) · D8(JWT bearer 분기) · D9(OIDC discovery) 를 신규 결정으로 분리. / 검토한 대안: logout 은 back-channel logout·로컬 cookie 삭제만 두 대안을 비교했고 → RP-Initiated 채택(back-channel 은 oauth2-proxy 수신 지원 미확인, §Audit `BACKCHANNEL_UNVERIFIED`). session storage 는 cookie·Redis 비교 → **인스턴스 수가 아니라 세션 payload 크기가 실제 결정 변수**임을 확인하고 조건부로 남김. / 근거: [[raw/official-docs/oauth2-proxy-endpoints-signout-official]], [[raw/official-docs/keycloak-oidc-logout-endpoint-official]], [[raw/official-docs/oauth2-proxy-cookie-redirect-flags-official]], [[raw/official-docs/oauth2-proxy-session-storage-official]], [[raw/official-docs/oauth2-proxy-behaviour-cookie-vs-bearer-official]]
|
|
- **2026-07-18** (drift 해소): 기존 **"token introspection 모드"** 명칭을 **"JWT bearer 검증 경로"**로 교정. 공식 문서가 RFC 7662 introspection endpoint 호출을 서술하지 않으므로 두 메커니즘을 동일시하지 않는다. 로컬 JWKS 검증 여부는 실측 전까지 `needs-confirmation`으로 유지한다.
|
|
|
|
## 결정-근거 매핑
|
|
|
|
> 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시. 본 sub-sub-branch 의 결정 (D1) 은 학습 범위 분할 (scoping) 결정으로 외부 vendor doc 인용 없음 — UNSUPPORTED_DECISION 으로 표기.
|
|
> TODO 항목 중 외부 vendor doc 으로 뒷받침되는 것은 D2~ 로 분리해 명시.
|
|
|
|
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
|
|---|---|---|---|---|---|
|
|
| D1 | 본 sub-sub 는 oauth2-proxy 단독 컴포넌트 학습으로 한정 (nginx 통합 / spoofing 방어 / Traefik 대안 분리) | N/A — 학습 범위 분할(organizational). 분기 없음 | UNSUPPORTED_DECISION (학습 분할 결정은 내부 scoping — 외부 vendor doc 인용 불요) | N/A (organizational decision) | 분할이 너무 잘게 쪼개져 다 모았을 때 비교 매트릭스를 다시 합성해야 하는 비용 |
|
|
| D2 | `provider=keycloak-oidc` + `--client-id` + `--client-secret` + `--oidc-issuer-url` 4종 파라미터를 oauth2-proxy ↔ Keycloak 연결의 필수 입력으로 채택 | Keycloak **17+** → `--oidc-issuer-url=https://<host>/realms/<realm>`. **17 미만** → `/auth/realms/<realm>` (legacy context path). group/role 을 oauth2-proxy 레벨에서 안 쓸 거면 generic `provider=oidc` 도 가능하나 D4 의 native 매핑을 잃음 | `raw/official-docs/oauth2-proxy-keycloak-oidc-provider-official.md#O2PK-C1`, `raw/official-docs/oauth2-proxy-keycloak-oidc-provider-official.md#O2PK-C2`, `raw/official-docs/oauth2-proxy-overview-config-official.md#OAUTH2PROXY-C5` | `official-vendor-doc` (oauth2-proxy 공식 + Keycloak 17+ issuer URL 패턴 명시) | Keycloak 17+ context-path (`/realms/` vs `/auth/realms/`) 가 reverse-proxy 가 `/auth` prefix 를 재추가한 환경에서 어떻게 동작하는지 미검증 |
|
|
| D3 | OIDC scopes 정리: `openid profile email` + `groups` (group authorization 필요 시) + `offline_access` (refresh token 필요 시) — Keycloak client scope 매핑 필요 | `--allowed-group` 을 쓸 때만 `groups` scope + Group Membership mapper 추가(O2PK-C5). refresh token 이 필요할 때만 `offline_access` — 불필요하면 빼서 세션 payload 를 줄임(D5a 의 4kb 압력과 직결) | `raw/official-docs/oauth2-proxy-keycloak-oidc-provider-official.md#O2PK-C5` (groups client scope 필요), `raw/official-docs/oauth2-proxy-keycloak-oidc-provider-official.md#O2PK-C3` | `official-vendor-doc` (**`groups` 부분만**) + `UNSUPPORTED_DECISION` (**base scope · `offline_access` 부분**) | ① client scope 이름이 정확히 `groups` 가 아닐 때 동작 보장 안 됨 — default vs optional scope 구분 별도. ② **base scope 문자열(`openid profile email`)과 `offline_access`↔refresh token 관계는 본 branch 근거 raw 에 문자열이 0건** — `O2PK-C3`/`C5` 는 `groups` client scope + Group Membership mapper 만 증명한다. 이 부분은 OIDC 일반 배경지식에서 온 사용자 임의 결정이며 벤더 권고가 아님(§구현 가이드 1 의 `--scope` `UNSUPPORTED_IMPL_DECISION` 참조) |
|
|
| D4 | role/group claim 매핑: `--allowed-role=<realm role>` 또는 `--allowed-role=<client>:<client role>` + `--allowed-group=</group>` | realm 전역 권한 → `--allowed-role=<realm role>`. 특정 client 한정 권한 → `--allowed-role=<client id>:<client role>`. 조직 트리 기반 → `--allowed-group=</group>` (+ D3 의 `groups` scope 필수). 인가를 edge 에서 안 하고 backend 로 미룰 거면 셋 다 미설정("valid user" 만 요구, O2PK-C3) | `raw/official-docs/oauth2-proxy-keycloak-oidc-provider-official.md#O2PK-C3`, `raw/official-docs/oauth2-proxy-keycloak-oidc-provider-official.md#O2PK-C4` | `official-vendor-doc` | **인가(authorization) 실패 시 응답 코드는 여전히 미확인.** D8 의 `O2PBEH-C2~C4` 는 *인증(authentication)* 단계의 401/403/redirect 만 증명 — role/group 불일치 시의 코드는 별도. nginx `error_page` 처리에 영향 |
|
|
| D5a | session storage 백엔드 기본값 = cookie (`--session-store-type=cookie`, stateless, 클라이언트 저장 + 매 요청 전송, 세션 lock 부재로 동시 refresh 시 재인증 강제 가능) | 세션 payload 가 4kb 미만으로 유지되고(= D3 에서 `offline_access`/과다 role claim 회피) 컴포넌트 최소화가 우선이면 cookie. payload 가 4kb 를 넘길 여지가 있거나 access_token 을 backend 로 전달하면 → **D5b(Redis)**. 인스턴스 개수는 이 선택의 기준이 **아님**(단일 EC2 여도 4kb 압력은 동일) | `raw/official-docs/oauth2-proxy-session-storage-official.md#O2PSESS-C1`, `raw/official-docs/oauth2-proxy-session-storage-official.md#O2PSESS-C2`, `raw/official-docs/oauth2-proxy-nginx-integration-official.md#O2PN-C6` (**4kb 임계값의 실제 owner claim** — session-storage 문서가 아니라 nginx 통합 문서에 있음) | `official-vendor-doc` | 쿠키 실제 바이트 한도·4kb 초과 시 분할(split) 동작은 이 자료에 없음 — 별도 raw source 필요 (`OAUTH2PROXY-SESSION-STORAGE` 문서 §Usage Boundaries 참고). Azure/Google federation 사례의 큰 토큰 크기를 Keycloak native 환경에 일반화 금지 |
|
|
| D5b | Redis session store 채택 시 `--session-store-type=redis` + `--redis-connection-url=redis://host[:port][/db-number]` 로 연결하며, 클라이언트에는 ticket(`{CookieName}-{ticketID}.{secret}`)만 전달 (세션 본문은 서버측 Redis 에 `SETEX` 로 암호화 저장). Sentinel/Cluster 는 `--redis-use-sentinel=true`/`--redis-use-cluster=true` (상호 배타)로 구성 | 4kb 초과 위험 **또는** access_token 헤더 전달 중 하나라도 해당하면 Redis. 둘 다 아니면 D5a 로 남김(컴포넌트 1개 추가는 P1A 의 "단일 EC2 최소 구성" 과 상충). standalone 이 기본이고 Sentinel↔Cluster 는 상호 배타이므로 동시 지정 금지. **"다중 replica" 는 트리거가 아님** — cookie store 는 "completely stateless"(`O2PSESS-C1`) 라 `--cookie-secret` 만 공유하면 replica 간 세션이 성립한다. D5a 의 "인스턴스 수는 기준이 아님" 과 정합 | `raw/official-docs/oauth2-proxy-session-storage-official.md#O2PSESS-C3`, `raw/official-docs/oauth2-proxy-session-storage-official.md#O2PSESS-C4`, `raw/official-docs/oauth2-proxy-session-storage-official.md#O2PSESS-C5`, `raw/official-docs/oauth2-proxy-session-storage-official.md#O2PSESS-C6` | `official-vendor-doc` | P1A(단일 EC2, documented-only) 규모에서 Redis 도입이 실제로 "필요"한지는 이 자료가 증명하지 않음 — 메커니즘 존재만 확인. Redis 도입 시 `--cookie-secret` 관리 부담은 사라지지 않음(ticket 암호화에 계속 사용) |
|
|
| D5c | cookie 속성값 표준화 채택: `--cookie-secret`(seed string, `-file` 변형은 raw binary 16/24/32byte) / `--cookie-domain` / `--cookie-secure=true`(기본값) / `--cookie-samesite=""`(기본값 — 이때 브라우저가 실제로 어떤 SameSite 로 해석하는지는 `O2PCOOKIE-C1` 범위 밖) / `--cookie-expire=168h0m0s`(기본값) / `--cookie-refresh`(기본 비활성, Keycloak 은 지원 provider 목록에 포함) | HTTPS 종단이 있으면 `--cookie-secure=true`(기본값 유지). 순수 로컬 `http://` 시연에 한해서만 `false` — 이 경우 "로컬 한정 예외" 라벨 필수. `--cookie-csrf-samesite` 를 따로 안 주면 CSRF 쿠키가 세션 쿠키의 samesite 를 **상속**(O2PCOOKIE-C6)하므로, samesite 를 조일 때 두 값을 함께 판단 | `raw/official-docs/oauth2-proxy-cookie-redirect-flags-official.md#O2PCOOKIE-C1`, `#O2PCOOKIE-C2`, `#O2PCOOKIE-C3`, `#O2PCOOKIE-C4`, `#O2PCOOKIE-C5`, `#O2PCOOKIE-C6` | `official-vendor-doc` | 공식 문서는 각 플래그의 존재·기본값만 증명 — P1A 배포에서 `--cookie-samesite` 를 `lax`/`strict`/`none` 중 무엇으로 명시할지는 별도 아키텍처 결정(교차 사이트 redirect 여부에 따름), byte 길이 제약이 `--cookie-secret-file` 행에만 명시돼 `--cookie-secret` 자체에도 적용되는지는 미확정 |
|
|
| D6 | RP-Initiated Logout 채택 — `/oauth2/sign_out?rd=<Keycloak end_session_endpoint>` 형태로 **`rd` query parameter**(또는 `X-Auth-Request-Redirect` 헤더)에 Keycloak `end_session_endpoint`(`/realms/{realm}/protocol/openid-connect/logout`)를 지정하고, `{id_token}` placeholder 로 `id_token_hint` 를 주입. **전제: 그 도메인이 `--whitelist-domain` 에 등록돼야 함(D7)** | Keycloak 세션까지 끊어야 하면 이 결정. oauth2-proxy 로컬 cookie 만 지우면 충분하면 기본 `/oauth2/sign_out`(rd 없이) — 단 이 경우 **IdP 세션이 남아 재접근 시 자동 재로그인**(O2PE-C1)되므로 "로그아웃이 안 된 것처럼" 보임. `post_logout_redirect_uri` 를 쓰려면 `client_id` 또는 `id_token_hint` 중 하나를 반드시 동반(KC-LOGOUT-C5) | `raw/official-docs/oauth2-proxy-endpoints-signout-official.md#O2PE-C1`, `#O2PE-C2`, `#O2PE-C3`, `#O2PE-C4`, `raw/official-docs/keycloak-oidc-logout-endpoint-official.md#KC-LOGOUT-C1`, `#KC-LOGOUT-C2`, `#KC-LOGOUT-C3`, `#KC-LOGOUT-C4`, `#KC-LOGOUT-C5`, `#KC-LOGOUT-C6` | `official-vendor-doc` (**파라미터 계약** — `rd`/`{id_token}`/`id_token_hint`/`post_logout_redirect_uri`: oauth2-proxy 측 + Keycloak 측 **양측** 교차 확보, 2026-07-17 UNSUPPORTED_DECISION 해소) + `needs-confirmation` (**경로 문자열**) | ① oauth2-proxy 가 back-channel logout **수신자**로 동작하는지는 공식 문서에서 확인 안 됨(§Audit & Findings `BACKCHANNEL_UNVERIFIED`). ② logout 후 Keycloak 세션이 실제로 종료되는지는 여전히 실측 대상(§Claims To Verify). ③ `rd` 대상이 `end_session_endpoint` 여야 한다는 것은 문서의 **권고(convention)** 이지 oauth2-proxy 가 강제 검증하지 않음. ④ **경로 `/realms/{realm}/protocol/openid-connect/logout` 은 `KC-LOGOUT-C1` 의 quote 에 없고 claim 서술문에만 존재** → 하드코딩 금지, discovery 응답의 `end_session_endpoint` 를 읽을 것(§구현 가이드 2 의 3단계 · §Claims To Verify) |
|
|
| D7 | `--whitelist-domain` 에 redirect 허용 도메인을 **명시 등록**. 서브도메인 전체 허용은 `.example.com` 또는 `*.example.com` prefix 사용 | Keycloak 이 oauth2-proxy 와 **다른 도메인**이면 필수 — 미등록 시 D6 의 logout redirect 가 **조용히 무시**됨(O2PE-C4). 같은 도메인 안에서 상대경로 redirect 만 쓰면 **불필요할 가능성** (단정 불가 — 미설정 시 기본 동작이 공식 문서에 없어 Open Risk 참조. 확정 전까지는 안전측으로 항상 명시 등록 권장). 기본 동작은 URL 프로토콜의 default port(80/443)만 허용하므로, 비표준 포트를 쓰면 포트까지 명시 필요(O2PCOOKIE-C7) | `raw/official-docs/oauth2-proxy-cookie-redirect-flags-official.md#O2PCOOKIE-C7`, `raw/official-docs/oauth2-proxy-endpoints-signout-official.md#O2PE-C4` | `official-vendor-doc` | **미설정 시 기본 동작(외부 도메인 전부 차단인지)이 공식 문서에 명시되지 않음** — `needs-confirmation`. 또한 이 옵션의 suffix 매칭에 과거 우회 취약점 이력이 있다고 **전해지나 본 라운드에서 검증하지 않았다** (미검증 — §Audit & Findings `WHITELIST_CVE_HISTORY`, raw 미보존) → 옵션 설정만으로 open redirect 가 닫힌다고 단정 금지 |
|
|
| D8 | 요청 검증 분기: 브라우저 요청은 session cookie 경로, `Authorization: Bearer <JWT>` 요청은 `--skip-jwt-bearer-tokens` 경로로 **자동 분기**(택1 아님 — 한 배포에서 공존). invalid JWT 는 기본 로그인 redirect, `--bearer-token-login-fallback=false` 면 403 | API/M2M 클라이언트가 있으면 `--skip-jwt-bearer-tokens` 설정 + `--bearer-token-login-fallback=false`(JSON 클라이언트에 HTML 로그인 페이지 대신 403 반환). 브라우저 전용이면 기본값 유지. 다른 issuer 의 JWT 도 받으려면 `--extra-jwt-issuers=<issuer>=<audience>` | `raw/official-docs/oauth2-proxy-behaviour-cookie-vs-bearer-official.md#O2PBEH-C1`, `#O2PBEH-C2`, `#O2PBEH-C3`, `#O2PBEH-C4` | `official-vendor-doc` (**실패 3경로** — `O2PBEH-C2`/`C3`/`C4`) + `UNSUPPORTED_DECISION` (**인증 강제 라우트에서의 cookie/JWT 공존·우선순위**) | ① **명칭 경계** — 이 경로는 **JWT bearer 검증 경로**이며 RFC 7662 introspection 호출로 부르지 않는다. 로컬 JWKS 서명 검증 여부도 behaviour 페이지가 직접 명시하지 않아 **미확정**이다. ② **헤드라인의 "자동 분기·공존" 은 공식 보장이 아님** — `O2PBEH-C1` 은 `--skip-auth-route` **전용 인용**이고 그 does-not-prove 가 강제 라우트에서의 cookie/JWT 순서·우선순위를 범위 밖으로 못박는다. 통과 경로는 실패 경로(`C3`/`C4`)의 대우에서 도출한 추론(§구현 가이드 3 · §Claims To Verify) |
|
|
| D9 | OIDC discovery 활성(기본) — `--oidc-issuer-url` 로부터 `.well-known/openid-configuration` 자동 조회. 우회하려면 `--skip-oidc-discovery` + `--login-url`(Authentication endpoint) / `--redeem-url`(Token redemption endpoint) / `--oidc-jwks-url` **3종 전부** 수동 지정 | 네트워크로 issuer 에 도달 가능하면 기본값(discovery 활성). **폐쇄망 등으로 issuer 도달이 불가능**하면 `--skip-oidc-discovery` + 3종 수동(이게 `O2PCOOKIE-C8` 이 실제로 닫는 축). 서명 키를 정적으로 고정하려면 `--oidc-public-key-file`(PEM) — 단 키 rotation 시 수동 재배포 필요. **기동 순서(Keycloak 이 늦게 뜨는 문제)는 이 결정의 축이 아니다** — 그건 [[raw/branch-notes/feature-keycloak-docker-compose-stack]] `D3`(healthcheck 게이팅)가 owner 이며, discovery 를 끄는 것은 그 문제의 해법이 아님 | `raw/official-docs/oauth2-proxy-cookie-redirect-flags-official.md#O2PCOOKIE-C8`, `#O2PCOOKIE-C9` | `official-vendor-doc` | **discovery 호출 시점(기동 1회 vs 주기적)과 JWKS cache TTL 이 공식 prose 문서에 없음** — `needs-confirmation`(§Claims To Verify). 기동 시 discovery 실패가 실제 실패 모드인지 확인되면 대응은 compose `D3` 로 위임(본 branch 재진술 금지) |
|
|
|
|
## 구현 가이드
|
|
|
|
> 본 sub-sub-branch 는 `documented-only` — 실 구성/시연 대상이 아니다. 따라서 본 §는 "코드를 어디에 쓸 것인가" 가 아니라 **학습 시연 문서상의 가상 구성 명세**(TODO 마지막 항목)로 읽는다. P3A 실 구현 단계에서 이 명세가 실제 config 의 출발점이 된다.
|
|
> 3-rule (CLAUDE.md §15.5) 적용: 각 sub-section 은 Decision ID + Supporting Claim ID 를 Trace 하고, 근거 없는 detail 은 `UNSUPPORTED_IMPL_DECISION` 라벨 + trade-off 한 줄을 단다.
|
|
|
|
### 1. oauth2-proxy 기동 플래그 세트 (문서상 가상 구성)
|
|
|
|
> **Trace**: D2(`O2PK-C1`/`O2PK-C2`/`OAUTH2PROXY-C5`) + D3(`O2PK-C3`/`O2PK-C5`) + D4(`O2PK-C3`/`O2PK-C4`) + D5a·D5c(`O2PSESS-C1`/`O2PSESS-C2`, `O2PCOOKIE-C1`~`C6`) + D9(`O2PCOOKIE-C8`/`C9`).
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: `--cookie-samesite=lax` 로 *명시* 하는 것 — 공식 문서는 기본값이 `""`(빈 문자열)임만 증명하고(`O2PCOOKIE-C1`) 어느 값을 쓰라고 권고하지 않는다. trade-off: OIDC 콜백이 cross-site top-level GET redirect 라 `lax` 가 CSRF 쿠키를 통과시키는 최소값으로 판단 — `strict` 는 콜백 실패 위험, `none` 은 CSRF 표면 확대. **P3A 실측 전까지 확정 아님.**
|
|
> - **UNSUPPORTED_IMPL_DECISION**: `--cookie-secret` 을 32byte 로 생성하는 관행 — 공식 문서는 16/24/32byte 제약을 `--cookie-secret-file`(raw binary) 행에만 명시하고(`O2PCOOKIE-C3`) `--cookie-secret`(seed string) 자체에 같은 제약이 걸리는지는 서술하지 않는다. trade-off: AES-256 을 쓰는 32byte 가 세 허용값 중 최댓값이라 안전측 선택.
|
|
> - **UNSUPPORTED_IMPL_DECISION**: `--cookie-domain` 의 값 — D5c 결정문이 이 플래그를 "표준화 대상" 으로 열거하나, **보존된 claim(`O2PCOOKIE-C1`~`C6`) 중 `--cookie-domain` 을 다루는 것은 없다**(C1 samesite / C2 secure / C3 secret / C4 expire / C5 refresh / C6 csrf-samesite). trade-off: oauth2-proxy 와 앱이 같은 host 면 미설정(host-only cookie)이 최소 표면; 서브도메인으로 분리되면 `.example.com` 로 넓혀야 하나 그만큼 쿠키 전송 범위가 커짐. **미설정 시 host-only 가 되어 서브도메인 구성에서 로그인 루프의 원인이 될 수 있음** — P3A 에서 실측 필요.
|
|
> - **UNSUPPORTED_IMPL_DECISION**: `--scope` 의 base 값 `openid profile email` 과 "`offline_access` 는 refresh 필요 시만" 조건 — `O2PK-C5`(groups scope + mapper 필요) / `O2PK-C3`(인가 확장) 어느 것도 base scope 문자열이나 `offline_access` ↔ refresh token 관계를 서술하지 않는다(OIDC 일반 배경지식). trade-off: `openid` 는 OIDC 필수, `profile email` 은 `X-Auth-Request-Email` 등 헤더 전달용 관행 — **공식 vendor doc 의 권고 아님**.
|
|
|
|
| 플래그 | 값 (학습 시연 가정) | 근거 | 비고 |
|
|
|---|---|---|---|
|
|
| `--provider` | `keycloak-oidc` | D2 / `O2PK-C1` | generic `oidc` 대비 role/group native 매핑 확보 |
|
|
| `--client-id` / `--client-secret` | (realm client 에서 발급) | D2 / `O2PK-C1` | Usage 예시는 confidential client 형식 |
|
|
| `--oidc-issuer-url` | `https://<keycloak host>/realms/<realm>` | D2 / `O2PK-C2` | Keycloak 26.x = 17+ → `/auth` prefix **없음** |
|
|
| `--scope` | `openid profile email` (+`groups` 조건부) | D3 / `O2PK-C5` — **base 값 + `offline_access` 조건은 `UNSUPPORTED_IMPL_DECISION`**(위 참조) | `groups` 는 `--allowed-group` 쓸 때만(이건 `O2PK-C5` 근거 있음). `offline_access` 는 refresh 필요 시만 — 넣으면 세션 payload 가 커져 D5a 의 4kb 압력 상승 |
|
|
| `--cookie-domain` | (미정 — 배포 토폴로지 의존) | **`UNSUPPORTED_IMPL_DECISION`**(위 참조 — 보존 claim 없음) | 같은 host 면 미설정(host-only), 서브도메인 분리 시 `.example.com`. 미설정 + 서브도메인 = 로그인 루프 위험 |
|
|
| `--allowed-role` / `--allowed-group` | 조건부 (D4 선택 조건 표 참조) | D4 / `O2PK-C4` | 미설정 시 "valid user" 만 요구 |
|
|
| `--session-store-type` | `cookie` (기본, 단일 EC2 학습 구성) | D5a / `O2PSESS-C1` | 4kb 압력 시 `redis` (D5b) |
|
|
| `--cookie-secure` | `true` (기본값 유지) | D5c / `O2PCOOKIE-C2` | 로컬 `http://` 시연에 한해 `false` — 예외 라벨 필수 |
|
|
| `--cookie-expire` | `168h0m0s` (기본값 유지) | D5c / `O2PCOOKIE-C4` | `0` 이면 브라우저 종료 시 만료 |
|
|
| `--whitelist-domain` | Keycloak 도메인 (D6 전제) | D7 / `O2PCOOKIE-C7`·`O2PE-C4` | **미등록 시 logout redirect 무시** |
|
|
| `--code-challenge-method` | `S256` | D2 / `O2PK-C6` | PKCE — 형제 branch `feature-keycloak-pkce-flow-stages` 가 owner |
|
|
|
|
### 2. 로그아웃 URL 조립 (D6 의 실제 형태)
|
|
|
|
> **Trace**: D6(`O2PE-C1`~`C4`, `KC-LOGOUT-C1`~`C6`) + D7(`O2PCOOKIE-C7`).
|
|
>
|
|
> - **근거 있는 결정**: `rd` 파라미터 · `{id_token}` placeholder · `id_token_hint`/`post_logout_redirect_uri` 요구사항 (`O2PE-C2`, `O2PE-C3`, `O2PE-C4`, `KC-LOGOUT-C3`, `KC-LOGOUT-C5`, `KC-LOGOUT-C6`) — 양측 공식 문서 verbatim 으로 뒷받침됨.
|
|
> - **UNSUPPORTED_IMPL_DECISION**: 3단계의 **경로 문자열** `/realms/<realm>/protocol/openid-connect/logout` — `KC-LOGOUT-C1` 의 evidence quote 전문은 "The logout endpoint logs out the authenticated user." 뿐이고 경로는 quote 에 **없다**(claim 서술문에만 존재). 같은 claim 이 "이 경로가 discovery 문서의 `end_session_endpoint` 필드 값과 동일하게 노출된다는 명시적 문장은 이 인용에 없음" 을 자인. trade-off: 경로를 하드코딩하지 말고 **discovery 응답의 `end_session_endpoint` 를 읽는 것이 안전** — 하드코딩은 Keycloak context-path 변경(D2 의 17+ 이슈)에 취약. `needs-confirmation` (§Claims To Verify).
|
|
|
|
**중요**: `--backend-logout-url` 이라는 플래그는 **oauth2-proxy 공식 endpoints 문서에 존재하지 않는다**(§Audit & Findings `MECHANISM_DRIFT`). 실제 메커니즘은 `rd` query parameter + placeholder 치환이다.
|
|
|
|
| 단계 | 조립 | 근거 |
|
|
|---|---|---|
|
|
| 1. 로그아웃 진입 | 사용자를 `/oauth2/sign_out?rd=<urlencoded end_session URL>` 로 redirect | `O2PE-C1`, `O2PE-C2` |
|
|
| 2. oauth2-proxy 동작 | 자신의 세션 cookie 만 삭제 → `rd` 대상으로 redirect. **`rd` 도메인이 `--whitelist-domain` 미등록이면 redirect 무시** | `O2PE-C1`, `O2PE-C4` |
|
|
| 3. `end_session_endpoint` | `https://<keycloak host>/realms/<realm>/protocol/openid-connect/logout` — **discovery 응답에서 읽을 것(하드코딩 금지)** | **`UNSUPPORTED_IMPL_DECISION`** (경로가 `KC-LOGOUT-C1` quote 에 없음 — claim 서술문만) |
|
|
| 4. `id_token_hint` 주입 | `rd` URL 안에 `{id_token}` placeholder 를 넣으면 oauth2-proxy 가 실제 ID Token 으로 치환 | `O2PE-C3` |
|
|
| 5. `post_logout_redirect_uri` | 쓰려면 `client_id` **또는** `id_token_hint` 중 하나 필수 + client 의 `Valid Post Logout Redirect URIs` 에 등록돼 있어야 함 | `KC-LOGOUT-C5`, `KC-LOGOUT-C6` |
|
|
|
|
### 3. 요청 검증 분기 (D8)
|
|
|
|
> **Trace**: D8(`O2PBEH-C1`~`C4`).
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: **아래 통과 2행**(`cookie 검증 후 통과` / `valid JWT → 세션 없이 통과`) — 근거인 `O2PBEH-C1` 은 **`--skip-auth-route` 로 인증이 스킵된 라우트** 전용 인용이며("Authentication is not enforced, but the proxy will opportunistically attempt to validate…"), 같은 claim 의 Does-not-prove 가 "스킵되지 않은(=인증 강제) 라우트에서 cookie/JWT 를 각각 어떤 순서·우선순위로 시도하는지는 본 인용 범위 밖" 이라 못박는다. 즉 **인증 강제 라우트의 통과 동작은 공식 미서술** — 아래 2행은 실패 경로(`C3`/`C4`)의 대우(對偶)에서 도출한 추론이다. trade-off: 실패 경로가 명시적으로 정의된 이상 통과 경로가 그 여집합이라고 보는 것이 합리적이나, 공식 보장은 아님. **실패 3행(`C2`/`C3`/`C4`)은 근거 있는 결정.**
|
|
> - 이 경로의 *명칭*은 §Audit & Findings `NAMING_DRIFT` 참조(구현 detail 이 아니라 용어 문제).
|
|
|
|
| 요청 형태 | oauth2-proxy 동작 | 근거 |
|
|
|---|---|---|
|
|
| session cookie 보유 브라우저 요청 | cookie 검증 후 통과 | **`UNSUPPORTED_IMPL_DECISION`** (추론 — `C3`/`C4` 실패 경로의 대우. `O2PBEH-C1` 은 skip-auth-route 전용) |
|
|
| `Authorization: Bearer <valid JWT>` (`--skip-jwt-bearer-tokens` 설정 시) | JWT 검증 후 세션 없이 통과 | **`UNSUPPORTED_IMPL_DECISION`** (추론 — 상동. §Claims To Verify 실측 대상) |
|
|
| `Authorization: Bearer <invalid JWT>` | **기본: 로그인 페이지 redirect** | `O2PBEH-C3` |
|
|
| 위 + `--bearer-token-login-fallback=false` | `403 Forbidden` | `O2PBEH-C4` |
|
|
| 미인증 + `Accept: application/json` | `401 Unauthorized` (redirect 아님) | `O2PBEH-C2` |
|
|
|
|
## 엣지·실패·의존
|
|
|
|
> R4(깊이 게이트) 캡처용. 정상 경로 외에 *학습 시연/후속 구현에서 부딪힐* 실패·엣지와 다른 계약 의존.
|
|
|
|
- **실패·엣지 경로**:
|
|
- **로그아웃이 조용히 실패**: `rd` 대상(Keycloak) 도메인이 `--whitelist-domain` 에 없으면 oauth2-proxy 가 **에러 없이 redirect 를 무시**한다(`O2PE-C4`). 결과적으로 oauth2-proxy cookie 만 지워지고 Keycloak 세션은 살아남아, 재접근 시 자동 재로그인(`O2PE-C1`)되어 **"로그아웃이 안 된 것처럼" 보인다**. D6 과 D7 이 한 몸인 이유 — D7 없이 D6 만 설정하면 D6 은 무효.
|
|
- **로그아웃 확인 화면**: `id_token_hint` 없이 `end_session_endpoint` 를 호출하면 Keycloak 이 사용자에게 로그아웃 확인을 요구할 수 있다(`KC-LOGOUT-C3`) → 무인 redirect 흐름이 사용자 클릭에서 멈춤.
|
|
- **post_logout_redirect_uri 거부**: client 의 `Valid Post Logout Redirect URIs` 에 미등록이면 거부(`KC-LOGOUT-C6`), `client_id`/`id_token_hint` 둘 다 없으면 거부(`KC-LOGOUT-C5`).
|
|
- **동시 요청 세션 충돌**: cookie store 는 세션 lock 이 없어 동시 refresh 시 충돌 → **강제 재인증** 가능(`O2PSESS-C2`). 단일 EC2 여도 다중 탭/병렬 XHR 이면 발생 — 인스턴스 수와 무관.
|
|
- **세션 4kb 초과**: access_token 을 cookie 에 담으면 4kb 한도에 걸릴 수 있고, nginx 는 `auth_request` 응답의 **첫 `Set-Cookie` 만 복사**하므로 분할 쿠키가 유실될 수 있다 → 로그인 루프. 대응 owner 는 형제 branch(아래 의존 참조). 단, Keycloak native user store(Google federation 없음)라 Azure/Google federation 사례보다 토큰이 작을 가능성 — **실측 전까지 확정 불가**.
|
|
- **API 클라이언트에 HTML 로그인 페이지 반환**: invalid JWT 의 기본 동작이 로그인 redirect(`O2PBEH-C3`)라 JSON 클라이언트가 HTML 을 받는다 → `--bearer-token-login-fallback=false` 로 403 전환(`O2PBEH-C4`) 필요.
|
|
- **인가 거부(authentication 성공 + authorization 실패)**: 로그인은 됐으나 `--allowed-role`/`--allowed-group` 에 안 맞는 사용자의 **응답 코드가 미확정**이다. `O2PK-C3` 이 "인가 실패 시 401 vs 403 의 정확한 의미는 본 인용에 명시 없음" 을 자인하고, `O2PBEH-C2`~`C4` 는 **authentication 단계 전용**이라 이 경로를 덮지 못한다(D4 Open Risk). 코드가 안 정해지면 [[raw/branch-notes/feature-keycloak-nginx-auth-request-integration]] `D2`(subrequest 2xx/401/403 contract)의 `error_page` 분기도 못 닫는다 — 그쪽 계약과 맞물린 미결.
|
|
- **discovery 기동 순서**: Keycloak 이 아직 ready 가 아닌 시점에 oauth2-proxy 가 discovery 를 호출하면 기동에 실패할 수 있음 — **공식 문서로 미확인**(§Claims To Verify). 확인될 경우 대응 owner 는 [[raw/branch-notes/feature-keycloak-docker-compose-stack]] `D3`(healthcheck 게이팅)이며, **`--skip-oidc-discovery`(D9)는 이 문제의 해법이 아니다** — D9 는 issuer *도달 불가*(폐쇄망) 축이지 *기동 순서* 축이 아님.
|
|
|
|
- **다른 계약 의존** (대상 브랜치 + 그 Decision ID 병기):
|
|
- [[raw/branch-notes/feature-keycloak-nginx-auth-request-integration]] `D2`(subrequest 2xx/401/403 contract) + `D3`(`auth_request_set`→`proxy_set_header` 헤더 전파) + `D5`(4kb cookie split 대응) — 본 branch 의 `/oauth2/auth` 엔드포인트(`O2PE-C5`: 202/401 만 반환, nginx `auth_request` 용)와 D5a 의 4kb 압력이 이 계약을 통해 실현된다. 그쪽 계약이 바뀌면 본 branch D5a/D5c 의 cookie 전제가 영향받음.
|
|
- [[raw/branch-notes/feature-keycloak-header-spoofing-defense]] `D1`(백엔드 ingress-뒤 격리) — 본 branch 는 인증 결과를 헤더로 전달하는 지점까지만 다루고, 그 헤더의 위조 방어는 이 계약이 owner.
|
|
- [[raw/branch-notes/feature-keycloak-docker-compose-stack]] `D3`(healthcheck 로 의존성 강제 — `depends_on: condition: service_healthy` + Keycloak `/health/ready`) — **기동 순서 게이팅의 owner 는 이 계약이다.** 그쪽 D3 의 선택 조건이 문자 그대로 "app 이 startup 시 keycloak JWKS/issuer discovery 에 의존할 때 이 결정" 이라 본 branch D9(discovery)와 정확히 맞물린다. 본 branch 는 *discovery 측 조건*(끌지 말지)만 소유하고 *게이팅 메커니즘*은 이 계약을 참조만 한다 — 재진술 금지(`rules/consistency-contract` Single-Owner).
|
|
- [[raw/branch-notes/feature-keycloak-https-termination-caddy-nginx]] `D1`/`D2`(Cloudflare Tunnel / Caddy edge TLS 종단) + `D5`(TLS 1.2+ / HSTS 강제) — D5c 의 `--cookie-secure=true` 전제(HTTPS 종단 존재)가 이 계약에 의존. 종단이 없으면 D5c 의 기본값 유지가 로컬 시연에서 로그인 루프를 만든다.
|
|
- [[raw/branch-notes/feature-keycloak-pkce-flow-stages]] `D1`(PKCE method = `S256` 만 정리 대상) — `--code-challenge-method=S256`(`O2PK-C6`)의 PKCE 단계 분해는 그쪽이 owner. 본 branch 는 플래그 존재만 인용.
|
|
- [[raw/branch-notes/feature-keycloak-refresh-rotation-and-logout]] D4(명시적 revoke 와 logout 분리) — 본 branch D6 은 *logout* 만 소유하고 executable revoke/logout 시나리오는 그쪽 경계. rotation 정책은 [[raw/branch-notes/feature-keycloak-refresh-token-rotation]] D1이 소유하며, `--cookie-refresh`(`O2PCOOKIE-C5`)와의 상호작용은 본 branch 에서 재정의하지 않는다.
|
|
|
|
## Audit & Findings
|
|
|
|
> `/branch-spec` 자동조사(2026-07-17) 중 **기존 노트 본문과 공식 문서가 어긋난 지점**. CLAUDE.md §2 drift-surface 원칙에 따라 사용자 작성 본문을 자동 rewrite 하지 않고 정합 권고만 남긴다.
|
|
|
|
| ID | 내용 | 근거 | 권고 |
|
|
|---|---|---|---|
|
|
| `NAMING_DRIFT` | **해소(2026-07-18)** — `--skip-jwt-bearer-tokens`/`--extra-jwt-issuers` 경로를 **"JWT bearer 검증 경로"**로 통일했다. 공식 문서가 RFC 7662 introspection endpoint 호출을 서술하지 않으므로 introspection과 동일시하지 않는다. 다만 `.well-known/jwks.json` 참조는 로컬 JWKS 검증을 시사할 뿐 메커니즘을 직접 증명하지 않는다. | `raw/official-docs/oauth2-proxy-behaviour-cookie-vs-bearer-official.md#O2PBEH-C1` + 같은 문서 §Usage Boundaries | 현재 용어를 유지하고, 로컬 JWKS 검증인지 여부만 §Claims To Verify에서 실측한다. |
|
|
| `MECHANISM_DRIFT` | 자동조사 초기 가설(및 일부 2차 자료)은 로그아웃이 `--backend-logout-url` 플래그로 동작한다고 전제했으나, oauth2-proxy 공식 endpoints 문서에 **`backend.logout` 문자열이 0건**(agent grep 확인). 실제 메커니즘은 `rd` query parameter(또는 `X-Auth-Request-Redirect` 헤더) + `{id_token}` placeholder 치환이며, `rd` 대상이 `end_session_endpoint` 여야 한다는 것은 문서의 **권고(convention)** 이지 강제 검증이 아니다. | `raw/official-docs/oauth2-proxy-endpoints-signout-official.md#O2PE-C2`, `#O2PE-C3` | D6 과 §구현 가이드 2 는 이미 정정된 메커니즘으로 작성됨. 외부 블로그가 `--backend-logout-url` 을 언급하면 버전/오정보 의심. |
|
|
| `BACKCHANNEL_UNVERIFIED` | oauth2-proxy 가 **back-channel logout 수신자**(Keycloak 이 Logout Token 을 POST 하는 대상)로 동작하는지 공식 endpoints 문서에서 확인 안 됨 — `backchannel`/`logout token` 문자열 0건(agent grep). Keycloak 측에는 client `Backchannel logout URL` 설정이 존재(`KC-LOGOUT-C7`)하므로 **Keycloak 은 보낼 수 있으나 oauth2-proxy 가 받을 수 있는지가 미확인**. | `raw/official-docs/oauth2-proxy-endpoints-signout-official.md` §Usage Boundaries + `keycloak-oidc-logout-endpoint-official.md#KC-LOGOUT-C7` | back-channel logout 은 본 branch 에서 **채택하지 않음**(D6 은 RP-Initiated 방식). 다중 client SSO 요구가 생기면 재검토 — 그때 oauth2-proxy 수신 지원 여부부터 공식 확인. 커뮤니티 이슈(#1224)는 미지원을 시사하나 **이슈 트래커는 공식 근거 아님**. |
|
|
| `WHITELIST_CVE_HISTORY` | `--whitelist-domain` 의 suffix 매칭에 과거 취약점 이력이 **있다고 전해짐 — 미검증(raw 미보존)**. WebSearch 로 식별자 존재만 확인했을 뿐 **GHSA/CVE 원문을 대조하지 않았다**: CVE-2021-21291(`.example.com` 등록 시 `badexample.com` 도 매칭됐다는 suffix 매칭 결함으로 *전해짐*), GHSA-j7px-6hwj-hpjg / GHSA-5m6c-jp6f-2vcv / GHSA-qqxw-m5fj-f7gv (open-redirect 우회로 *전해짐*). **위 ID·메커니즘은 인용이 아니라 후속 확인 대상이다.** | WebSearch 로 식별자 존재만 확인 — **raw 미보존, verbatim 미확보, 원문 미대조** | D7 을 "이 옵션을 켜면 open redirect 가 닫힌다"로 단정 금지. 버전 currency(수정 릴리스 이후 고정)가 defense-in-depth 로 필요. 정식 인용하려면 GHSA 페이지를 별도 `wiki-source-summarizer` 로 보존해야 함 — **본 라운드 미수행**. |
|
|
| `SESSION_STORAGE_4KB_ABSENT` | session storage 공식 페이지에 `4k`/`4096`/`split` 문자열이 **0건** — 4kb cookie split 함정의 근거는 이 페이지가 아니라 **nginx 통합 페이지**([[raw/official-docs/oauth2-proxy-nginx-integration-official]], "Nginx normally only copies the first `Set-Cookie` header ... if your cookies are larger than 4kb, you will need to extract additional cookies manually")에 있다. | `raw/official-docs/oauth2-proxy-session-storage-official.md` §Usage Boundaries | D5a 의 Open Risk 에 반영 완료. 4kb 대응의 owner 는 [[raw/branch-notes/feature-keycloak-nginx-auth-request-integration]] D5. |
|
|
|
|
## 검증해야 할 주장
|
|
|
|
> 공식 vendor docs 가 옵션의 존재와 형식을 증명해도 내 학습 시연에서의 정확한 동작은 별개. 다음은 P3A 또는 학습 시연 단계에서 실측해야 할 주장.
|
|
|
|
| Claim | Why uncertain | How to verify | Status |
|
|
|---|---|---|---|
|
|
| `provider=keycloak-oidc` 와 `provider=oidc` (generic) 의 실질 차이는 group/role claim native 추출 여부 | `O2PK-C5` 는 `groups` client scope mapper 가 필요하다고 명시하지만, generic `oidc` provider 가 동일 mapping 으로 동작하는지의 비교 vendor doc 미확보 | 두 provider 로 동일 Keycloak realm 에 연결한 oauth2-proxy 컨테이너 2개 띄우고 `--allowed-group=/dev` 동작 비교 | `needs-confirmation` |
|
|
| OIDC discovery (`.well-known/openid-configuration`) 호출 시점과 cache TTL | `O2PCOOKIE-C8`/`C9` (D9) 로 discovery **우회 방법**(`--skip-oidc-discovery` + 수동 endpoint 3종)은 확보했으나, discovery 를 *켰을 때* 언제 호출되는지(기동 1회 vs 주기적)와 JWKS cache TTL 은 공식 prose 문서에 서술 없음. 관련 플래그(`--oidc-jwks-cache-duration` 류)도 overview 페이지에서 미발견 → 소스코드(`providers/oidc.go`) 확인이 필요할 수 있음 | oauth2-proxy 시작 후 wireshark/tcpdump 로 discovery endpoint 호출 빈도 측정 | `needs-confirmation` |
|
|
| **oauth2-proxy 기동이 Keycloak ready 에 의존하는지** (docker-compose 기동 순서 함정) | discovery 가 기동 시 issuer 에 도달해야 한다면, Keycloak 이 늦게 뜰 때 oauth2-proxy 가 죽는다. 공식 문서에 기동 순서 요구사항 서술 없음 — 커뮤니티 이슈에만 신호 존재(공식 근거 아님) | Keycloak 을 의도적으로 늦게 기동시킨 뒤 oauth2-proxy 컨테이너의 exit code / 재시도 로그 확인. 실패하면 게이팅으로 대응 — **메커니즘은 [[raw/branch-notes/feature-keycloak-docker-compose-stack]] `D3` 가 owner**(본 노트 재진술 금지) | `needs-confirmation` |
|
|
| `--skip-jwt-bearer-tokens` 경로가 **로컬 JWKS 서명 검증**인지, 별도 network 검증을 수행하는지 (§Audit `NAMING_DRIFT`) | behaviour 페이지는 "opportunistically attempt to validate"(`O2PBEH-C1`) 로만 서술하고 메커니즘을 명시하지 않는다. overview 페이지의 `--extra-jwt-issuers` 설명이 `.well-known/jwks.json` 을 참조해 로컬 검증을 시사하지만 verbatim 확정은 아니다. 따라서 현재 명칭은 중립적인 "JWT bearer 검증 경로"로 한정한다. | Bearer JWT 요청 중 Keycloak `/protocol/openid-connect/token/introspect` 접근 로그와 JWKS 조회를 함께 관찰한다. Keycloak을 내린 상태에서 JWKS 캐시만으로 검증이 통과하는지도 확인한다. | `needs-confirmation` |
|
|
| cookie session 모드 vs Redis session store 모드의 성능/운영 차이 | `O2PSESS-C1`~`C6` (D5a/D5b) 로 두 모드의 **메커니즘**(stateless cookie / ticket+SETEX)과 플래그는 확보. 그러나 P1A(Keycloak native user store, Google federation 없음) 에서 세션이 실제로 4kb 를 넘는지, Redis round-trip 이 latency 에 얼마나 기여하는지는 수치 미확보 — 4kb 초과 사례는 Azure federation 사례라 일반화 불가 | 로그인 후 브라우저 devtools 로 `_oauth2_proxy` cookie 실제 바이트 측정(4kb 대비) → 단일 oauth2-proxy 에 Redis backend 연결 후 cookie 크기 / login latency 비교 | `planned` |
|
|
| RP-Initiated Logout 호출 시 Keycloak 세션이 실제로 종료되는지 | D6 은 `O2PE-C1`~`C4` + `KC-LOGOUT-C1`~`C6` 으로 **메커니즘 근거는 확보**(UNSUPPORTED 해소). 다만 공식 문서는 옵션·파라미터의 존재를 증명할 뿐 내 구성에서 세션이 실제로 끊기는지는 증명하지 않음 | logout 후 Keycloak admin console 의 active session 조회 + cookie 재제출 시 재로그인 강제 여부 확인 | `planned` |
|
|
| **인가 거부 시 실제 응답 코드/본문** (401 vs 403 vs 로그인 루프) | D4 Open Risk 가 자인 — `O2PK-C3` 은 인가 실패 코드의 의미를 명시 안 하고, `O2PBEH-C2`~`C4` 는 authentication 단계 전용. [[raw/branch-notes/feature-keycloak-nginx-auth-request-integration]] D2의 `error_page` 분기가 이 값에 의존 | 허용 role 이 **없는** 사용자로 로그인 후 보호 경로 요청 → 응답 코드/본문 확인. `/oauth2/auth` subrequest 응답도 함께 확인(202/401 만 반환하는지 — `O2PE-C5` 와 대조) | `needs-confirmation` |
|
|
| **인증이 강제된(=`--skip-auth-route` 아닌) 라우트에서 Bearer-only 요청이 session cookie 없이 통과하는지** | `O2PBEH-C1` 은 **스킵된 라우트** 전용 인용이고, 그 Does-not-prove 가 "강제 라우트에서 cookie/JWT 를 어떤 순서·우선순위로 시도하는지는 범위 밖" 이라 자인. §구현 가이드 3 의 통과 2행은 실패 경로의 대우에서 도출한 **추론**이지 공식 보장 아님 | 일반(비스킵) 경로에 `Authorization: Bearer <valid JWT>` 만 담아 요청 → cookie 없이 202/200 이 오는지 확인. 또는 `configuration/overview` 페이지를 별도 raw 로 보존해 verbatim 확정 | `needs-confirmation` |
|
|
| **Keycloak 26.x 의 `end_session_endpoint` 실측값이 `/realms/{realm}/protocol/openid-connect/logout` 인지** | `KC-LOGOUT-C1` 의 evidence quote 전문은 "The logout endpoint logs out the authenticated user." 뿐 — **경로 문자열은 quote 에 없고** claim 서술문에만 있다. 하드코딩하면 Keycloak context-path 변경(D2 의 17+ 이슈)에 취약 | `curl https://<keycloak host>/realms/<realm>/.well-known/openid-configuration \| jq -r .end_session_endpoint` 로 실제 노출값 확인 → D6/§구현 가이드 2 의 3단계 경로와 대조 | `needs-confirmation` |
|
|
| **`rd` 도메인이 `--whitelist-domain` 미등록일 때 logout 이 조용히 실패하는지** | `O2PE-C4` 가 "리다이렉트가 무시된다" 고 명시하나, 무시 시 사용자에게 보이는 최종 화면(에러 페이지 vs 기본 sign-out 페이지)은 서술 없음 — D6 의 가장 현실적인 실패 모드라 실측 가치 높음 | Keycloak 도메인을 `--whitelist-domain` 에서 **뺀 상태**로 logout 시도 → 최종 랜딩 화면 + Keycloak 세션 잔존 여부 확인 | `planned` |
|
|
| `--whitelist-domain` 옵션이 open redirect 공격을 실제로 차단하는지 | `O2PCOOKIE-C7` (`raw/official-docs/oauth2-proxy-cookie-redirect-flags-official.md`) 로 옵션의 존재·문법(서브도메인 wildcard/포트 지정)은 확인됐으나, 미설정 시 기본 동작(전체 차단 여부)과 실제 공격 시나리오에서의 차단 여부는 공식 문서에 없음 | 공격 시나리오 (`rd=https://evil.example.com`) 로 redirect 시도 후 oauth2-proxy 응답 확인 | `planned` |
|
|
|
|
## 마주친 문제
|
|
|
|
- 아직 없음(문서 단계).
|
|
|
|
## 묶음
|
|
|
|
<!-- GENERATED: sources:start -->
|
|
- [[raw/official-docs/keycloak-oidc-logout-endpoint-official]]
|
|
- [[raw/official-docs/oauth2-proxy-behaviour-cookie-vs-bearer-official]]
|
|
- [[raw/official-docs/oauth2-proxy-cookie-redirect-flags-official]]
|
|
- [[raw/official-docs/oauth2-proxy-endpoints-official]]
|
|
- [[raw/official-docs/oauth2-proxy-endpoints-signout-official]]
|
|
- [[raw/official-docs/oauth2-proxy-keycloak-oidc-provider-official]]
|
|
- [[raw/official-docs/oauth2-proxy-nginx-integration-official]]
|
|
- [[raw/official-docs/oauth2-proxy-overview-config-official]]
|
|
- [[raw/official-docs/oauth2-proxy-session-storage-official]]
|
|
- [[raw/official-docs/security-jwt-rfc-7519-validation]]
|
|
<!-- GENERATED: sources:end -->
|
|
|
|
> 본 sub-sub-branch 는 leaf — 자식 자료 없음. Phase 3 P3A 실 구현 또는 외부 산출물 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.
|
|
|
|
### 근거 자료
|
|
|
|
- [[raw/official-docs/oauth2-proxy-overview-config-official]]
|
|
- [[raw/official-docs/oauth2-proxy-keycloak-oidc-provider-official]]
|
|
- [[raw/official-docs/oauth2-proxy-cookie-redirect-flags-official]] — D5c (cookie 속성값 표준화: samesite/secure/secret/expire/refresh) + `--whitelist-domain` + `--skip-oidc-discovery`
|
|
- [[raw/official-docs/oauth2-proxy-session-storage-official]] — D5a/D5b (session storage 백엔드: cookie vs redis)
|
|
- [[raw/official-docs/oauth2-proxy-endpoints-signout-official]]
|
|
- [[raw/official-docs/oauth2-proxy-behaviour-cookie-vs-bearer-official]]
|
|
- [[raw/official-docs/keycloak-oidc-logout-endpoint-official]] — D6 (RP-Initiated Logout) Keycloak 측 근거: `end_session_endpoint`/`id_token_hint`/`post_logout_redirect_uri`/Backchannel Logout URL
|
|
|
|
### 오류 기록 (이 sub-sub-branch 작업 중 발생)
|
|
|
|
- (없음 — 현재 documented-only 단계)
|
|
|
|
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
|
|
|
- (없음 — Phase 3 실 구현 단계에 누적)
|
|
|
|
## 관련 일일 노트
|
|
|
|
|
|
## 완료 후 정리
|
|
|
|
> 본 sub-sub-branch는 **문서까지만**. wiki 추출은 root branch의 비교 매트릭스 시점에 일괄 처리.
|
|
|
|
- PR 링크:
|
|
- 리뷰 메모:
|
|
- 머지 결과 / 배포 환경: 해당 없음 (문서 단계)
|
|
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
|
|
- `actually-implemented` 항목: 없음
|
|
- `locally-verified` 항목: 없음
|
|
- `prod-verified` 항목: 없음
|
|
- **추출하지 않을 항목** (planned / documented-only / abandoned):
|
|
- 본 sub-sub-branch 전체가 `documented-only` 등급. P1A 부모 sub-branch와 함께 추후 비교 매트릭스 / `wiki/concepts/keycloak-deployment-patterns.md`에 인용되는 형태로만 활용. `wiki/projects/` 직접 승급 없음.
|