Files
document-haness/.playwright-mcp/page-2026-09-07T10-16-32-232Z.yml
T

664 lines
52 KiB
YAML

- generic [ref=f10e3]:
- link "본문으로 건너뛰기" [ref=f10e4] [cursor=pointer]:
- /url: "#main-content"
- banner [ref=f10e5]:
- generic [ref=f10e6]:
- link "TechLog Studio" [ref=f10e7] [cursor=pointer]:
- /url: /studio
- text: TechLog
- generic [ref=f10e8]: Studio
- navigation "Studio 주 탐색" [ref=f10e10]:
- link "작업본" [ref=f10e11] [cursor=pointer]:
- /url: /studio/documents
- link "게시 기록" [ref=f10e12] [cursor=pointer]:
- /url: /studio/publications
- link "새 문서" [ref=f10e13] [cursor=pointer]:
- /url: /studio/documents/new
- link "주제·프로젝트" [ref=f10e14] [cursor=pointer]:
- /url: /studio/taxonomy
- link "릴리즈" [ref=f10e15] [cursor=pointer]:
- /url: /studio/releases
- link "공개 사이트 보기" [ref=f10e16] [cursor=pointer]:
- /url: /
- button "로그아웃" [ref=f10e17]
- main [ref=f10e18]:
- generic [ref=f10e19]:
- generic [ref=f10e20]:
- region [ref=f10e21]:
- generic [ref=f10e22]:
- paragraph [ref=f10e23]: CONCEPT · VERSION 5
- heading "문서 편집" [level=1] [ref=f10e24]
- paragraph [ref=f10e25]: Forward-Auth와 Nginx auth_request의 동작
- region [ref=f10e26]:
- generic [ref=f10e27]:
- paragraph [ref=f10e28]: DOCUMENT
- heading "기본 정보" [level=2] [ref=f10e29]
- generic [ref=f10e30]:
- generic [ref=f10e31]:
- generic [ref=f10e32]: 제목
- textbox "제목" [ref=f10e33]: Forward-Auth와 Nginx auth_request의 동작
- generic [ref=f10e34]:
- generic [ref=f10e35]: slug
- textbox "slug" [ref=f10e36]:
- /placeholder: 비우면 제목에서 만듭니다 (영문 소문자·숫자·하이픈)
- text: forward-auth-and-auth-request
- generic [ref=f10e37]:
- generic [ref=f10e38]: 요약
- textbox "요약" [ref=f10e39]: forward-auth는 실제 요청을 upstream으로 넘기기 전에 별도의 인증 endpoint에 허용 여부를 묻는 방식이다. Nginx에서는 auth_request directive가 그 질문을 subrequest로 만든다. 인증 결과는 upstream 요청의 헤더로 바뀌고, upstream은 JWT 대신 그 헤더를 입력으로 받는다.
- generic [aria-hidden] [ref=f10e40]: 목록 카드에는 약 90자까지 보입니다 · 189 / 2000
- generic [ref=f10e41]:
- generic [ref=f10e42]: Topic
- combobox "Topic" [ref=f10e43]:
- option "선택하지 않음"
- option "JPA 피드 조회 성능"
- option "OAuth/OIDC 인증 경계" [selected]
- generic [ref=f10e44]:
- generic [ref=f10e45]: Project
- combobox "Project" [ref=f10e46]:
- option "미지정"
- option "Backend Clean Architecture"
- option "KeyCloak Patterns" [selected]
- option "Liner N + 1문제"
- status [ref=f10e47]
- group "축 — 고르지 않으면 이 주제의 공통 기록이 됩니다" [ref=f10e48]:
- generic [ref=f10e50] [cursor=pointer]:
- checkbox "SPA" [ref=f10e51]
- generic [ref=f10e52]: SPA
- generic [ref=f10e53] [cursor=pointer]:
- checkbox "Mediator" [ref=f10e54]
- generic [ref=f10e55]: Mediator
- generic [ref=f10e56] [cursor=pointer]:
- checkbox "BFF" [ref=f10e57]
- generic [ref=f10e58]: BFF
- generic [ref=f10e59] [cursor=pointer]:
- checkbox "Forward-Auth" [ref=f10e60]
- generic [ref=f10e61]: Forward-Auth
- group "관계" [ref=f10e62]:
- generic [ref=f10e64]:
- generic [ref=f10e65]:
- generic [ref=f10e66]: 관계 1 대상
- combobox "관계 1 대상" [ref=f10e67]:
- option "대상 선택"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
- option "Collection Fetch Join으로 N+1을 해결하다 만난 MultiBag예외와 행 폭증 문제"
- option "Collection Fetch Join Pagination의 In-memory Paging"
- option "Fetch 타입이 아닌 조회 방식으로 인한 N+1"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [disabled]
- option "Projection 이후에도 1,509행을 읽은 Row Over-fetch"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출"
- option "SPA에서 OAuth Token을 JavaScript Memory에 보관한 경우"
- option "Visibility OR이 Keyset Index를 깨뜨린 문제"
- option "Authorization Code와 PKCE가 보호하는 구간"
- option "Bearer JWT가 인증된 principal이 되기까지"
- option "Cookie로 인증하는 요청에서 CSRF token이 하는 일"
- option "브라우저가 credential을 보관하는 위치와 그 성질"
- option "Forward-Auth와 Nginx auth_request의 동작"
- option "외부 IdP Brokering의 동작"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "Feed Visibility Query Pattern"
- option "Fetch Join · Batch · Projection 선택 기준"
- option "Fetch Type과 Fetch Strategy 구분"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건" [selected]
- option "외부 IdP 연동과 Application 인증 구조의 경계"
- option "JPA N+1 정량 진단 기준"
- option "Keyset Pagination 설계 기준"
- option "OAuth/OIDC 인증 패턴 선택 기준" [disabled]
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "PostgreSQL Query Plan 측정 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Top-N-per-group 선택 기준"
- option "실제 동시 트래픽에서도 이 구조가 안정적인가"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "ANALYZE 이후 Cardinality Estimate는 어떻게 달라지는가"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "feed_visible을 Production CQRS로 승격할 것인가"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Highlight 없는 FeedItem을 허용할 것인가"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "Round Trip과 Row Volume을 독립 측정할 것인가"
- option "BFF가 OAuth Token을 관리하는 조건"
- option "Collection Fetch Join과 Pagination을 같이 사용하지 않는다"
- option "Entity Graph 조회에는 Batch Fetch를 사용한다"
- option "Feed Pagination은 Keyset을 사용한다"
- option "외부 IdP와의 연동이라도 별도의 인증 방식이 아니다."
- option "Query Plan은 실제 PostgreSQL에서 측정한다"
- option "Query Strategy는 FeedQueryPort 뒤에서 소유한다"
- option "현재 Read Model은 CQRS-lite로 유지한다"
- option "화면 조회는 Read Projection을 사용한다"
- generic [ref=f10e68]:
- generic [ref=f10e69]: 관계 1 이유
- textbox "관계 1 이유" [ref=f10e70]: 이 동작을 운영에서 신뢰하려면 무엇이 필요한지 정리한 기준이다.
- generic [aria-hidden] [ref=f10e71]: 공개 화면의 「다음에 읽을 것」에 그대로 나갑니다.
- generic [ref=f10e72]:
- button "위로" [disabled] [ref=f10e73]
- button "아래로" [ref=f10e74]
- button "삭제" [ref=f10e75]
- generic [ref=f10e76]:
- generic [ref=f10e77]:
- generic [ref=f10e78]: 관계 2 대상
- combobox "관계 2 대상" [ref=f10e79]:
- option "대상 선택"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
- option "Collection Fetch Join으로 N+1을 해결하다 만난 MultiBag예외와 행 폭증 문제"
- option "Collection Fetch Join Pagination의 In-memory Paging"
- option "Fetch 타입이 아닌 조회 방식으로 인한 N+1"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [selected]
- option "Projection 이후에도 1,509행을 읽은 Row Over-fetch"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출"
- option "SPA에서 OAuth Token을 JavaScript Memory에 보관한 경우"
- option "Visibility OR이 Keyset Index를 깨뜨린 문제"
- option "Authorization Code와 PKCE가 보호하는 구간"
- option "Bearer JWT가 인증된 principal이 되기까지"
- option "Cookie로 인증하는 요청에서 CSRF token이 하는 일"
- option "브라우저가 credential을 보관하는 위치와 그 성질"
- option "Forward-Auth와 Nginx auth_request의 동작"
- option "외부 IdP Brokering의 동작"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "Feed Visibility Query Pattern"
- option "Fetch Join · Batch · Projection 선택 기준"
- option "Fetch Type과 Fetch Strategy 구분"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건" [disabled]
- option "외부 IdP 연동과 Application 인증 구조의 경계"
- option "JPA N+1 정량 진단 기준"
- option "Keyset Pagination 설계 기준"
- option "OAuth/OIDC 인증 패턴 선택 기준" [disabled]
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "PostgreSQL Query Plan 측정 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Top-N-per-group 선택 기준"
- option "실제 동시 트래픽에서도 이 구조가 안정적인가"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "ANALYZE 이후 Cardinality Estimate는 어떻게 달라지는가"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "feed_visible을 Production CQRS로 승격할 것인가"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Highlight 없는 FeedItem을 허용할 것인가"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "Round Trip과 Row Volume을 독립 측정할 것인가"
- option "BFF가 OAuth Token을 관리하는 조건"
- option "Collection Fetch Join과 Pagination을 같이 사용하지 않는다"
- option "Entity Graph 조회에는 Batch Fetch를 사용한다"
- option "Feed Pagination은 Keyset을 사용한다"
- option "외부 IdP와의 연동이라도 별도의 인증 방식이 아니다."
- option "Query Plan은 실제 PostgreSQL에서 측정한다"
- option "Query Strategy는 FeedQueryPort 뒤에서 소유한다"
- option "현재 Read Model은 CQRS-lite로 유지한다"
- option "화면 조회는 Read Projection을 사용한다"
- generic [ref=f10e80]:
- generic [ref=f10e81]: 관계 2 이유
- textbox "관계 2 이유" [ref=f10e82]: 헤더 위조를 실제로 재현한 기록이다.
- generic [aria-hidden] [ref=f10e83]: 공개 화면의 「다음에 읽을 것」에 그대로 나갑니다.
- generic [ref=f10e84]:
- button "위로" [ref=f10e85]
- button "아래로" [ref=f10e86]
- button "삭제" [ref=f10e87]
- generic [ref=f10e88]:
- generic [ref=f10e89]:
- generic [ref=f10e90]: 관계 3 대상
- combobox "관계 3 대상" [ref=f10e91]:
- option "대상 선택"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
- option "Collection Fetch Join으로 N+1을 해결하다 만난 MultiBag예외와 행 폭증 문제"
- option "Collection Fetch Join Pagination의 In-memory Paging"
- option "Fetch 타입이 아닌 조회 방식으로 인한 N+1"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [disabled]
- option "Projection 이후에도 1,509행을 읽은 Row Over-fetch"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출"
- option "SPA에서 OAuth Token을 JavaScript Memory에 보관한 경우"
- option "Visibility OR이 Keyset Index를 깨뜨린 문제"
- option "Authorization Code와 PKCE가 보호하는 구간"
- option "Bearer JWT가 인증된 principal이 되기까지"
- option "Cookie로 인증하는 요청에서 CSRF token이 하는 일"
- option "브라우저가 credential을 보관하는 위치와 그 성질"
- option "Forward-Auth와 Nginx auth_request의 동작"
- option "외부 IdP Brokering의 동작"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "Feed Visibility Query Pattern"
- option "Fetch Join · Batch · Projection 선택 기준"
- option "Fetch Type과 Fetch Strategy 구분"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건" [disabled]
- option "외부 IdP 연동과 Application 인증 구조의 경계"
- option "JPA N+1 정량 진단 기준"
- option "Keyset Pagination 설계 기준"
- option "OAuth/OIDC 인증 패턴 선택 기준" [selected]
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "PostgreSQL Query Plan 측정 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Top-N-per-group 선택 기준"
- option "실제 동시 트래픽에서도 이 구조가 안정적인가"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "ANALYZE 이후 Cardinality Estimate는 어떻게 달라지는가"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "feed_visible을 Production CQRS로 승격할 것인가"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Highlight 없는 FeedItem을 허용할 것인가"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "Round Trip과 Row Volume을 독립 측정할 것인가"
- option "BFF가 OAuth Token을 관리하는 조건"
- option "Collection Fetch Join과 Pagination을 같이 사용하지 않는다"
- option "Entity Graph 조회에는 Batch Fetch를 사용한다"
- option "Feed Pagination은 Keyset을 사용한다"
- option "외부 IdP와의 연동이라도 별도의 인증 방식이 아니다."
- option "Query Plan은 실제 PostgreSQL에서 측정한다"
- option "Query Strategy는 FeedQueryPort 뒤에서 소유한다"
- option "현재 Read Model은 CQRS-lite로 유지한다"
- option "화면 조회는 Read Projection을 사용한다"
- generic [ref=f10e92]:
- generic [ref=f10e93]: 관계 3 이유
- textbox "관계 3 이유" [ref=f10e94]: 이 구조를 언제 고르는지 비교한 기준이다.
- generic [aria-hidden] [ref=f10e95]: 공개 화면의 「다음에 읽을 것」에 그대로 나갑니다.
- generic [ref=f10e96]:
- button "위로" [ref=f10e97]
- button "아래로" [disabled] [ref=f10e98]
- button "삭제" [ref=f10e99]
- button "관계 추가" [ref=f10e100]
- region [ref=f10e101]:
- generic [ref=f10e102]:
- paragraph [ref=f10e103]: CONCEPT
- heading "개념" [level=2] [ref=f10e104]
- generic [ref=f10e105]:
- generic [ref=f10e106]:
- generic [ref=f10e107]: 기준 버전
- textbox "기준 버전 “Kubernetes 1.31” 처럼 무엇을 보고 썼는지. 비워도 됩니다." [ref=f10e108]: oauth2-proxy 7.15.2 · Nginx auth_request module
- generic [ref=f10e109]: “Kubernetes 1.31” 처럼 무엇을 보고 썼는지. 비워도 됩니다.
- generic [ref=f10e110]:
- generic [ref=f10e111]: 본문 Markdown
- group "Markdown 삽입" [ref=f10e112]:
- button "코드" [ref=f10e113] [cursor=pointer]
- button "표" [ref=f10e114] [cursor=pointer]
- button "목록" [ref=f10e115] [cursor=pointer]
- textbox "본문 Markdown" [ref=f10e116]: "## 요청 하나가 두 번 평가된다 :::evidence key=\"ap4-edge-forward-auth-flow-a6ec423a\" alt=\"브라우저, Nginx, oauth2-proxy, Spring upstream 사이에서 AP4_SESSION 검증, identity header 덮어쓰기, internal token 검증과 JSON 응답이 이어지는 순서도.\" caption=\" \" zoom=\"true\" ::: 브라우저 요청이 들어오면 Nginx는 바로 upstream을 호출하지 않는다. `location /`에 다음 directive가 있다. ```nginx label=\"general location의 auth_request\" auth_request /oauth2/auth; ``` Nginx는 먼저 `/oauth2/auth`로 subrequest를 만들어 인증 결과를 받고, 그다음에 원래 요청을 처리한다. 한 번의 외부 요청이 인증 판단과 upstream 전달 두 단계로 나뉜다. `location = /oauth2/auth`는 `internal`로 선언한다. Nginx가 만드는 subrequest만 들어갈 수 있고 브라우저가 같은 URL을 직접 호출하면 정상 auth endpoint로 쓸 수 없다. 외부에서 이 경로를 부르면 404가 된다. ## subrequest가 실어 보내는 것 subrequest는 body를 보내지 않고 `Content-Length`를 비운다. 원래 요청의 문맥은 헤더로 바뀐다. | subrequest 헤더 | 값의 출처 | |---|---| | `X-Original-URL` | scheme, host와 original request URI | | `X-Real-IP` | client address | | `X-Forwarded-For` | proxy chain | | `X-Forwarded-Host` | original host | | `X-Forwarded-Proto` | original scheme | | `X-Forwarded-Uri` | original request URI | | `Cookie` | 브라우저에 cookie가 있을 때 원래 요청의 값 | oauth2-proxy는 이 정보로 session이 유효한지 판단한다. ## 401이 두 갈래로 갈린다 인증 결과가 401일 때 무엇을 돌려줄지는 location마다 다르다. | 외부 입력 | 인증 상태 | 결과 | |---|---|---| | `GET /` | 미인증 | `/oauth2/start`로 302 | | `GET /api/edge` | 미인증 | `Location` 없는 401 JSON | general location은 `@oauth2_signin`으로 이동해 로그인을 시작한다. ```http label=\"미인증 navigation의 응답\" HTTP/1.1 302 Found Location: http://localhost:8088/oauth2/start?rd=http://localhost:8088/ ``` exact API location은 redirect 없이 401을 만든다. 브라우저 UX와 프로그램이 부르는 API UX를 나눈 구성이다. 이 분리는 해당 path에만 구성돼 있고 다른 path는 general location 규칙을 따른다. ## 인증 결과를 변수로 옮긴다 oauth2-proxy가 session을 유효하다고 판단하면 auth 응답에 사용자와 이메일이 들어 있다. Nginx는 `auth_request_set`으로 그 값을 local 변수에 복사한다. ```text label=\"auth_request_set 변수\" $auth_user ← oauth2-proxy X-Auth-Request-User $auth_email ← oauth2-proxy X-Auth-Request-Email $auth_cookie ← oauth2-proxy Set-Cookie ``` ## upstream 요청을 새로 만든다 원래 요청을 그대로 전달하지 않는다. 외부 `/api/edge`는 내부 `/edge/me`로 다시 매핑되고 헤더는 Nginx가 만든 값으로 채워진다. ```http label=\"Nginx가 만드는 upstream 요청\" GET http://app:8081/edge/me X-Auth-Request-User: <oauth2-proxy-authenticated-user> X-Auth-Request-Email: <oauth2-proxy-authenticated-email> X-Internal-Auth-Token: <nginx-environment-secret> ``` client가 보낸 같은 이름의 헤더를 merge하지 않고 덮어쓴다. 공격자가 `X-Auth-Request-User: spoofed-admin`을 보내도 upstream 입력은 oauth2-proxy가 확인한 실제 user가 된다. 이 덮어쓰기가 forward-auth의 핵심이다. upstream이 받는 요청에서 브라우저가 보낸 헤더와 edge가 만든 헤더는 구분되지 않으므로, 구분은 edge에서 끝나야 한다. ## upstream은 두 겹을 확인한다 Spring controller는 헤더 두 개를 함께 본다. ```text label=\"/edge/me의 확인 순서\" 1. X-Auth-Request-User가 blank인지 확인 2. X-Internal-Auth-Token을 읽는다 3. 설정된 token과 MessageDigest.isEqual로 비교 4. 둘 다 유효하면 allowlist된 identity field만 응답에 넣는다 ``` `MessageDigest.isEqual`은 입력값의 일치 길이에 따라 실행 시간이 크게 달라지지 않는 비교다. user 헤더가 없거나 internal token이 틀리면 401이다. ```json label=\"신뢰 조건을 만족하지 못한 응답\" { \"error\": \"trusted edge authentication is required\" } ``` 이 검사는 Spring Security의 `/edge/**` rule이 아니라 controller가 직접 한다. 현재 `SecurityConfig`는 `/edge/**`를 `permitAll`로 두고 있어서, 새 edge endpoint를 추가하면서 같은 검사를 부르지 않으면 보호가 자동으로 따라오지 않는다. ## 세 겹이 서로를 대신하지 않는다 ```text label=\"AP4가 사용하는 세 방어선\" network isolation : app 8081과 oauth2-proxy 4180을 host에 publish하지 않는다 header overwrite : client가 보낸 동명 헤더를 Nginx 값으로 덮어쓴다 internal token : upstream이 edge를 거쳤다는 추가 신호를 확인한다 ``` controller의 shared token만으로는 외부 직접 접근이 어려워지는 network 성질을 대신하지 못한다. network isolation만으로는 내부 workload나 잘못된 proxy 헤더가 신뢰되는 문제를 대신하지 못한다. ## 지금 구성이 보여 주지 않는 것 general `location /`도 `proxy_pass http://app:8081/edge/me`를 쓴다. `/orders/123` 같은 임의 upstream path를 보존하는 범용 reverse proxy가 아니다. auth-request와 header trust를 관찰하는 fixture다. 실제 upstream을 붙이면 URI rewrite, request body, timeout, retry, response header, logout, 상태 변경 요청 보호를 따로 설계해야 한다. 현재 edge 응답은 user와 email만 전달하고 role, groups, tenant, token expiry는 전달하지 않는다."
- generic [ref=f10e117]: “##” 소제목이 목차가 됩니다. 아키텍처 도식은 아래에서 삽입하세요.
- group [ref=f10e118]:
- paragraph [ref=f10e119]: EVIDENCE
- heading "본문에 Asset 삽입" [level=3] [ref=f10e120]
- paragraph [ref=f10e121]: 목록에서 선택하면 본문 커서 위치에 evidence 구문을 삽입합니다. READY 상태의 Asset만 선택할 수 있습니다.
- generic [ref=f10e122]:
- generic [ref=f10e123]:
- generic [ref=f10e124]: 업로드 종류
- combobox "업로드 종류" [ref=f10e125]:
- option "이미지" [selected]
- option "다이어그램"
- option "첨부파일"
- button "Asset 업로드" [ref=f10e126]
- generic [ref=f10e127]:
- search [ref=f10e128]:
- generic [ref=f10e129]: Asset 검색
- generic [ref=f10e130]:
- searchbox "Asset 검색" [ref=f10e131]
- button "검색" [ref=f10e132]
- generic [ref=f10e133]:
- checkbox "삽입할 때 크게 보기 허용" [checked] [ref=f10e134]
- generic [ref=f10e135]: 삽입할 때 크게 보기 허용
- status [ref=f10e136]: 삽입할 수 있는 Asset 24개
- list [ref=f10e137]:
- listitem [ref=f10e138]:
- button "ap4-edge-trust-architecture-1a916e10" [ref=f10e139]
- button "삭제" [ref=f10e140]
- listitem [ref=f10e141]:
- button "ap3-bff-session-flow-1b005e15" [ref=f10e142]
- button "삭제" [ref=f10e143]
- listitem [ref=f10e144]:
- button "ap3-bff-architecture-a27ea91c" [ref=f10e145]
- button "삭제" [ref=f10e146]
- listitem [ref=f10e147]:
- button "ap2-mediator-handoff-flow-efe7039c" [ref=f10e148]
- button "삭제" [ref=f10e149]
- listitem [ref=f10e150]:
- button "ap2-mediator-architecture-c95ed25f" [ref=f10e151]
- button "삭제" [ref=f10e152]
- listitem [ref=f10e153]:
- button "projection-row-over-fetch-f2b1943b" [ref=f10e154]
- button "삭제" [ref=f10e155]
- listitem [ref=f10e156]:
- button "cartesian-row-multiplication-dce2e166" [ref=f10e157]
- button "삭제" [ref=f10e158]
- listitem [ref=f10e159]:
- button "eager-lazy-query-sequence-47c12bda" [ref=f10e160]
- button "삭제" [ref=f10e161]
- listitem [ref=f10e162]:
- button "ap3-bff-session-flow-a8dfff6f" [ref=f10e163]
- button "삭제" [ref=f10e164]
- listitem [ref=f10e165]:
- button "ap2-mediator-handoff-flow-8c2a6f8f" [ref=f10e166]
- button "삭제" [ref=f10e167]
- listitem [ref=f10e168]:
- button "ap4-edge-forward-auth-flow-a6ec423a" [ref=f10e169]
- button "삭제" [ref=f10e170]
- listitem [ref=f10e171]:
- button "ap3-csrf-boundary-971df81c" [ref=f10e172]
- button "삭제" [ref=f10e173]
- listitem [ref=f10e174]:
- button "login-api-phase-split-3e354274" [ref=f10e175]
- button "삭제" [ref=f10e176]
- listitem [ref=f10e177]:
- button "ap1-browser-bearer-flow-a7f8aa9e" [ref=f10e178]
- button "삭제" [ref=f10e179]
- listitem [ref=f10e180]:
- button "ap1-direct-architecture-0adf4199" [ref=f10e181]
- button "삭제" [ref=f10e182]
- listitem [ref=f10e183]:
- button "nplus1-query-fanout-644febe6" [ref=f10e184]
- button "삭제" [ref=f10e185]
- listitem [ref=f10e186]:
- button "ap4-edge-trust-1cff2399" [ref=f10e187]
- button "삭제" [ref=f10e188]
- listitem [ref=f10e189]:
- button "ap3-csrf-split-501dd1f7" [ref=f10e190]
- button "삭제" [ref=f10e191]
- listitem [ref=f10e192]:
- button "ap3-bff-custody-82fa18bd" [ref=f10e193]
- button "삭제" [ref=f10e194]
- listitem [ref=f10e195]:
- button "ap2-split-custody-779cb791" [ref=f10e196]
- button "삭제" [ref=f10e197]
- listitem [ref=f10e198]:
- button "ap1-custody-v3-6e0376d2" [ref=f10e199]
- button "삭제" [ref=f10e200]
- listitem [ref=f10e201]:
- button "ap1-custody-v2-e110bd98" [ref=f10e202]
- button "삭제" [ref=f10e203]
- listitem [ref=f10e204]:
- button "ap1-credential-custody-f5e0c027" [ref=f10e205]
- button "삭제" [ref=f10e206]
- listitem [ref=f10e207]:
- button "screenshot-from-2026-08-21-18-04-49-72f1f9c6" [ref=f10e208]
- button "삭제" [ref=f10e209]
- region [ref=f10e210]:
- generic [ref=f10e211]:
- paragraph [ref=f10e212]: LIVE
- heading "즉시 미리보기" [level=2] [ref=f10e213]
- generic [ref=f10e216]:
- generic [ref=f10e217]:
- navigation "문서 경로" [ref=f10e218]:
- link "동작 원리" [ref=f10e219] [cursor=pointer]:
- /url: /explore/concepts
- generic [aria-hidden] [ref=f10e220]: /
- generic [ref=f10e221]: OAuth/OIDC 인증 경계
- generic [aria-hidden] [ref=f10e222]: /
- link "KeyCloak Patterns" [ref=f10e223] [cursor=pointer]:
- /url: /projects/keycloak-patterns
- heading "Forward-Auth와 Nginx auth_request의 동작" [level=1] [ref=f10e224]
- paragraph [ref=f10e225]: forward-auth는 실제 요청을 upstream으로 넘기기 전에 별도의 인증 endpoint에 허용 여부를 묻는 방식이다. Nginx에서는 auth_request directive가 그 질문을 subrequest로 만든다. 인증 결과는 upstream 요청의 헤더로 바뀌고, upstream은 JWT 대신 그 헤더를 입력으로 받는다.
- generic [ref=f10e226]:
- generic [ref=f10e227]:
- term [ref=f10e228]: 기준
- definition [ref=f10e229]:
- paragraph [ref=f10e230]: oauth2-proxy 7.15.2 · Nginx auth_request module
- generic [ref=f10e231]:
- term [ref=f10e232]: 기록
- definition [ref=f10e233]: 게시 게시 전
- group [ref=f10e235]:
- generic "목차 · 요청 하나가 두 번 평가된다" [ref=f10e236] [cursor=pointer]
- article [ref=f10e238]:
- region [ref=f10e239]:
- heading [level=2] [ref=f10e240]:
- link "요청 하나가 두 번 평가된다 바로가기" [ref=f10e241] [cursor=pointer]:
- /url: "#요청-하나가-두-번-평가된다"
- text: 요청 하나가 두 번 평가된다
- generic [aria-hidden] [ref=f10e242]: "#"
- figure [ref=f10e243]:
- button "ap4-edge-forward-auth-flow-a6ec423a 이미지 크게 보기" [ref=f10e244]:
- img "브라우저, Nginx, oauth2-proxy, Spring upstream 사이에서 AP4_SESSION 검증, identity header 덮어쓰기, internal token 검증과 JSON 응답이 이어지는 순서도." [ref=f10e245]
- generic [ref=f10e246]: 크게 보기
- generic [ref=f10e247]: 브라우저, Nginx, oauth2-proxy, Spring upstream 사이에서 AP4_SESSION 검증, identity header 덮어쓰기, internal token 검증과 JSON 응답이 이어지는 순서도.
- paragraph [ref=f10e248]:
- text: 브라우저 요청이 들어오면 Nginx는 바로 upstream을 호출하지 않는다.
- code [ref=f10e249]: location /
- text: 에 다음 directive가 있다.
- figure "NGINX ·general location의 auth_request 코드 복사" [ref=f10e250]:
- generic [ref=f10e251]:
- generic [ref=f10e252]: NGINX
- generic [ref=f10e253]: ·general location의 auth_request
- button "코드 복사" [ref=f10e254] [cursor=pointer]: 복사
- region "general location의 auth_request 코드" [ref=f10e255]:
- code [ref=f10e256]: auth_request /oauth2/auth;
- paragraph [ref=f10e258]:
- text: Nginx는 먼저
- code [ref=f10e259]: /oauth2/auth
- text: 로 subrequest를 만들어 인증 결과를 받고, 그다음에 원래 요청을 처리한다. 한 번의 외부 요청이 인증 판단과 upstream 전달 두 단계로 나뉜다.
- paragraph [ref=f10e260]:
- code [ref=f10e261]: location = /oauth2/auth
- text:
- code [ref=f10e262]: internal
- text: 로 선언한다. Nginx가 만드는 subrequest만 들어갈 수 있고 브라우저가 같은 URL을 직접 호출하면 정상 auth endpoint로 쓸 수 없다. 외부에서 이 경로를 부르면 404가 된다.
- region [ref=f10e263]:
- heading [level=2] [ref=f10e264]:
- link "subrequest가 실어 보내는 것 바로가기" [ref=f10e265] [cursor=pointer]:
- /url: "#subrequest가-실어-보내는-것"
- text: subrequest가 실어 보내는 것
- generic [aria-hidden] [ref=f10e266]: "#"
- paragraph [ref=f10e267]:
- text: subrequest는 body를 보내지 않고
- code [ref=f10e268]: Content-Length
- text: 를 비운다. 원래 요청의 문맥은 헤더로 바뀐다.
- region "표" [ref=f10e269]:
- table [ref=f10e270]:
- caption [ref=f10e271]
- rowgroup [ref=f10e272]:
- row [ref=f10e273]:
- columnheader "subrequest 헤더" [ref=f10e274]
- columnheader "값의 출처" [ref=f10e275]
- rowgroup [ref=f10e276]:
- row [ref=f10e277]:
- cell [ref=f10e278]:
- code [ref=f10e279]: X-Original-URL
- cell "scheme, host와 original request URI" [ref=f10e280]
- row [ref=f10e281]:
- cell [ref=f10e282]:
- code [ref=f10e283]: X-Real-IP
- cell "client address" [ref=f10e284]
- row [ref=f10e285]:
- cell [ref=f10e286]:
- code [ref=f10e287]: X-Forwarded-For
- cell "proxy chain" [ref=f10e288]
- row [ref=f10e289]:
- cell [ref=f10e290]:
- code [ref=f10e291]: X-Forwarded-Host
- cell "original host" [ref=f10e292]
- row [ref=f10e293]:
- cell [ref=f10e294]:
- code [ref=f10e295]: X-Forwarded-Proto
- cell "original scheme" [ref=f10e296]
- row [ref=f10e297]:
- cell [ref=f10e298]:
- code [ref=f10e299]: X-Forwarded-Uri
- cell "original request URI" [ref=f10e300]
- row [ref=f10e301]:
- cell [ref=f10e302]:
- code [ref=f10e303]: Cookie
- cell "브라우저에 cookie가 있을 때 원래 요청의 값" [ref=f10e304]
- paragraph [ref=f10e305]: oauth2-proxy는 이 정보로 session이 유효한지 판단한다.
- region [ref=f10e306]:
- heading [level=2] [ref=f10e307]:
- link "401이 두 갈래로 갈린다 바로가기" [ref=f10e308] [cursor=pointer]:
- /url: "#401이-두-갈래로-갈린다"
- text: 401이 두 갈래로 갈린다
- generic [aria-hidden] [ref=f10e309]: "#"
- paragraph [ref=f10e310]: 인증 결과가 401일 때 무엇을 돌려줄지는 location마다 다르다.
- region "표" [ref=f10e311]:
- table [ref=f10e312]:
- caption [ref=f10e313]
- rowgroup [ref=f10e314]:
- row [ref=f10e315]:
- columnheader "외부 입력" [ref=f10e316]
- columnheader "인증 상태" [ref=f10e317]
- columnheader "결과" [ref=f10e318]
- rowgroup [ref=f10e319]:
- row [ref=f10e320]:
- cell [ref=f10e321]:
- code [ref=f10e322]: GET /
- cell "미인증" [ref=f10e323]
- cell [ref=f10e324]:
- code [ref=f10e325]: /oauth2/start
- text: 로 302
- row [ref=f10e326]:
- cell [ref=f10e327]:
- code [ref=f10e328]: GET /api/edge
- cell "미인증" [ref=f10e329]
- cell [ref=f10e330]:
- code [ref=f10e331]: Location
- text: 없는 401 JSON
- paragraph [ref=f10e332]:
- text: general location은
- code [ref=f10e333]: "@oauth2_signin"
- text: 으로 이동해 로그인을 시작한다.
- figure "HTTP ·미인증 navigation의 응답 코드 복사" [ref=f10e334]:
- generic [ref=f10e335]:
- generic [ref=f10e336]: HTTP
- generic [ref=f10e337]: ·미인증 navigation의 응답
- button "코드 복사" [ref=f10e338] [cursor=pointer]: 복사
- region "미인증 navigation의 응답 코드" [ref=f10e339]:
- code [ref=f10e340]: "HTTP/1.1 302 Found Location: http://localhost:8088/oauth2/start?rd=http://localhost:8088/"
- paragraph [ref=f10e342]: exact API location은 redirect 없이 401을 만든다. 브라우저 UX와 프로그램이 부르는 API UX를 나눈 구성이다. 이 분리는 해당 path에만 구성돼 있고 다른 path는 general location 규칙을 따른다.
- region [ref=f10e343]:
- heading [level=2] [ref=f10e344]:
- link "인증 결과를 변수로 옮긴다 바로가기" [ref=f10e345] [cursor=pointer]:
- /url: "#인증-결과를-변수로-옮긴다"
- text: 인증 결과를 변수로 옮긴다
- generic [aria-hidden] [ref=f10e346]: "#"
- paragraph [ref=f10e347]:
- text: oauth2-proxy가 session을 유효하다고 판단하면 auth 응답에 사용자와 이메일이 들어 있다. Nginx는
- code [ref=f10e348]: auth_request_set
- text: 으로 그 값을 local 변수에 복사한다.
- figure "TEXT ·auth_request_set 변수 코드 복사" [ref=f10e349]:
- generic [ref=f10e350]:
- generic [ref=f10e351]: TEXT
- generic [ref=f10e352]: ·auth_request_set 변수
- button "코드 복사" [ref=f10e353] [cursor=pointer]: 복사
- region "auth_request_set 변수 코드" [ref=f10e354]:
- code [ref=f10e355]: $auth_user ← oauth2-proxy X-Auth-Request-User $auth_email ← oauth2-proxy X-Auth-Request-Email $auth_cookie ← oauth2-proxy Set-Cookie
- region [ref=f10e357]:
- heading [level=2] [ref=f10e358]:
- link "upstream 요청을 새로 만든다 바로가기" [ref=f10e359] [cursor=pointer]:
- /url: "#upstream-요청을-새로-만든다"
- text: upstream 요청을 새로 만든다
- generic [aria-hidden] [ref=f10e360]: "#"
- paragraph [ref=f10e361]:
- text: 원래 요청을 그대로 전달하지 않는다. 외부
- code [ref=f10e362]: /api/edge
- text: 는 내부
- code [ref=f10e363]: /edge/me
- text: 로 다시 매핑되고 헤더는 Nginx가 만든 값으로 채워진다.
- figure "HTTP ·Nginx가 만드는 upstream 요청 코드 복사" [ref=f10e364]:
- generic [ref=f10e365]:
- generic [ref=f10e366]: HTTP
- generic [ref=f10e367]: ·Nginx가 만드는 upstream 요청
- button "코드 복사" [ref=f10e368] [cursor=pointer]: 복사
- region "Nginx가 만드는 upstream 요청 코드" [ref=f10e369]:
- code [ref=f10e370]: "GET http://app:8081/edge/me X-Auth-Request-User: <oauth2-proxy-authenticated-user> X-Auth-Request-Email: <oauth2-proxy-authenticated-email> X-Internal-Auth-Token: <nginx-environment-secret>"
- paragraph [ref=f10e372]:
- text: client가 보낸 같은 이름의 헤더를 merge하지 않고 덮어쓴다. 공격자가
- code [ref=f10e373]: "X-Auth-Request-User: spoofed-admin"
- text: 을 보내도 upstream 입력은 oauth2-proxy가 확인한 실제 user가 된다.
- paragraph [ref=f10e374]: 이 덮어쓰기가 forward-auth의 핵심이다. upstream이 받는 요청에서 브라우저가 보낸 헤더와 edge가 만든 헤더는 구분되지 않으므로, 구분은 edge에서 끝나야 한다.
- region [ref=f10e375]:
- heading [level=2] [ref=f10e376]:
- link "upstream은 두 겹을 확인한다 바로가기" [ref=f10e377] [cursor=pointer]:
- /url: "#upstream은-두-겹을-확인한다"
- text: upstream은 두 겹을 확인한다
- generic [aria-hidden] [ref=f10e378]: "#"
- paragraph [ref=f10e379]: Spring controller는 헤더 두 개를 함께 본다.
- figure "TEXT ·/edge/me의 확인 순서 코드 복사" [ref=f10e380]:
- generic [ref=f10e381]:
- generic [ref=f10e382]: TEXT
- generic [ref=f10e383]: ·/edge/me의 확인 순서
- button "코드 복사" [ref=f10e384] [cursor=pointer]: 복사
- region "/edge/me의 확인 순서 코드" [ref=f10e385]:
- code [ref=f10e386]: 1. X-Auth-Request-User가 blank인지 확인 2. X-Internal-Auth-Token을 읽는다 3. 설정된 token과 MessageDigest.isEqual로 비교 4. 둘 다 유효하면 allowlist된 identity field만 응답에 넣는다
- paragraph [ref=f10e388]:
- code [ref=f10e389]: MessageDigest.isEqual
- text: 은 입력값의 일치 길이에 따라 실행 시간이 크게 달라지지 않는 비교다.
- paragraph [ref=f10e390]: user 헤더가 없거나 internal token이 틀리면 401이다.
- figure "JSON ·신뢰 조건을 만족하지 못한 응답 코드 복사" [ref=f10e391]:
- generic [ref=f10e392]:
- generic [ref=f10e393]: JSON
- generic [ref=f10e394]: ·신뢰 조건을 만족하지 못한 응답
- button "코드 복사" [ref=f10e395] [cursor=pointer]: 복사
- region "신뢰 조건을 만족하지 못한 응답 코드" [ref=f10e396]:
- code [ref=f10e397]: "{ \"error\": \"trusted edge authentication is required\" }"
- paragraph [ref=f10e399]:
- text: 이 검사는 Spring Security의
- code [ref=f10e400]: /edge/**
- text: rule이 아니라 controller가 직접 한다. 현재
- code [ref=f10e401]: SecurityConfig
- text:
- code [ref=f10e402]: /edge/**
- text:
- code [ref=f10e403]: permitAll
- text: 로 두고 있어서, 새 edge endpoint를 추가하면서 같은 검사를 부르지 않으면 보호가 자동으로 따라오지 않는다.
- region [ref=f10e404]:
- heading [level=2] [ref=f10e405]:
- link "세 겹이 서로를 대신하지 않는다 바로가기" [ref=f10e406] [cursor=pointer]:
- /url: "#세-겹이-서로를-대신하지-않는다"
- text: 세 겹이 서로를 대신하지 않는다
- generic [aria-hidden] [ref=f10e407]: "#"
- figure "TEXT ·AP4가 사용하는 세 방어선 코드 복사" [ref=f10e408]:
- generic [ref=f10e409]:
- generic [ref=f10e410]: TEXT
- generic [ref=f10e411]: ·AP4가 사용하는 세 방어선
- button "코드 복사" [ref=f10e412] [cursor=pointer]: 복사
- region "AP4가 사용하는 세 방어선 코드" [ref=f10e413]:
- code [ref=f10e414]: "network isolation : app 8081과 oauth2-proxy 4180을 host에 publish하지 않는다 header overwrite : client가 보낸 동명 헤더를 Nginx 값으로 덮어쓴다 internal token : upstream이 edge를 거쳤다는 추가 신호를 확인한다"
- paragraph [ref=f10e416]: controller의 shared token만으로는 외부 직접 접근이 어려워지는 network 성질을 대신하지 못한다. network isolation만으로는 내부 workload나 잘못된 proxy 헤더가 신뢰되는 문제를 대신하지 못한다.
- region [ref=f10e417]:
- heading [level=2] [ref=f10e418]:
- link "지금 구성이 보여 주지 않는 것 바로가기" [ref=f10e419] [cursor=pointer]:
- /url: "#지금-구성이-보여-주지-않는-것"
- text: 지금 구성이 보여 주지 않는 것
- generic [aria-hidden] [ref=f10e420]: "#"
- paragraph [ref=f10e421]:
- text: general
- code [ref=f10e422]: location /
- text:
- code [ref=f10e423]: proxy_pass http://app:8081/edge/me
- text: 를 쓴다.
- code [ref=f10e424]: /orders/123
- text: 같은 임의 upstream path를 보존하는 범용 reverse proxy가 아니다. auth-request와 header trust를 관찰하는 fixture다.
- paragraph [ref=f10e425]: 실제 upstream을 붙이면 URI rewrite, request body, timeout, retry, response header, logout, 상태 변경 요청 보호를 따로 설계해야 한다. 현재 edge 응답은 user와 email만 전달하고 role, groups, tenant, token expiry는 전달하지 않는다.
- region [ref=f10e426]:
- paragraph [ref=f10e427]: Next
- heading "다음에 읽을 것" [level=2] [ref=f10e428]
- list [ref=f10e429]:
- listitem [ref=f10e430]:
- link "적용 기준 Forward-Auth에서 Identity Header를 신뢰하기 위한 조건" [ref=f10e431] [cursor=pointer]:
- /url: /references/forward-auth-identity-header-trust
- generic [ref=f10e432]: 적용 기준
- generic [ref=f10e433]:
- strong [ref=f10e434]: Forward-Auth에서 Identity Header를 신뢰하기 위한 조건
- paragraph [aria-hidden] [ref=f10e435]: 이 동작을 운영에서 신뢰하려면 무엇이 필요한지 정리한 기준이다.
- generic [aria-hidden] [ref=f10e436]:
- listitem [ref=f10e437]:
- link "검증 기록 Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [ref=f10e438] [cursor=pointer]:
- /url: /cases/identity-header-trust
- generic [ref=f10e439]: 검증 기록
- generic [ref=f10e440]:
- strong [ref=f10e441]: Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유
- paragraph [aria-hidden] [ref=f10e442]: 헤더 위조를 실제로 재현한 기록이다.
- generic [aria-hidden] [ref=f10e443]:
- listitem [ref=f10e444]:
- link "적용 기준 OAuth/OIDC 인증 패턴 선택 기준" [ref=f10e445] [cursor=pointer]:
- /url: /references/oauth-oidc-pattern-selection-criteria
- generic [ref=f10e446]: 적용 기준
- generic [ref=f10e447]:
- strong [ref=f10e448]: OAuth/OIDC 인증 패턴 선택 기준
- paragraph [aria-hidden] [ref=f10e449]: 이 구조를 언제 고르는지 비교한 기준이다.
- generic [aria-hidden] [ref=f10e450]:
- complementary [ref=f10e451]:
- heading "작업 상태" [level=2] [ref=f10e452]
- status "편집 상태" [ref=f10e453]: 저장됨
- generic [ref=f10e454]:
- generic [ref=f10e455]:
- term [ref=f10e456]: 저장 버전
- definition [ref=f10e457]: "5"
- generic [ref=f10e458]:
- term [ref=f10e459]: 종류
- definition [ref=f10e460]: 동작 원리
- paragraph [ref=f10e461]: 불완전한 초안도 저장할 수 있습니다. Ctrl+S 로도 저장합니다. 게시를 누르면 채워야 할 칸을 그 자리에 표시합니다.
- generic [ref=f10e462]:
- button "저장" [disabled] [ref=f10e463]
- button "게시" [ref=f10e464]
- paragraph [ref=f10e465]: 버전 5으로 저장했습니다.