---
title: branch / feature-keycloak-nginx-auth-request-integration (P1A — nginx auth_request 통합)
source_type: branch-note
status: raw
id: BR-KEYCLOAK-PATTERNS-OVERVIEW-013
kind: project-work-item
project: keycloak-patterns-overview
work_item: WI-KEYCLOAK-PATTERNS-OVERVIEW-013
inherits: [DEC-KEYCLOAK-PATTERNS-OVERVIEW-EDGE-FORWARDAUTH-001@1]
refines: []
overrides: []
depends_on: [WI-KEYCLOAK-PATTERNS-OVERVIEW-012]
contract_packet: 1
branch: feature-keycloak-nginx-auth-request-integration
parent_branch:
related_projects: [keycloak-patterns]
tags: [branch, keycloak-patterns, p1a, nginx, auth-request, subrequest, cookie-limit]
created: 2026-05-25
target_merge:
status_label: in-progress
contract_packet_sha256: f798f591573547bd411e1edfe92d4c5c999d10c22903ac34e81c02b0f934581f
---
# branch: feature-keycloak-nginx-auth-request-integration (P1A — nginx auth_request 통합)
> Layer: `raw/branch-notes/` — [[raw/project-notes/keycloak-patterns-overview]] Work Item의 직접 자식 branch.
> nginx의 `auth_request` directive와 oauth2-proxy `/oauth2/auth` endpoint contract를 **subrequest 응답 단위**로 분해. oauth2-proxy 자체 설정은 `-1-1`, 네트워크 격리는 `-1-3`에서 별도 다룬다.
> 본 sub-sub-branch는 **문서까지만** (`documented-only`).
> `status_label`: `in-progress`
## 부모 (필수)
[[raw/project-notes/keycloak-patterns-overview]]
## 브랜치 계약 패킷
- **생성 시 프로젝트 개정**: `1`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: nginx auth_request 통합과 4KB cookie split case가 검증된다
### 상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
| `DEC-KEYCLOAK-PATTERNS-OVERVIEW-EDGE-FORWARDAUTH-001@1` | AP4는 oauth2-proxy ForwardAuth를 사용한다 | nginx auth_request 통합과 4KB cookie split case 검증에 적용한다 | [[raw/project-notes/keycloak-patterns-overview]] |
### 브랜치 지역 결정
> 기존 branch-local 결정은 아래 `## Decision Evidence Map`의 D-row가 소유하며 이 packet에서 복제하지 않는다.
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|---|---|---|---|
### 선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|---|---|---|---|
없음.
## 목표
P1A 패턴이 작동하는 **물리적 지점**은 nginx `auth_request` directive가 oauth2-proxy로 subrequest를 보내고, 응답 status(2xx/401)와 응답 헤더(`X-Auth-Request-*`)를 받아 backend로 propagate하는 그 한 줄에 모인다. 본 문서는 이 contract의 각 부품 — directive 위치, subrequest body 처리, `auth_request_set` 변수 추출, named location `@oauth2_signin` redirect, 그리고 **4kb cookie/header 한도 함정** — 을 분리해서 본다.
핵심 질문:
1. `auth_request` directive는 어느 `server` / `location` block에 놓아야 하는가? 모든 backend `location`에 반복해야 하는가, 아니면 상속되는가?
2. oauth2-proxy `/oauth2/auth` endpoint의 **응답 contract**는? 202 / 401 / 403의 의미와 nginx 측 처리.
3. `X-Auth-Request-User` 같은 응답 헤더를 backend `proxy_pass`에 어떻게 전달하는가? (`auth_request_set` + `proxy_set_header`)
4. access_token까지 cookie에 담으면 왜 nginx가 502를 내는가? → `proxy_buffer_size` / `large_client_header_buffers` / 4kb 한도와 multi-part cookie splitter.
- 이슈:
- PR:
## 범위
### 포함 범위
> 본 sub-sub 가 **결정을 소유하는** 범위. 각 항목은 `Decision Evidence Map` 의 D-ID 로 종결.
>
> ⚠️ **산출물의 지위**: 본 branch 의 nginx.conf 명세는 부모 D1 이 K8s+ingress-nginx 분기를 유지하는 한 **학습용 참조 구현이며 배포 대상이 아니다** (부모 D1 은 단일 VM docker-compose 를 Traefik 으로 보낸다) — §엣지·실패·의존 의 "다른 계약 의존" 첫 bullet 참조.
- nginx `auth_request` subrequest 응답 contract (2xx allow / 401 redirect / 403 deny) 의 nginx 측 처리 — D2
- 인증 실패 응답의 route 별 분기 (browser-facing 302 vs API/machine plain 401) — D9
- `auth_request_set` + `proxy_set_header` 2-step 헤더 propagation 메커니즘 — D3
- backend 로 전달할 `X-Auth-Request-*` 헤더 목록 선정 — D4
- 4kb cookie/header 한도 함정 인식 + cookie split 동작 + nginx buffer 튜닝 — D5
- subrequest 의 원 요청 body 차단 (`proxy_pass_request_body off` + `Content-Length ""`) — D6
- oauth2-proxy 자체 endpoint 의 nginx 라우팅 (`location /oauth2/` prefix vs `location = /oauth2/auth` exact 분리) — D7
- access token 을 cookie/헤더로 전달할지의 분기 (학습 단계는 양쪽 다이어그램화) — D8
### 제외 범위
> 의도적으로 제외. 각 항목은 **다른 owner** 가 있거나 본 branch 단계(`documented-only`) 밖이다.
- **oauth2-proxy 자체 구성** (`--provider`, `--cookie-secret`, `--oidc-issuer-url`, OIDC code flow) → 형제 [[raw/branch-notes/feature-keycloak-oauth2-proxy-oidc-flow]]
- **네트워크 격리 / header spoofing 방어** (NetworkPolicy, SG, mTLS, shared-secret 헤더) → 형제 [[raw/branch-notes/feature-keycloak-header-spoofing-defense]]. 본 branch 는 헤더를 *주입* 할 뿐, 그 헤더를 외부 위조로부터 지키는 것은 그쪽 결정 — D3 Open Risk 참조
- **Traefik `forwardAuth` 비교** → 형제 [[raw/branch-notes/feature-keycloak-traefik-forwardauth-alternative]] (D1 이 본 branch 를 nginx 조합으로 한정)
- **K8s ingress-nginx annotation 방식** (`nginx.ingress.kubernetes.io/auth-url`·`auth-signin`) — 2026-07-17 조사에서 공식 대안으로 확인됐으나 본 branch 는 standalone nginx config 를 다룬다. D7 의 선택 조건에 분기만 기록하고 명세는 남기지 않음 (`OUT_OF_BRANCH_SCOPE`)
- **backend RS 의 access token audience validation** → [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] (D8 Open Risk 가 이 의존을 명시)
- **실 구현 / 실측** — 본 sub-sub 는 `documented-only`. 모든 실측 항목은 `Claims To Verify` 로 분리되어 P3A 단계에서 수행
## 근거 (필수, 최소 1개+)
> 부모 sub-branch에서 인용한 외부 자료를 재참조 (추가 조사 없음).
- [[raw/official-docs/nginx-auth-request-module-official]] — ngx_http_auth_request_module (2xx=allow / 401|403=deny contract)
- [[raw/official-docs/oauth2-proxy-nginx-integration-official]] — oauth2-proxy 공식의 nginx 통합 가이드
- [[raw/official-docs/nginx-core-module-location-internal-official]] — D7: `internal` directive 동작(외부 요청 404) + `location` 매칭 우선순위(exact `=` > prefix longest-match) 메커니즘 근거
- [[raw/official-docs/oauth2-proxy-endpoints-official]] — oauth2-proxy 자체 endpoint(`/oauth2/start`·`/oauth2/callback`·`/oauth2/sign_in`·`/oauth2/sign_out`·`/oauth2/userinfo`·`/oauth2/static/*`·`/oauth2/auth`) 별 용도와 호출 주체 근거 (D7)
- [[raw/official-docs/proxy-pass-request-body-nginx-official]] — `proxy_pass_request_body` directive 의 Default(`on`)/Description/공식 예제 (D6 메커니즘 근거)
## TODO
각 항목 옆에 증거 등급 표기.
- [ ] nginx config `location = /oauth2/auth` block 작성 (internal, `proxy_pass http://oauth2-proxy.upstream`, `proxy_pass_request_body off`, `proxy_set_header Content-Length ""`) — 등급: `planned`
- [ ] `auth_request /oauth2/auth;` directive를 protect할 `location /api/` 등 backend block에 추가 — 등급: `planned`
- [ ] subrequest 응답 status별 nginx 처리: 2xx=allow / 401=`error_page 401 = @oauth2_signin` / 403=deny — 등급: `planned`
- [ ] named location `@oauth2_signin` 작성: `return 302 https://$host/oauth2/start?rd=$scheme://$host$request_uri;` — 등급: `planned`
- [ ] 응답 헤더 propagation: `auth_request_set $user $upstream_http_x_auth_request_user;` + `proxy_set_header X-User $user;` — 등급: `planned`
- [ ] 전달할 헤더 목록 정리: `X-Auth-Request-User`, `X-Auth-Request-Email`, `X-Auth-Request-Groups`, (옵션) `X-Auth-Request-Access-Token` — 등급: `planned`
- [ ] **4kb cookie 한도 함정**: access_token cookie 포함 시 nginx 기본 `proxy_buffer_size 4k` / `large_client_header_buffers` 초과 → 502/400 발생. 해결: oauth2-proxy `--cookie-secret` + cookie 분할 (`_oauth2_proxy_0`, `_1`, …) 동작 이해 — 등급: `planned`
- [ ] nginx 측 튜닝 옵션 정리: `proxy_buffer_size 16k; proxy_buffers 4 16k; large_client_header_buffers 4 16k;` — 등급: `planned`
- [ ] `/oauth2/callback`, `/oauth2/start`, `/oauth2/sign_out` 등 oauth2-proxy 자체 endpoint들이 nginx를 통과하도록 `location /oauth2/` block 작성 — 등급: `planned`
- [ ] subrequest의 원래 request body가 oauth2-proxy로 전달되지 않도록 `proxy_pass_request_body off` 강제 + `Content-Length` 빈 값 처리 — 등급: `planned`
## 진행 중 메모
> 작업하며 떠오른 메모.
- nginx `auth_request` core는 별도의 authorization subrequest를 만든다. 다만 실제 `location = /oauth2/auth`가 `proxy_pass`로 oauth2-proxy에 전달될 때는 proxy module의 기본값이 `proxy_pass_request_body on`이므로 원 요청 body가 upstream으로 전달될 수 있다. `/oauth2/auth`는 헤더·쿠키만 검사하므로 실행 설정에서 `proxy_pass_request_body off`와 빈 `Content-Length`를 함께 명시한다.
- `auth_request_set`은 subrequest **응답 헤더**에서 값을 빼와서 nginx 변수에 담는 단계. 이걸 빠뜨리면 backend는 그냥 unauthenticated 요청을 받는다.
- 4kb 한도 함정은 P1A 패턴에서 가장 흔한 502 원인. 면접 질문 후보: "edge ForwardAuth 운영 중 backend가 갑자기 502 내기 시작하면 어디부터 보겠나?"
## 결정 사항 (decisions)
- **2026-05-25**: 본 sub-sub는 **nginx + oauth2-proxy 조합**에 한정. Traefik의 `forwardAuth` middleware는 `-1-4`에서 별도 비교.
- **2026-05-25 (decision candidate)**: access_token을 cookie에 담을지(backend에서 토큰 필요) 헤더 noise로만 식별자만 넘길지는 backend 요구에 따라 갈림. 학습 단계에서는 둘 다 다이어그램화.
- **2026-07-17**: 인증 실패 응답을 **browser-facing route(302 redirect) 와 API/machine route(plain 401 pass-through) 로 분리** (→ D9). / 이유: 기존 D2 의 Open Risk("XHR 에 302 는 부적절")를 닫는 답을 공식 문서에서 확보. / 검토한 대안: 전 route 일괄 302(= 기존 D2 단독) — API client 가 로그인 HTML 을 받게 되어 기각. / 근거: `[[raw/official-docs/oauth2-proxy-nginx-integration-official]]#O2PN-C9` (§Browser vs API Routes).
- **2026-07-17**: D6(subrequest body 차단)·D7(oauth2-proxy endpoint 라우팅)의 `UNSUPPORTED_DECISION` 라벨 **해소**. / 근거: `#O2PN-C7`·`#O2PN-C8`(공식 nginx.conf 4-block 예제 — 기존 raw 가 산문 섹션만 인용하고 예제 블록 자체를 놓치고 있었음), `#NGAR-C8`(nginx.org 벤더-중립 Example Configuration — oauth2-proxy 와 독립된 2번째 공식 출처), `[[raw/official-docs/proxy-pass-request-body-nginx-official]]#NGXPM-C1`(default `on` 시맨틱), `[[raw/official-docs/oauth2-proxy-endpoints-official]]#O2EP-C1`~`C8`(endpoint 별 용도), `[[raw/official-docs/nginx-core-module-location-internal-official]]#NGCM-C1`·`#NGCM-C2`(`internal` 동작 + exact>prefix 매칭). / **단, `/oauth2/auth` 에 `internal;` 을 붙이는 하드닝은 공식 예제에 없으므로 `UNSUPPORTED_IMPL_DECISION` 으로 잔존** (§구현 가이드 §1).
## 결정-근거 매핑
> 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시. TODO 의 nginx config 패턴 (subrequest contract / `auth_request_set` / 4kb cookie 한도 / 401 redirect) 은 모두 공식 nginx 모듈 + oauth2-proxy nginx 통합 페이지에서 직접 뒷받침됨.
>
> **2026-07-17 갱신**: D6·D7 의 `UNSUPPORTED_DECISION` 라벨 해소(공식 nginx.conf 예제 블록 + endpoint 목록 + nginx core module 근거 확보), D9 신설(browser vs API route 분리). `선택 조건` 열 추가. **남은 `UNSUPPORTED_DECISION` 은 D1 하나뿐이며, 이는 학습 범위 분할이라는 조직적 결정이라 vendor doc 인용 대상이 아니다.** 근거 없는 *구현* detail 은 §구현 가이드의 `UNSUPPORTED_IMPL_DECISION` 4건(`internal;` 부착 / upstream 주소 / 버퍼 수치 / browser-API 판별 기준 + `@oauth2_signin` 목적지)으로 분리했다.
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|---|
| D1 | 본 sub-sub 는 nginx + oauth2-proxy 조합으로 한정 (Traefik forwardAuth 는 sibling sub-sub `-1-4` 에서 별도 비교) | **부모 D1 의 스택 선택 기준을 상속**: K8s + ingress-nginx 환경 → oauth2-proxy(본 노트) / 단일 VM docker-compose → Traefik forwardAuth(sibling `-1-4`). 즉 본 노트의 config 는 *전자* 를 가정한다 — [[raw/branch-notes/feature-keycloak-edge-forwardauth-no-google]] `D1` 참조. ⚠️ 본 branch 가 조사한 공식 예제는 **standalone nginx** 기준이라 부모 D1 의 분기와 긴장 관계 — §엣지·실패·의존 참조 | UNSUPPORTED_DECISION (학습 분할 결정 — vendor doc 인용 불요) | N/A (organizational decision) | nginx config 만 보면 Traefik 와의 비교 매트릭스 작성 시 누락된 옵션 (`authResponseHeaders` 등) 인식 지연 |
| D2 | `/oauth2/auth` subrequest 응답 contract 채택: 2xx → allow, 401 → `error_page 401 = @oauth2_signin` redirect, 403 → deny | **route 유형이 분기 기준**: browser-facing route(사람이 브라우저로 여는 페이지) → 본 결정대로 401 을 `@oauth2_signin` 302 redirect 로 변환. **API/machine route → 302 로 변환하지 않고 plain 401 pass-through (D9)**. 403 은 양쪽 공통 deny(재로그인해도 해소 안 되는 인가 실패이므로 redirect 무의미). 2xx 변종(200/202/204)은 모두 allow 로 동일 취급(`NGAR-C2` does-not-prove 상 변종별 차이 없음) | `raw/official-docs/nginx-auth-request-module-official.md#NGAR-C2` (nginx 응답 코드 contract), `raw/official-docs/oauth2-proxy-nginx-integration-official.md#O2PN-C2` (`/oauth2/auth` 202/401 spec), `raw/official-docs/oauth2-proxy-nginx-integration-official.md#O2PN-C5` (401 → 302 redirect 패턴) | `official-vendor-doc` (nginx F5 공식 + oauth2-proxy 공식 양측 verbatim 확인) | XHR/API 요청에 302 redirect 반환이 적절하지 않음 (`O2PN-C5` does-not-prove) — API client 별도 처리 필요 |
| D3 | 응답 헤더 propagation 메커니즘 채택: `auth_request_set $user $upstream_http_x_auth_request_user` + `proxy_set_header X-User $user` 2-step | **backend 가 사용자 신원을 필요로 하는가** 가 분기 기준: 필요 → 2-step 전개(+ oauth2-proxy 를 `--set-xauthrequest` 로 실행해야 응답 헤더가 나옴, `O2PN-C3`). 불필요(단순 인증 게이팅만) → `auth_request` 만 두고 `auth_request_set`/`proxy_set_header` 생략 — 이 경우 backend 는 "누구인지" 모른 채 "인증됨" 만 보장받음. 대안(oauth2-proxy 를 reverse-proxy 모드로 두고 `--pass-user-headers` 사용, `OAUTH2PROXY-C4`)은 edge ForwardAuth 패턴이 아니므로 본 branch 범위 밖 | `raw/official-docs/nginx-auth-request-module-official.md#NGAR-C5` (`auth_request_set` + `$upstream_http_*` 일반 메커니즘), `raw/official-docs/oauth2-proxy-nginx-integration-official.md#O2PN-C3` (X-User/X-Email 헤더 매핑) | `official-vendor-doc` | backend 가 `X-User` 헤더를 신뢰해도 안전한 보안 전제는 미증명 (`O2PN-C3` does-not-prove) — header spoofing 방어 (sub-sub `-1-3`) 필요 |
| D4 | 전달 헤더 목록: `X-Auth-Request-User`, `X-Auth-Request-Email`, `X-Auth-Request-Groups`, (옵션) `X-Auth-Request-Access-Token` | **최소 전달 원칙 — backend 가 실제로 쓰는 claim 만**: 식별자만 필요 → `User`(+`Email`) 만. role/group 기반 authz → `Groups` 추가. backend 가 토큰 자체를 필요 → `Access-Token` 추가(단 이는 D8 이 소유하는 분기이며 `--pass-access-token` 선행 필요). 헤더 이름 규약은 본 branch 가 아니라 **부모 D2 가 owner** (nginx 계열 `X-Auth-Request-*` 우선) — 부모가 규약을 바꾸면 본 행도 따라감 | `raw/official-docs/oauth2-proxy-overview-config-official.md#OAUTH2PROXY-C3` (4종 X-Auth-Request-* 응답 헤더), `raw/official-docs/oauth2-proxy-overview-config-official.md#OAUTH2PROXY-C2` (`--pass-access-token` → `X-Forwarded-Access-Token` / `X-Auth-Request-Access-Token`), `raw/official-docs/oauth2-proxy-nginx-integration-official.md#O2PN-C4` (`--pass-access-token` + access token forwarding 패턴) | `official-vendor-doc` | `X-Auth-Request-Preferred-Username` 도 spec 에 존재 (`OAUTH2PROXY-C3`) — 학습 시 추가 정리 필요. 또한 access token forwarding 이 RS audience validation 을 대체 안 함 (`O2PN-C4` does-not-prove) |
| D5 | 4kb cookie 한도 함정 인식 + cookie split 대응 (`_oauth2_proxy_0`, `_1`, …) + nginx buffer 튜닝 (`proxy_buffer_size 16k; proxy_buffers 4 16k; large_client_header_buffers 4 16k`) | **세션 cookie 가 4KB 를 넘는가** 가 분기 기준이며, 이는 **D8 의 선택에 종속**: D8-(a) access token 을 세션에 포함 → 4KB 초과 가능성 높음(특히 Keycloak 의 realm/client role claim 이 많을 때) → 버퍼 튜닝 + split cookie 대응 **필수**. D8-(b) 식별자만 전달 → 4KB 여유 → 기본 버퍼로 충분하나, claim 이 늘면 재검토. **한도 자체(4KB)만 공식이고 튜닝 수치(16k)는 사용자 임의** — §구현 가이드 §4 의 `UNSUPPORTED_IMPL_DECISION` 참조 | `raw/official-docs/oauth2-proxy-nginx-integration-official.md#O2PN-C6` (4KB 한도 + cookie split + nginx 의 first Set-Cookie 만 복사 한계) | `official-vendor-doc` (oauth2-proxy 공식이 4KB 한계와 split 동작 명시) | nginx 가 multi-part Set-Cookie 를 모두 복사하도록 하는 정확한 lua/scripting 방식은 인용 범위 밖 (`O2PN-C6` does-not-prove) — 실 적용 시 lua 스크립트 작성 필요 |
| D6 | `proxy_pass_request_body off` + `Content-Length ""` 로 subrequest body 전달 차단 | **auth_request 목적지가 body 를 읽는가** 가 분기 기준: `/oauth2/auth` 처럼 헤더/쿠키만 보고 202/401 을 내는 인증 체크 endpoint(`O2PN-C2`) → 본 결정대로 `off`. 목적지가 body 를 실제로 검사해야 하는 커스텀 인증 서비스(예: request-signing 검증)라면 → `off` 하면 인증이 깨지므로 default(`on`) 유지 — 단 그 구성은 본 branch 범위 밖(oauth2-proxy 전용 조사). 제3 옵션 `proxy_request_buffering` 은 *전달 여부* 가 아니라 *버퍼링 방식* 을 제어하므로 본 분기와 무관(2026-07-17 조사에서 탐색 후 기각) | `raw/official-docs/nginx-auth-request-module-official.md#NGAR-C8` (nginx 공식 "Example Configuration" 이 `/auth` subrequest 목적지 location 에서 이 두 directive 를 예제로 명시), `raw/official-docs/proxy-pass-request-body-nginx-official.md#NGXPM-C1` (`proxy_pass_request_body` 의 Default 는 `on` — 명시적 `off` 없이는 원본 body 가 그대로 proxied server 로 전달된다는 directive 자체의 메커니즘), `raw/official-docs/proxy-pass-request-body-nginx-official.md#NGXPM-C2` (`ngx_http_proxy_module` 자체 공식 문서도 `off` + `Content-Length ""` 조합을 예제로 제시 — X-Accel-Redirect 맥락이지만 `NGAR-C8` 과 독립된 2번째 공식 출처) | `official-vendor-doc` (nginx 모듈 설계자 자신의 vendor-neutral 예제 2건 — `ngx_http_auth_request_module`(`NGAR-C8`) + `ngx_http_proxy_module`(`NGXPM-C1`/`C2`) 양쪽에서 독립 확인됨. **directive 메커니즘(default on/off 시맨틱) 자체는 이제 2중 확인**) | `NGXPM-C1`/`C2` 는 auth_request subrequest 가 반드시 `proxy_pass` 기반 location 으로 라우팅된다는 것을 증명하지 않으며, 이 조합이 auth_request 서브리퀘스트에 대해 "공식적으로 필수"임을 증명하지도 않는다 (그 전용 권고는 `NGAR-C8` 담당, `NGXPM-C2` 는 X-Accel-Redirect 예제일 뿐). 설정을 **없을 때 정확히 어떤 에러가 발생하는지도 여전히 증명하지 않음** (원문들은 권장/기본값 설명만 제시, 실패 모드 기술 없음) — subrequest 가 body 를 가지고 가면 POST endpoint 가 의도치 않게 트리거되거나 oauth2-proxy CPU 증가 가능하다는 추론은 여전히 `needs-confirmation` |
| D7 | `/oauth2/callback`, `/oauth2/start`, `/oauth2/sign_out` 등 oauth2-proxy 자체 endpoint 들이 nginx 를 통과하도록 `location /oauth2/` block 작성 (동시에 `/oauth2/auth` 만 별도 `location = /oauth2/auth` exact block 으로 분리 가능한지는 nginx 매칭 메커니즘에 달림) | **배포 형태가 분기 기준**: 단일 도메인 + standalone nginx → 본 결정(`location /oauth2/` prefix + `location = /oauth2/auth` exact, 공식 1차 예제 `O2PN-C7`). K8s + ingress-nginx → nginx.conf 대신 annotation(`auth-url`/`auth-signin`) 방식 — 본 branch 범위 밖(`OUT_OF_BRANCH_SCOPE`, §범위 참조). 다중 앱 도메인 SSO → 도메인마다 prefix block 반복(공식 예제 주석의 `X-Auth-Request-Redirect $scheme://$host$request_uri` 가 이 변형을 시사). **oauth2-proxy 를 별도 subdomain 에 중앙 배치하는 안은 2026-07-17 조사에서 공식 근거 부족으로 기각** (cross-domain cookie 설계가 전부 미증명 추론 영역) | `raw/official-docs/oauth2-proxy-nginx-integration-official.md#O2PN-C7` (**1차 근거** — 공식 nginx.conf 예제가 `location /oauth2/` prefix block 과 `location = /oauth2/auth` exact block 을 실제로 2-block 으로 제시), `raw/official-docs/oauth2-proxy-endpoints-official.md#O2EP-C1` (`/oauth2/start` = OAuth cycle 시작 redirect URL), `#O2EP-C2` (`/oauth2/callback` = IdP 가 설정하는 callback url), `#O2EP-C3` (`/oauth2/sign_in`), `#O2EP-C4` (`/oauth2/sign_out`), `#O2EP-C5` (`/oauth2/userinfo`), `#O2EP-C6` (`/oauth2/static/*`), `#O2EP-C7` (`/oauth2/auth` 만 별도로 "for use with the Nginx auth_request directive" 라벨 — 나머지 6개 endpoint 는 이 라벨이 없음), `#O2EP-C8` (전체 목록이 `/oauth2` prefix 를 공유하며 `--proxy-prefix` 로 변경 가능), `raw/official-docs/nginx-core-module-location-internal-official.md#NGCM-C1` (`internal;` 직접 정의 + `auth_request` 서브리퀘스트가 공식 "internal request" 트리거 목록에 포함됨을 확인), `raw/official-docs/nginx-core-module-location-internal-official.md#NGCM-C2` (`location` 매칭에서 `=` exact match 가 발견되면 즉시 검색 종료 — prefix `location /oauth2/` 과 exact `location = /oauth2/auth` 가 같은 `/oauth2` 네임스페이스 아래 충돌 없이 공존 가능한 nginx 엔진 메커니즘) | `official-vendor-doc` (oauth2-proxy 공식 nginx.conf 예제 + endpoint 목록 + nginx F5 공식 core module 3중 verbatim 확인). **prefix/exact 2-block 분리 자체는 `O2PN-C7` 공식 예제로 직접 근거 있음** — 공식이 실제로 그렇게 config 를 제시한다. 미증명인 것은 오직 **`/oauth2/auth` 에 `internal;` 을 붙이는 하드닝 처방** 뿐이며(공식 예제엔 `internal` 문자열 자체가 없음), 이는 `/oauth2/auth` 만 auth_request 전용 라벨이 있다는 **비대칭**(`O2EP-C7`) + exact>prefix 매칭 규칙(`NGCM-C2`) + auth_request 가 internal 트리거 목록에 포함(`NGCM-C1`)을 결합한 사용자 추론이다 | `/oauth2/auth` 에 `internal;` 을 붙여도 안전한지는 공식 문서 미진술 (`O2EP-C7` does-not-prove) — `NGCM-C1`+`NGCM-C2` 는 그런 구성이 nginx 엔진 차원에서 **기술적으로 가능**하다는 메커니즘만 증명하며, oauth2-proxy 공식이 그렇게 **권고**한다는 것은 증명하지 않는다 (oauth2-proxy 공식 nginx 통합 예제 자체는 `internal;` 미사용 — negative finding, `NGCM-C2` Usage Boundaries 참조). nginx 측 `location = /oauth2/auth { internal; ... }` 격리와 `location /oauth2/ { ... }` 공개 block 을 분리하는 실제 구성은 여전히 사용자 추론 영역이며, `oauth2-proxy-nginx-integration-official.md#O2PN-C5` (401→302 redirect) 와 결합해도 나머지 5개 endpoint(`start`/`sign_in`/`userinfo`/`static`) 의 명시적 라우팅 예제는 없음 |
| D8 | access token 을 cookie 에 담을지 헤더로만 식별자 넘길지는 backend 요구에 따라 갈림 (학습 단계는 둘 다 다이어그램화) | **backend 가 토큰 자체를 필요로 하는가** 가 분기 기준: (a) backend 가 RS 로서 토큰을 검증하거나 그 토큰으로 다운스트림 API 를 호출해야 함 → `--pass-access-token` + `auth_request_set $token $upstream_http_x_auth_request_access_token`. **대가: 세션이 4KB 를 넘겨 D5 의 버퍼 튜닝·split cookie 대응이 필수가 됨.** (b) backend 가 "누구인지" 만 필요 → 식별자 헤더만 전달(D4), 토큰 미전달 → 세션이 작아 D5 부담 없음. 학습 단계에선 **결정을 확정하지 않고 양쪽을 다이어그램화** (실 채택은 P3A) | `raw/official-docs/oauth2-proxy-overview-config-official.md#OAUTH2PROXY-C2`, `raw/official-docs/oauth2-proxy-nginx-integration-official.md#O2PN-C4` (둘 다 `--pass-access-token` 옵션의 존재만 증명) | `official-vendor-doc` (옵션 존재) + 학습용 분기 결정 | backend RS 에서 access token audience validation 을 별도 수행해야 함 (`O2PN-C4` does-not-prove) — RS 측 검증 누락 시 P1A 무력화. (a) 선택 시 D5 의 4KB 함정이 *가능성* 이 아니라 *확정 과제* 로 전환됨 |
| D9 | 인증 실패 응답을 **route 유형별로 분리**: browser-facing route → 401 을 `@oauth2_signin` 302 redirect 로 변환(D2), API/machine route → `error_page 401 =401` 로 **plain 401 을 그대로 pass-through** (redirect 금지) | **요청 주체가 사람의 브라우저인가 기계인가**: 사람이 브라우저로 여는 페이지 route → 302(로그인 화면으로 유도해야 UX 성립). SPA 의 XHR/fetch·CLI·서버간 호출 등 machine client route(예: `location /api/`) → plain 401/403(redirect 를 따라가면 로그인 HTML 을 JSON 대신 받게 되거나 CORS 로 실패). **한 서버에 두 유형이 공존하면 location 단위로 분리** — 본 결정이 D2 의 "XHR 에 302 는 부적절" Open Risk 를 닫는 답 | `raw/official-docs/oauth2-proxy-nginx-integration-official.md#O2PN-C9` (§Browser vs API Routes — "Redirecting authentication failures (302 to `/oauth2/sign_in`) should **only be used for browser-facing routes**. API or machine clients should receive a plain 401/403 response without redirect." + `location /api/ { auth_request /oauth2/auth; error_page 401 =401; proxy_pass http://backend/; }` 예제), `raw/official-docs/oauth2-proxy-nginx-integration-official.md#O2PN-C5` (browser 측 302 패턴 — 본 결정이 분리해낸 반대편) | `official-vendor-doc` (2026-07-17 조사로 확보 — 렌더링 HTML + GitHub raw markdown 2중 fetch 로 실존 확인, 이전 조사의 불일치는 재현되지 않음) | 공식은 **권고(should)** 일 뿐 강제 규범이 아님 (`O2PN-C9` does-not-prove). 또한 브라우저 `fetch` 가 302 를 실제로 어떻게 follow 하는지(그리고 CORS preflight 영향)는 원문 범위 밖 — `Claims To Verify` 로 실측 이관. route 를 browser/API 로 **어떤 기준으로 나눌지**(path prefix? `Accept` 헤더? `X-Requested-With`?)는 공식 미제시 — §구현 가이드 §3 의 `UNSUPPORTED_IMPL_DECISION` 참조 |
## 구현 가이드
> 본 sub-sub 는 `documented-only` — 여기서 "구현"은 **nginx.conf 를 되묻지 않고 작성할 수 있는 수준의 사전 명세**를 뜻한다. 각 block 을 결정(D2~D9) + 근거 Claim ID 로 trace 하고, 공식 예제가 *말하지 않는* 선택은 `UNSUPPORTED_IMPL_DECISION` + trade-off 한 줄로 분리한다(CLAUDE.md §15.5 R2).
>
> **OUT_OF_BRANCH_SCOPE 정제(R3)**: oauth2-proxy 자체 flag(`--set-xauthrequest`·`--pass-access-token`·`--cookie-secret`)의 *값과 구성* 은 형제 [[raw/branch-notes/feature-keycloak-oauth2-proxy-oidc-flow]] 소유 — 본 §에는 "이 flag 가 켜져 있어야 이 nginx 설정이 성립한다"는 **전제** 로만 등장하고 명세는 남기지 않는다. 네트워크 격리·헤더 위조 방어는 형제 [[raw/branch-notes/feature-keycloak-header-spoofing-defense]] 소유. K8s ingress-nginx annotation 방식은 §범위 Out of scope.
### 0. nginx.conf 4-block 골격 (전체 구조)
> **Trace**: 공식 1차 예제 `oauth2-proxy-nginx-integration-official#O2PN-C7` (prefix/exact 2-block 분리) + `#O2PN-C9` (API route 4번째 block) + `nginx-core-module-location-internal-official#NGCM-C2` (exact `=` 가 prefix 를 이기는 매칭 규칙 — 이 골격이 성립하는 nginx 엔진 근거).
| # | block | 위상 | 소유 결정 |
|---|---|---|---|
| 1 | `location /oauth2/` | **public** — 브라우저가 직접 도달 (start·callback·sign_in·sign_out·userinfo·static) | D7 |
| 2 | `location = /oauth2/auth` | **subrequest 전용** — exact match 라 1번 prefix 에 가로채이지 않음 | D6, D7 |
| 3 | `location /` | 보호 대상 **browser-facing** route → 401 을 302 로 | D2, D3, D4 |
| 4 | `location @oauth2_signin` | named location — 3번의 `error_page 401` 목적지 | D2 |
| 5 | `location /api/` | 보호 대상 **API/machine** route → 401 을 그대로 | D9 |
**핵심 메커니즘**: 1번과 2번은 같은 `/oauth2` 문자열을 공유하지만 **충돌하지 않는다** — `NGCM-C2` 가 증명하듯 `=` exact match 가 발견되면 nginx 는 검색을 즉시 종료하므로 `/oauth2/auth` 요청은 항상 2번으로 간다. **2번 block 을 지우면 `/oauth2/auth` 가 1번 prefix 로 매칭되어 D6 의 body 차단이 조용히 사라진다** (§엣지 참조).
### 1. `location = /oauth2/auth` — subrequest 목적지 (D6, D7)
> **Trace**: D6/D7 / `O2PN-C7`(공식 예제의 exact block), `O2PN-C8`(`Content-Length ""` + `proxy_pass_request_body off` + 인라인 주석), `NGAR-C8`(nginx.org 벤더-중립 Example Configuration 이 동일 패턴), `NGXPM-C1`(`proxy_pass_request_body` default 는 `on` — 명시 안 하면 body 가 전달됨), `O2PN-C2`(`/oauth2/auth` 는 202/401 만 반환).
>
> - **UNSUPPORTED_IMPL_DECISION**: (a) **`internal;` 부착 여부** — 공식 예제에는 `internal` 문자열이 아예 없다(`O2PN-C7` negative finding). `NGCM-C1` 이 "`auth_request` 는 공식 internal-request 트리거 목록에 포함" 을 증명하므로 **기술적으로는 안전하게 부착 가능**하나, *공식이 권고한다* 고 쓰면 과장이다. trade-off: 부착하면 외부 client 가 `/oauth2/auth` 를 직접 호출해 세션 유효성만 떠보는 표면이 사라지지만, `/oauth2/auth` 를 폴링하는 모니터링이 있다면 깨진다(공식이 그런 용도를 언급하지 않아 반증·확증 모두 불가 — `needs-confirmation`). **학습 단계 권고: 부착하지 않고 공식 예제를 그대로 재현 → P3A 에서 하드닝 선택.** (b) **upstream 주소** — 공식 예제는 `http://127.0.0.1:4180`. docker-compose 스택이면 service 명(`http://oauth2-proxy:4180`)이 되어야 하나 이는 배포 형태 의존이며 공식 미제시.
| directive | 값 | 근거 / 사유 |
|---|---|---|
| `proxy_pass` | `http://127.0.0.1:4180` (공식 예제 값) | `O2PN-C7`. 배포 형태에 따라 service 명으로 교체 — `UNSUPPORTED_IMPL_DECISION` (b) |
| `proxy_set_header Host` | `$host` | `O2PN-C7` |
| `proxy_set_header X-Real-IP` | `$remote_addr` | `O2PN-C7` |
| `proxy_set_header X-Forwarded-Uri` | `$request_uri` | `O2PN-C7` |
| `proxy_set_header Content-Length` | `""` | `O2PN-C8`, `NGAR-C8` — body 차단의 짝 |
| `proxy_pass_request_body` | `off` | `O2PN-C8`, `NGAR-C8`. **생략하면 default `on`(`NGXPM-C1`) 이라 body 가 전달됨** |
| `internal` | (미부착 — 학습 단계) | `UNSUPPORTED_IMPL_DECISION` (a) |
> ⚠️ **인용 경계**: 공식 주석 `# nginx auth_request includes headers but not body` 는 auth_request 의 **설계 사실** 을 말할 뿐, "body 를 넘기면 POST 가 오발동하거나 CPU 가 오른다"는 **인과** 를 말하지 않는다(`O2PN-C8` does-not-prove). 그 인과는 본 노트의 추론이며 §Claims To Verify 로 분리했다 — D6 의 근거로 재진술 금지.
### 2. `location /oauth2/` — oauth2-proxy 공개 endpoint (D7)
> **Trace**: D7 / `O2PN-C7`(공식 예제의 prefix block + `X-Auth-Request-Redirect` 헤더), `oauth2-proxy-endpoints-official#O2EP-C1`~`C6`(각 endpoint 의 용도), `#O2EP-C7`(`/oauth2/auth` 만 "for use with the Nginx auth_request directive" 라벨 — 나머지 6개엔 그 제약 없음), `#O2EP-C8`(전체가 `/oauth2` prefix 공유, `--proxy-prefix` 로 변경 가능).
| endpoint | 이 block 으로 노출되는 이유 | 근거 |
|---|---|---|
| `/oauth2/start` | OAuth cycle 을 시작하는 redirect URL — 브라우저가 진입 | `O2EP-C1` |
| `/oauth2/callback` | IdP 가 **브라우저를 이 URL 로 되돌린다** — 외부 도달 불가면 로그인 자체가 완결 불가 | `O2EP-C2` |
| `/oauth2/sign_in` | 로그인 페이지(겸 cookie 제거) | `O2EP-C3` |
| `/oauth2/sign_out` | 세션 cookie 제거 | `O2EP-C4` |
| `/oauth2/userinfo` | 세션의 email 을 JSON 으로 반환 | `O2EP-C5` |
| `/oauth2/static/*` | sign_in/error 페이지의 stylesheet 등 | `O2EP-C6` |
`proxy_set_header X-Auth-Request-Redirect $request_uri;` 를 포함(`O2PN-C7`). 다중 도메인이면 공식 예제 주석대로 `$scheme://$host$request_uri` 로 확장(D7 선택 조건).
> ⚠️ **경계**: 위 6개가 "public 이어야 한다"는 **처방** 은 공식 문장이 아니다. 공식은 각 endpoint 가 *무엇을 하는지* 만 말한다(`O2EP-C1`~`C6` does-not-prove: 호출 주체). "그러므로 브라우저가 도달해야 한다"는 결론은 `/oauth2/callback` 의 "the oauth app will be configured with this as the callback url"(`O2EP-C2`) 에서만 강하게 함의되고, 나머지는 **본 노트의 추론**이다.
### 3. 보호 대상 route — browser vs API 분리 (D2, D9, D3, D4)
> **Trace**: D2/`O2PN-C5`(401 → `error_page` → 302 redirect), `NGAR-C2`(2xx allow / 401·403 deny contract) · D9/`O2PN-C9`(§Browser vs API Routes + `error_page 401 =401` 예제) · D3/`NGAR-C5`(`auth_request_set` + `$upstream_http_*`), `O2PN-C3`(`X-User`/`X-Email` 매핑, `--set-xauthrequest` 전제) · D4/`OAUTH2PROXY-C3`(4종 `X-Auth-Request-*` 응답 헤더).
>
> - **UNSUPPORTED_IMPL_DECISION**: (a) **browser/API 판별 기준** — 공식 예제는 `location /api/` 라는 **path prefix** 로 나눈다(`O2PN-C9`). 그러나 실제 앱이 path 로 깔끔히 갈리지 않으면(같은 path 에 HTML/JSON 혼재) `Accept` 헤더나 `X-Requested-With` 기반 분기가 필요한데 **공식은 이를 제시하지 않는다**. trade-off: path prefix 는 단순·명시적이나 앱 구조를 강제한다. 학습 단계는 공식대로 path prefix 채택. (b) **`@oauth2_signin` 의 목적지가 `/oauth2/start` vs `/oauth2/sign_in`** — 아래 별도 표 참조. (c) **backend 로 넘길 헤더 이름**(`X-User`) — 공식 예제 값이나, 본 프로젝트의 헤더 명명 규약 owner 는 **부모 D2**(`X-Auth-Request-*` 우선)이므로 부모 규약과 충돌 시 부모가 이긴다.
| route 유형 | `auth_request` | 401 처리 | 근거 |
|---|---|---|---|
| browser-facing (`location /`) | `auth_request /oauth2/auth;` | `error_page 401 = @oauth2_signin;` → 302 | D2, `O2PN-C5` |
| API/machine (`location /api/`) | `auth_request /oauth2/auth;` | `error_page 401 =401;` → **plain 401 pass-through** | D9, `O2PN-C9` |
> ⚠️ **오타 아님**: 두 `error_page` 의 `=` 형태 차이(`= @oauth2_signin` 의 space + `=` vs `=401` 의 붙임)는 **의도적**이며 각각 공식 예제 verbatim 이다(`O2PN-C5` / `O2PN-C9`). nginx `error_page` 에서 `= @named` 는 named location 이 정한 코드를 따르고, `=401` 은 응답 코드를 401 로 **강제**한다 — 문법이 낯설다고 임의로 통일하면 D9 가 깨진다. (`error_page` 의 `=` 시맨틱 자체를 증명하는 claim 은 아직 raw 에 없음 — 필요 시 `nginx-core-module-location-internal-official` 에 증설)
헤더 propagation 2-step (browser route 기준, D3/D4):
| 단계 | directive | 근거 |
|---|---|---|
| 1. subrequest 응답 헤더 → nginx 변수 | `auth_request_set $user $upstream_http_x_auth_request_user;`
`auth_request_set $email $upstream_http_x_auth_request_email;` | `NGAR-C5`, `O2PN-C3` |
| 2. 변수 → backend 요청 헤더 | `proxy_set_header X-User $user;`
`proxy_set_header X-Email $email;` | `O2PN-C3` |
| (D8-a 선택 시) 토큰 | `auth_request_set $token $upstream_http_x_auth_request_access_token;` | `O2PN-C4` — `--pass-access-token` 전제 |
**전제**: oauth2-proxy 가 `--set-xauthrequest` 로 실행되지 않으면 `X-Auth-Request-*` 응답 헤더 자체가 나오지 않아 위 2-step 이 **조용히 빈 값** 이 된다(`O2PN-C3`). 그 flag 의 owner 는 형제 [[raw/branch-notes/feature-keycloak-oauth2-proxy-oidc-flow]].
#### `@oauth2_signin` 목적지 — `/oauth2/start` vs `/oauth2/sign_in` (`UNSUPPORTED_IMPL_DECISION` (b))
본 노트의 기존 TODO 는 `return 302 .../oauth2/start?rd=...` 로 적혀 있으나, **공식 예제(`O2PN-C5`)는 `/oauth2/sign_in?rd=...`** 를 쓴다. 둘 다 실재하는 endpoint 이며 동작이 다르다:
| 목적지 | 동작 | 언제 |
|---|---|---|
| `/oauth2/sign_in` | oauth2-proxy 자체 **로그인 페이지** 를 보여줌 (`O2EP-C3`) — 공식 예제 값 | IdP 가 여럿이거나 중간 확인 화면을 원할 때 |
| `/oauth2/start` | OAuth cycle 을 **즉시 시작** 하는 redirect (`O2EP-C1`) — 중간 페이지 생략 | IdP 가 Keycloak 하나뿐이라 "Sign in with…" 화면이 군더더기일 때 |
trade-off: 본 프로젝트는 IdP 가 Keycloak 단일이므로 `/oauth2/start` 가 클릭 1회를 줄인다. 다만 **공식 예제 이탈**이므로 P3A 에서 실제 UX 를 확인하고 확정할 것. 공식이 `/oauth2/start` 를 `error_page` 목적지로 권고한 문장은 없다.
### 4. 4kb cookie 한도 + buffer 튜닝 (D5, D8)
> **Trace**: D5/`O2PN-C6`("some provider's cookies can exceed the 4kb limit and so the OAuth2 Proxy splits these into multiple parts. Nginx normally only copies the first `Set-Cookie` header from the auth_request to the response") · D8/`OAUTH2PROXY-C2`, `O2PN-C4`(`--pass-access-token` 존재).
>
> - **UNSUPPORTED_IMPL_DECISION**: **튜닝 수치 `16k` 는 전부 사용자 임의**. 공식이 말하는 것은 **4KB 한도의 존재와 cookie split 동작뿐** — `proxy_buffer_size`/`proxy_buffers`/`large_client_header_buffers` 를 얼마로 올려야 하는지는 어떤 인용에도 없다. trade-off: 16k 는 "4KB 의 4배" 라는 경험적 여유값이며 메모리를 그만큼 더 쓴다. 실 토큰 크기를 측정해 정하는 것이 옳다(§Claims To Verify).
| 항목 | 명세 | 근거 |
|---|---|---|
| 한도 | 일부 provider 의 cookie 가 **4KB 초과** → oauth2-proxy 가 `_oauth2_proxy_0`, `_1`, … 로 split | `O2PN-C6` |
| **nginx 의 한계** | nginx 는 auth_request 응답에서 **첫 번째 `Set-Cookie` 만 복사** → split 된 나머지 part 가 유실 | `O2PN-C6` |
| 버퍼 튜닝 | `proxy_buffer_size 16k; proxy_buffers 4 16k; large_client_header_buffers 4 16k;` | `UNSUPPORTED_IMPL_DECISION` — 수치 근거 없음 |
| 적용 조건 | D8-(a) 선택 시 필수 / D8-(b) 면 여유 | D5 선택 조건 |
> ⚠️ **미해결**: split cookie 를 nginx 가 모두 복사하게 만드는 정확한 방법(lua 등)은 **인용 범위 밖**(`O2PN-C6` does-not-prove). 즉 D8-(a) 를 택하면 이 branch 의 명세만으로는 첫 로그인이 깨질 수 있으며, 해법은 P3A 에서 별도 조사가 필요하다 — 본 §가 닫지 못한 유일한 in-scope 구멍.
## 엣지·실패·의존
> R4 캡처용. 정상 경로 외에 *구현 중 부딪힐* 실패/엣지/다른 계약 의존.
- **실패·엣지 경로**:
- **`auth_request` 모듈 미컴파일** (D2 전제): 모듈은 기본 빌드에 없고 `--with-http_auth_request_module` 이 필요(`NGAR-C7`). distribution 패키지가 항상 포함한다는 보장 없음 → 기대 동작: nginx 가 `auth_request` directive 를 unknown 으로 보고 **기동 실패**. 착수 전 `nginx -V 2>&1 | grep auth_request` 로 확인(§Claims To Verify).
- **`location = /oauth2/auth` 삭제 시 조용한 회귀** (D6/D7): exact block 을 지우면 `/oauth2/auth` 가 `location /oauth2/` prefix 로 매칭되고(`NGCM-C2`), 그 block 엔 `proxy_pass_request_body off` 가 없으므로 **default `on`(`NGXPM-C1`) 으로 되돌아가 body 가 전달된다**. 에러 없이 동작하므로 탐지가 어렵다.
- **`auth_request_set` 누락 시 조용한 인증 우회 착시** (D3): 2-step 중 1단계를 빠뜨리면 nginx 는 여전히 2xx/401 게이팅을 하지만 backend 는 **빈 `X-User`** 를 받는다. backend 가 헤더 유무로 신원을 판단하면 "인증됐는데 익명" 상태가 된다. `--set-xauthrequest` 미설정도 같은 증상(`O2PN-C3`).
- **subrequest 5xx / timeout** (D2): `NGAR-C3` 은 "Any other response code returned by the subrequest is considered an error" 만 말하고 **client 가 받는 정확한 코드(500 vs 502)는 미기재**. 즉 oauth2-proxy 가 죽으면 전체 요청이 fail-closed 로 차단되는데, 그 코드가 무엇인지 모른 채 알람을 설계하게 됨(§Claims To Verify).
- **split cookie 유실로 첫 로그인 실패** (D5/D8-a): nginx 가 첫 `Set-Cookie` 만 복사(`O2PN-C6`) → 세션이 절반만 심어져 로그인 루프. 해법(lua)이 인용 범위 밖이라 **본 노트만으로는 닫히지 않음**.
- **4KB 초과 시 502** (D5): 사용자 추론이며 미검증 — `O2PN-C6` 은 한도와 split 만 말한다(§Claims To Verify).
- **API route 에 302 를 반환** (D9): fetch/XHR 이 redirect 를 따라가 JSON 대신 로그인 HTML 을 받거나 CORS 로 실패. `O2PN-C9` 가 이 경로를 "should only be used for browser-facing routes" 로 명시.
- **403 은 redirect 로 해소되지 않음** (D2): 401(미인증)과 달리 403(인가 실패)은 재로그인해도 그대로이므로 `@oauth2_signin` 으로 보내면 무한 루프가 된다 → deny 유지.
- **다른 계약 의존** (대상 브랜치 + Decision ID 병기):
- 부모 [[raw/branch-notes/feature-keycloak-edge-forwardauth-no-google]] `D1`(스택 선택: K8s+ingress-nginx → oauth2-proxy / 단일 VM docker-compose → Traefik) — ⚠️ **긴장 관계**: 본 branch 가 채택한 공식 예제는 **standalone nginx** 기준인데, 부모 D1 은 단일 VM docker-compose 를 Traefik 쪽으로 보낸다. 즉 본 노트의 config 가 실제로 쓰이는 조건은 *K8s + ingress-nginx* 인데, 그 환경에서는 nginx.conf 대신 **annotation 방식**(D7 선택 조건, §범위 Out of scope)이 된다. **부모 D1 의 분기가 유지되는 한 본 §구현 가이드의 nginx.conf 는 "학습용 참조 구현"이지 배포 대상이 아니다** — 이 모순은 본 branch 가 단독으로 풀 수 없고 부모 D1 의 재검토 또는 배포 형태 확정이 선행돼야 한다.
- 부모 [[raw/branch-notes/feature-keycloak-edge-forwardauth-no-google]] `D2`(헤더 명명 = `X-Auth-Request-*` 우선) — 본 branch D4 가 전달할 헤더 **이름의 owner**. 부모가 규약을 바꾸면 §3 의 `proxy_set_header` 이름이 따라 바뀐다.
- 부모 [[raw/branch-notes/feature-keycloak-edge-forwardauth-no-google]] `D3`(backend 인증 코드 제거 + ForwardAuth 위임) — 본 branch 전체의 **존재 전제**. 이 결정이 뒤집히면 본 노트 전부가 무효.
- 형제 [[raw/branch-notes/feature-keycloak-oauth2-proxy-oidc-flow]] — **oauth2-proxy flag 의 owner**. `--set-xauthrequest`(D3/D4 의 전제), `--pass-access-token`(D8-a 의 전제)이 그쪽에서 꺼지면 본 branch 의 헤더 명세가 조용히 빈다.
- 형제 [[raw/branch-notes/feature-keycloak-oauth2-proxy-oidc-flow]] — **`--proxy-prefix` (D7 의 전제, blast radius 최대)**: 모든 endpoint 가 `/oauth2` prefix 를 공유하는 것은 **기본값일 뿐 변경 가능**(`O2EP-C8`). 이 flag 가 바뀌면 헤더가 비는 정도가 아니라 §구현 가이드 §0 의 **5개 block 경로 전부 + `auth_request /oauth2/auth;` + `@oauth2_signin` 의 `/oauth2/start` 가 모두 조용히 404** 가 된다. 형제가 이 값을 확정하기 전에 본 branch 의 경로 의존을 알려야 한다.
- 형제 [[raw/branch-notes/feature-keycloak-header-spoofing-defense]] `D5` — **역방향 의존**: 그 branch 가 도입하려는 `X-Internal-Auth-Token` 은 본 branch 의 현재 전달 헤더 목록(D4: `X-Auth-Request-*` only)에 **없다**. 그쪽 D5 를 실 구현하려면 **본 branch 의 proxy 구성이 그 헤더를 주입하도록 확장되는 것이 선행**돼야 한다(신규 Decision 필요 — 현재 미존재 계약).
- 형제 [[raw/branch-notes/feature-keycloak-header-spoofing-defense]] 전체 — 본 branch D3 가 주입하는 `X-User` 를 backend 가 신뢰해도 되는 근거는 **본 branch 가 제공하지 않는다**(`O2PN-C3` does-not-prove). 네트워크 격리가 없으면 D3 는 보안적으로 무의미해진다.
- [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] — D8-(a) 채택 시 backend RS 의 audience 검증이 **필수 선행**(`O2PN-C4` does-not-prove: token forwarding ≠ audience validation).
- **범위 밖(본 branch 미소유)**: K8s ingress-nginx annotation 구성 / Traefik `forwardAuth` 매핑 / oauth2-proxy 의 provider·cookie secret 구성.
## 검증해야 할 주장
> 공식 vendor docs 가 contract 의 존재를 증명해도 내 학습 시연에서 실제 nginx 빌드의 동작은 별개. 다음은 P3A 또는 실 구성 단계에서 실측해야 할 주장.
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| 사용하는 nginx 패키지에 `--with-http_auth_request_module` 이 컴파일되어 있는지 | `NGAR-C7` 은 모듈이 기본 빌드 아님 + configure 옵션 필요를 명시. distribution 별 패키지가 항상 포함한다는 보장 없음 | `nginx -V 2>&1 \| grep auth_request` 출력 확인 (Alpine / Debian / Amazon Linux 등 환경별 차이) | `needs-confirmation` |
| nginx 가 oauth2-proxy 의 multi-part `Set-Cookie` 헤더를 모두 응답에 복사하는지 | `O2PN-C6` 은 nginx 가 기본적으로 첫 번째 Set-Cookie 만 복사한다고 명시. lua 스크립트 또는 별도 처리 필요 | curl 로 첫 로그인 후 응답 `Set-Cookie` 헤더 개수 확인 + `_oauth2_proxy_0`, `_1`, ... 모두 도달했는지 검증 | `planned` |
| subrequest 가 5xx 또는 timeout 시 nginx 가 정확히 어떤 응답 코드를 client 에 반환하는지 | `NGAR-C3` 은 "Any other response code is considered an error" 만 명시, 500 vs 502 구분 없음 | oauth2-proxy 를 의도적으로 다운시킨 후 nginx 응답 코드 측정 | `needs-confirmation` |
| `auth_request_set` 의 변수가 동일 location 내 여러 `proxy_set_header` 에 안정적으로 사용되는지 (변수 lifetime) | `NGAR-C5` 는 변수가 authorization request 완료 후 set 됨을 명시. 그러나 location 분기 / rewrite 후의 변수 lifetime 은 인용에 없음 | nested location + rewrite 시나리오 작성 후 backend 가 받는 `X-User` 헤더 값 추적 | `planned` |
| 4kb cookie 한도 함정이 access token 포함 시 실제로 502 를 유발하는지 | `O2PN-C6` 은 4kb 한도와 cookie split 만 명시. 502 발생 메커니즘은 본 사용자 메모의 추론 | access token 을 cookie 에 담은 상태에서 nginx 기본 buffer 로 시연 후 502 발생 여부 + 튜닝 (`proxy_buffer_size 16k`) 후 정상화 확인 | `needs-confirmation` |
| XHR / API client 가 401 → 302 redirect 를 받았을 때의 동작 (브라우저 fetch 의 redirect follow 정책) | **(2026-07-17 부분 해소)** — "API client 를 별도 처리해야 한다"는 *원칙* 자체는 이제 `O2PN-C9` 로 공식 근거 확보(302 는 browser-facing route 전용, API/machine 은 plain 401/403) → **D9 로 승격**. 다만 브라우저 `fetch` 가 302 를 실제로 어떻게 follow 하는지와 CORS preflight 영향은 `O2PN-C9` 인용 범위 밖(does-not-prove) — 여전히 실측 필요 | fetch / axios 로 protected endpoint 호출 후 redirect follow 동작 + CORS preflight 영향 확인. D9 적용 전(302)/후(`error_page 401 =401`) 응답을 비교 | `planned` |
| subrequest 에 body 를 전달하면 실제로 POST endpoint 오발동 또는 oauth2-proxy CPU 증가가 발생하는지 | **D6 의 Open Risk 에 있던 이 인과 서술은 2026-07-17 조사로 검증되지 않았다.** `O2PN-C8`/`NGAR-C8`/`NGXPM-C1` 은 (1) 공식 예제가 `off` 를 포함한다는 사실, (2) default 가 `on` 이라는 시맨틱만 증명하고, **body 를 넘겼을 때의 실패 모드는 어느 원문에도 없다**. 순수 사용자 추론이므로 D6 의 *근거* 로 재진술 금지 | `proxy_pass_request_body` 를 의도적으로 `on` 으로 둔 상태에서 대용량 body POST 를 반복 재현 → oauth2-proxy 로그(요청 body 수신 여부)와 CPU/메모리 관찰. 오발동 여부는 `/oauth2/auth` 가 body 를 읽는지로 판정 | `needs-confirmation` |
| `location = /oauth2/auth` 에 `internal;` 을 부착해도 `/oauth2/start`·`/oauth2/callback` 등 public endpoint 도달성이 깨지지 않는지 | `NGCM-C1`(auth_request 가 공식 internal-request 트리거 목록에 포함) + `NGCM-C2`(exact match 우선)로 **기술적 가능성** 은 근거 확보. 그러나 "그러므로 안전하다"는 결론은 3개 사실을 **결합한 추론**이며 어떤 공식 문서도 직접 말하지 않는다. 공식 예제 자체는 `internal;` 을 쓰지 않는다(`O2PN-C7` negative finding) | `internal;` 부착 후 (a) 브라우저로 `/oauth2/start` 진입 → 로그인 완결되는지, (b) 외부에서 `curl /oauth2/auth` → 404 반환되는지, (c) 보호 route 의 auth_request 는 정상 동작하는지 3종 확인 | `needs-confirmation` |
| nginx buffer 튜닝 수치(`16k`)가 본 프로젝트의 실제 Keycloak 토큰 크기에 적정한지 | 공식은 **4KB 한도의 존재** 만 말하고(`O2PN-C6`) 권장 버퍼 수치를 제시하지 않는다 — `16k` 는 "4KB 의 4배" 라는 사용자 임의값(§구현 가이드 §4 `UNSUPPORTED_IMPL_DECISION`) | 실제 Keycloak realm 의 access/refresh 토큰과 세션 cookie 크기를 측정한 뒤 필요한 버퍼를 역산. 과도한 값은 메모리 낭비이므로 실측 기반으로 확정 | `planned` |
| `@oauth2_signin` 의 목적지를 `/oauth2/start` 로 쓰는 것(현 TODO)이 공식 예제의 `/oauth2/sign_in`(`O2PN-C5`) 대비 UX·동작상 문제가 없는지 | 두 endpoint 는 동작이 다르다 — `/oauth2/start` 는 OAuth cycle 즉시 시작(`O2EP-C1`), `/oauth2/sign_in` 은 로그인 페이지 표시(`O2EP-C3`). 공식 예제는 후자를 쓴다. 단일 IdP(Keycloak) 환경에서 전자가 낫다는 것은 **사용자 판단**이며 공식 권고 아님 | 두 목적지로 각각 구성해 브라우저 진입 → Keycloak 로그인 → 원 URL 복귀(`rd` 파라미터)까지의 클릭 수와 중간 화면 유무 비교 | `planned` |
## 마주친 문제
- 아직 없음(문서 단계).
## 묶음
- [[raw/official-docs/nginx-auth-request-module-official]]
- [[raw/official-docs/nginx-core-module-location-internal-official]]
- [[raw/official-docs/oauth2-proxy-endpoints-official]]
- [[raw/official-docs/oauth2-proxy-nginx-integration-official]]
- [[raw/official-docs/oauth2-proxy-overview-config-official]]
- [[raw/official-docs/proxy-pass-request-body-nginx-official]]
- [[raw/official-docs/security-jwt-rfc-7519-validation]]
> 본 sub-sub-branch 는 leaf — 자식 자료 없음. Phase 3 P3A 실 구현 또는 외부 산출물 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.
### 오류 기록 (이 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/` 직접 승급 없음.