이번 파이프라인 작업과 무관하게 작업 트리에 남아 있던 것을 그대로 올린다. 사용자가 「전부 커밋」으로 정했고, 이번 작업과 섞이지 않게 커밋만 나눴다. 대부분은 clean-architecture-backend-template 의 그림 정본 재배치다 — final/assets/diagrams/<이름>/ 에 있던 것이 CLAUDE.md 가 적은 배치인 final/assets/<이름>/ 로 옮겨졌고 .techviz/<이름>/ 이 함께 들어왔다. 삽입 줄의 대부분(3.15M)이 그 .techviz context.json 이다. 그 밖에 ca-tmpl·document-haness 의 정리, .claude/agents/ 열한 개, writing-practitioner-guides 스킬, .playwright-mcp 세션 산출물, scripts/check-ssot-facts.py 와 그 시험이 들어 있다. 이 커밋의 내용은 내가 만든 것이 아니라 이전 세션이 남긴 것이고 검증하지 않았다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
304 lines
21 KiB
Markdown
304 lines
21 KiB
Markdown
---
|
|
id: a0e1cc05-92b3-4dac-bce1-513ab8cd862b
|
|
kind: CASE
|
|
slug: identity-header-trust
|
|
title: Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유
|
|
topic: oauth-oidc-auth-boundary
|
|
topicName: OAuth/OIDC 인증 경계
|
|
project: KeyCloak Patterns
|
|
status: 게시 중
|
|
version: 42
|
|
verifiedOn: 2026-08-25
|
|
studio: "https://hyeonworks.com/studio/documents/a0e1cc05-92b3-4dac-bce1-513ab8cd862b/edit"
|
|
public: "https://hyeonworks.com/cases/identity-header-trust"
|
|
assets:
|
|
- key: ap4-edge-trust-architecture
|
|
file: ../../../final/assets/ap4-edge-trust-architecture/ap4-edge-trust-architecture.svg
|
|
- key: ap4-edge-forward-auth-flow
|
|
file: ../../../final/assets/ap4-edge-forward-auth-flow/ap4-edge-forward-auth-flow.svg
|
|
sourceRevision: keycloak-patterns-lab@2026-08
|
|
source:
|
|
- final/document.md#검토한-선택지와-막힌-지점-ap4
|
|
- final/document.md#선택의-이유와-지킨-경계-ap4
|
|
- final/document.md#선택이-코드와-흐름에-반영되는-방식-ap4-완주
|
|
- final/document.md#결정이-지켜지는지-확인하는-방법-ap4
|
|
- final/document.md#얻은-것-잃은-것-적용하지-않을-때-ap4
|
|
---
|
|
|
|
# Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유
|
|
|
|
앞단에 세운 프록시가 로그인을 대신 받으면, 업스트림은 요청에 붙어 온 X-Auth-Request-User 헤더 하나만 보고 누가 보낸 요청인지 정한다. 이 헤더는 인증을 마친 프록시가 붙일 수도 있고 브라우저가 직접 적어 보낼 수도 있는데, 업스트림에 도착한 요청만으로는 둘을 가려낼 수 없다.
|
|
|
|
## 관계
|
|
|
|
- **Forward-Auth에서 Identity Header를 신뢰하기 위한 조건**
|
|
그 기준이 세운 다섯 조건이 Nginx 설정과 업스트림 코드의 어디에 들어가 있는지 하나씩 확인했다.
|
|
- **OAuth Token과 Application Session을 구분하는 기준**
|
|
AP4_SESSION은 브라우저와 Nginx 사이에서만 오가고 사용자 정보 헤더는 Nginx와 업스트림 사이에서만 붙으며, 업스트림은 JWT를 입력으로 받지 않는다.
|
|
- **OAuth/OIDC 인증 패턴 선택 기준**
|
|
이 구조를 고르면 OAuth를 모르는 업스트림 앞에 공통 관문이 생기는 대신, 네트워크 경로와 헤더를 어디까지 믿을지를 따로 정해 두어야 한다.
|
|
- **Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가**
|
|
지금 엣지는 사용자와 이메일만 넘기고 role은 넘기지 않는데, 그러면 인가를 어디서 판단할지 정해야 한다.
|
|
|
|
## 문제
|
|
|
|
앞단 프록시가 로그인을 맡으면 업스트림은 OAuth를 몰라도 되고, 대신 요청에 붙어 온 X-Auth-Request-User 하나로 사용자를 판단한다. 그러려면 업스트림은 이 헤더를 인증을 마친 엣지가 붙였는지 브라우저가 직접 적어 넣었는지 가릴 수 있어야 한다.
|
|
|
|
가릴 방법이 없다. 두 값은 업스트림이 받은 요청에서 이름도 형식도 같고, 어느 쪽이 붙였는지 적힌 곳이 없기 때문이다.
|
|
|
|
그래서 백엔드 포트가 외부에 열려 있거나 Nginx가 브라우저의 동명 헤더를 그대로 넘기면, 공격자가 원하는 이름을 적어 인증된 사용자처럼 요청을 보낼 수 있다.
|
|
|
|
## 결론
|
|
|
|
헤더를 인증 근거로 쓰려면 세 곳에서 막아야 한다.
|
|
|
|
호스트 포트 닫기 : 외부에서 업스트림과 프록시로 바로 가는 경로를 막는다
|
|
Nginx 헤더 덮어쓰기 : 클라이언트가 보낸 동명 헤더를 합치지 않고 덮어쓴다
|
|
업스트림 내부 토큰 : 엣지를 거치지 않은 내부 요청을 막는다
|
|
|
|
호스트 포트를 닫아도 같은 Compose 네트워크 안에서는 app의 8081에 닿을 수 있고 그 요청은 Nginx를 거치지 않으니, 덮어쓰기도 함께 지나친다.
|
|
|
|
이렇게 네트워크를 나눠 두는 것만으로는 내부 서비스가 보낸 요청도, Nginx가 잘못 넘긴 헤더도 걸러 내지 못한다. 공유 토큰만 두고 포트를 열어 두면, 그 토큰을 얻은 쪽이 Nginx를 건너뛰고 업스트림에 바로 붙을 수 있다.
|
|
|
|
## 검증 환경
|
|
|
|
Keycloak 26.7.0, oauth2-proxy 7.15.2
|
|
|
|
클라이언트 : edge-proxy
|
|
confidential, PKCE S256 : o
|
|
|
|
외부 공개
|
|
Nginx : 8088
|
|
app 8081, oauth2-proxy 4180 : Compose 네트워크에 expose만, 호스트 publish x
|
|
|
|
Nginx
|
|
auth_request /oauth2/auth
|
|
location = /oauth2/auth : internal
|
|
auth_request_set : user, email, Set-Cookie 복사
|
|
클라이언트가 보낸 동명 헤더 : 덮어쓰기
|
|
신뢰할 프록시 : 단일 IP
|
|
|
|
업스트림
|
|
EdgeIdentityController.currentUser(HttpServletRequest)
|
|
X-Internal-Auth-Token 비교 : MessageDigest.isEqual
|
|
SecurityConfig의 /edge/** : permitAll
|
|
|
|
AP4_SESSION
|
|
HttpOnly : true
|
|
SameSite : Lax
|
|
Secure : false, 로컬 HTTP 학습 환경 기준
|
|
expire : 1 hour, 프록시 설정값
|
|
session-cookie-minimal : true
|
|
|
|
서버 쪽 세션 저장소 : x
|
|
automatic discovery : x
|
|
login, token, JWKS, userinfo URL : 각각 따로 관리
|
|
|
|
HTTP : o
|
|
|
|
## 재현 조건
|
|
|
|
1. 쿠키 없이 GET /를 부르면 /oauth2/start로 302가 되는지 확인한다.
|
|
|
|
2. 쿠키 없이 GET /api/edge를 부르면 Location 없는 401이 되는지 확인한다.
|
|
|
|
3. authorization request에 client_id=edge-proxy와 code_challenge_method=S256이 있는지 확인한다.
|
|
|
|
4. 로그인 뒤 쿠키가 AP4_SESSION이며 HttpOnly와 SameSite=Lax인지 확인한다.
|
|
브라우저 요청 목록에 Keycloak 토큰 엔드포인트가 없어야 한다.
|
|
Web Storage가 비어 있어야 하고 document.cookie로 세션 쿠키를 읽을 수 없어야 한다.
|
|
|
|
5. 정상 세션에 다음 헤더를 얹어 GET /api/edge를 보낸다.
|
|
X-Auth-Request-User : spoofed-admin
|
|
X-Auth-Request-Email : spoofed-admin@example.test
|
|
X-Internal-Auth-Token : attacker-controlled-token
|
|
|
|
응답은 200이고, 그 안의 user는 spoofed-admin이 아니라 실제로 인증된 사용자여야 한다.
|
|
|
|
6. 외부에서 GET /oauth2/auth를 부르면 404인지 확인한다.
|
|
|
|
7. host의 4180과 8081에 접근할 수 없는지 확인한다.
|
|
|
|
8. 내부에서 /edge/me를 부를 때 사용자 정보 헤더만 있거나 내부 토큰이 없거나 틀리면 401이고,
|
|
둘 다 맞으면 200인지 확인한다.
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
forward-auth는 실제 요청을 업스트림으로 넘기기 전에 별도의 인증 엔드포인트에 허용 여부를 묻는 방식이고, Nginx에서는 `auth_request` 디렉티브가 그 질문을 하위 요청(subrequest)으로 만든다.
|
|
|
|
## 같은 이름의 헤더가 두 곳에서 만들어진다
|
|
|
|

|
|
|
|
로그인과 세션 검증은 앞단에 세운 oauth2-proxy가 맡는다. 이렇게 로그인을 대신 받는 관문을 엣지라고 부른다. 밖에서 오는 요청은 Nginx가 받고, Nginx는 oauth2-proxy에 세션이 유효한지 물어본 결과를 업스트림 요청에 연결한다. 브라우저가 직접 부를 수 있는 주소는 Nginx뿐이고, oauth2-proxy와 Spring 업스트림은 같은 배포 안에서만 부를 수 있다. 업스트림은 요청에 붙어 온 `X-Auth-Request-User` 하나로 사용자를 판단한다. 이 헤더는 oauth2-proxy가 확인한 로그인 사용자의 이름을 담아 엣지가 업스트림 요청에 붙이는 값이고, 아래에서는 엣지가 이렇게 만들어 붙이는 값을 사용자 정보 헤더라고 부른다.
|
|
|
|
같은 이름의 헤더는 브라우저도 직접 적어 보낼 수 있다. 업스트림이 받는 요청에서 두 값은 이름도 형식도 같고 어느 쪽이 붙였는지 적힌 곳도 없어서, 업스트림은 `X-Auth-Request-User`가 엣지에서 온 값인지 브라우저가 적어 넣은 값인지 가리지 못한다. 백엔드 포트가 외부에 열려 있거나 Nginx가 브라우저의 헤더를 그대로 넘기면 공격자가 인증된 사용자처럼 요청을 보낼 수 있다.
|
|
|
|
이 구조를 고른 이유는 업스트림에 OAuth 코드를 넣기 어려워서였다. 사용자별 API를 조합하고 세밀한 인가까지 애플리케이션이 직접 맡아야 하는 경우였다면 그 조합을 백엔드가 맡는 구조가 더 자연스럽다. 업스트림을 거의 고치지 않으려고 앞단에 관문을 세웠는데, 세우고 나니 외부에서 위조할 수 있는 헤더를 그대로 믿는 구성이 됐다. 아래 세 곳은 그다음에 붙인 것이다.
|
|
|
|
### 위조 헤더를 얹은 요청에서 무엇을 보는가
|
|
|
|
로그인을 마친 브라우저가 정상 요청에 헤더 3개를 얹어 보낸다.
|
|
|
|
```http label="공격자가 보낸 요청"
|
|
GET http://localhost:8088/api/edge
|
|
Cookie: AP4_SESSION=<opaque-session>
|
|
X-Auth-Request-User: spoofed-admin
|
|
X-Auth-Request-Email: spoofed-admin@example.test
|
|
X-Internal-Auth-Token: attacker-controlled-token
|
|
```
|
|
|
|
세션 자체는 유효해서 이 요청이 200으로 처리되는 것은 정상이다. 볼 값은 응답의 `user`이고, 여기에 `spoofed-admin`이 아니라 실제로 인증된 사용자가 들어 있어야 이 검사를 통과한다. 「로그인이 성공한다」를 성공 기준으로 삼으면 이 경계는 재지 못한다. 위조 헤더가 통과해도 정상 사용자는 자기 이름을 보기 때문이다.
|
|
|
|
## 세 곳에서 나눠 막는다
|
|
|
|

|
|
|
|
앞단을 Traefik ForwardAuth로 세우는 방법도 살펴봤다. 인증 판단을 맡길 수는 있지만 OIDC(OpenID Connect) 클라이언트나 세션 관리자 자체는 아니고, 지금 Nginx가 내는 속성을 그대로 내려면 네 가지가 더 필요하다. `trustForwardHeader=false`, 허용 목록에 있는 인증 응답 헤더만 복사, 로그인 리다이렉트를 따로 만드는 일, 그리고 업스트림 내부 토큰이나 더 강한 서비스 신원(workload identity) 주입이다. 마지막 항목이 대안 설정에 없어서 그대로 바꿔 끼울 수 있다고는 확인하지 못했다. Nginx를 쓴 것은 `auth_request`와 401 처리, 헤더 추출과 덮어쓰기를 한 파일에서 볼 수 있어서다.
|
|
|
|
세 곳은 각각 걸러 내는 요청과 놓치는 요청이 다르다.
|
|
|
|
### 밖에서 들어올 수 있는 길을 8088 하나로 줄인다
|
|
|
|
밖으로 연 포트는 Nginx의 8088 하나다. `app`의 8081과 oauth2-proxy의 4180은 Compose 네트워크에 `expose`만 하고 호스트 `ports`로는 내보내지 않아서, 이 둘에는 밖에서 직접 붙을 수 없다.
|
|
|
|
인증 엔드포인트도 같은 이유로 닫아 두는데, `location = /oauth2/auth`가 `internal`이라 Nginx가 만든 하위 요청만 들어갈 수 있고, 외부에서 같은 경로를 부르면 404가 된다. `internal`을 지정하지 않으면 브라우저가 이 경로를 직접 부를 수 있다.
|
|
|
|
이 경계는 엣지를 건너뛰고 업스트림이나 프록시로 바로 가는 경로만 막아서, 내부 서비스가 보낸 요청이나 Nginx가 잘못 넘긴 헤더는 여기서 걸리지 않는다.
|
|
|
|
### 클라이언트가 보낸 헤더를 덮어써서 지운다
|
|
|
|
Nginx는 업스트림을 부르기 전에 `auth_request`로 인증 결과를 먼저 묻고, 하위 요청이 돌려준 응답 코드로 요청을 계속할지 정한다.
|
|
|
|
```nginx label="upstream을 부르기 전에 먼저 물어본다"
|
|
auth_request /oauth2/auth;
|
|
```
|
|
|
|
oauth2-proxy가 세션을 유효하다고 판단하면 결과를 응답 헤더로 돌려준다. Nginx는 `auth_request_set`으로 그 값을 지역 변수 3개에 복사한다.
|
|
|
|
```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
|
|
```
|
|
|
|
그다음 원래 요청을 그대로 넘기지 않는다. 외부 `/api/edge`는 내부 `/edge/me`로 다시 매핑되고, 헤더 3개는 클라이언트가 보낸 값에 합치지 않고 덮어쓴다.
|
|
|
|
```http label="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>
|
|
```
|
|
|
|
그래서 클라이언트가 무엇을 보냈든 업스트림 입력은 oauth2-proxy가 확인한 값이 된다. 신뢰할 프록시 범위도 IP 1개로 좁혀 두었는데, 이 범위를 넓게 잡으면 같은 내부 네트워크에 있는 다른 서비스가 신뢰받는 프록시처럼 요청을 보낼 수 있기 때문이다.
|
|
|
|
### 업스트림이 내부 토큰까지 확인한다
|
|
|
|
`EdgeIdentityController.currentUser(HttpServletRequest)`가 `/edge/me`를 받는데, 여기서 확인하는 값은 2개다. `X-Auth-Request-User`를 읽어 비어 있는지 보고, `X-Internal-Auth-Token`을 읽어 배포할 때 설정해 둔 내부 토큰(internal token)과 비교한다.
|
|
|
|
비교에는 `MessageDigest.isEqual`을 쓴다. 두 바이트 배열이 앞에서 몇 바이트까지 같은지에 따라 실행 시간이 달라지지 않는 비교다.
|
|
|
|
두 조건이 모두 맞을 때만 허용 목록에 있는 필드 4개를 응답에 넣는다.
|
|
|
|
```json label="정상 응답 — 필드 4개"
|
|
{
|
|
"pattern": "AP4-edge-forward-auth",
|
|
"user": "regular-user",
|
|
"email": "regular-user@example.test",
|
|
"identityHeader": "X-Auth-Request-User"
|
|
}
|
|
```
|
|
|
|
이 JSON이 Nginx를 지나 브라우저가 부른 `/api/edge`의 응답이 된다.
|
|
|
|
하나라도 다르면 401이 된다.
|
|
|
|
```json label="사용자 헤더가 없거나 내부 토큰이 틀릴 때"
|
|
{
|
|
"error": "trusted edge authentication is required"
|
|
}
|
|
```
|
|
|
|
:::danger
|
|
|
|
현재 `SecurityConfig`는 `/edge/**`를 `permitAll`로 두고 `/edge/me` 컨트롤러가 직접 내부 토큰을 확인한다. 새 엣지 엔드포인트를 추가하면서 같은 메서드를 부르지 않으면 그 엔드포인트는 보호되지 않는다.
|
|
|
|
:::
|
|
|
|
검사가 컨트롤러 하나에만 들어 있어서 운영으로 넘어갈 때는 필터나 인터셉터, 시큐리티 체인처럼 대상 엔드포인트 전체에 걸리는 공통 경계로 옮겨야 한다. 이 검사는 엣지를 거치지 않고 들어온 내부 요청을 걸러 낸다. 다만 토큰을 얻은 쪽에는 소용이 없으므로, 호스트 포트는 계속 닫아 두어야 한다.
|
|
|
|
### 경로에 따라 다른 코드가 돌아온다
|
|
|
|
쿠키 없이 `/`를 부르면 `/oauth2/start`로 302가 되고, 같은 상태에서 `/api/edge`를 부르면 `Location` 없는 401이 된다. 사람은 로그인 화면으로 가야 하고 프로그램은 리다이렉트를 따라가는 대신 401을 받아야 해서, 같은 미인증 요청이라도 경로마다 결과를 다르게 두었다.
|
|
|
|
**리다이렉트 없는 JSON 401은 정확히 `/api/edge` 경로에만 구성돼 있다.** 다른 경로는 로그인 리다이렉트 규칙을 따른다.
|
|
|
|
| 외부 입력 | 인증 상태 | 결과 |
|
|
|---|---|---|
|
|
| `GET /` | 미인증 | `/oauth2/start` 302 |
|
|
| `GET /api/edge` | 미인증 | 리다이렉트 없는 401 |
|
|
| `GET /oauth2/auth` | 무관 | 404 |
|
|
| `GET /` + 위조 헤더 | 정상 세션 | 실제 사용자 200 |
|
|
| `/edge/me` + 사용자 정보 헤더만 | 내부 토큰 없음 | 401 |
|
|
| `/edge/me` + 틀린 토큰 | 토큰 불일치 | 401 |
|
|
|
|
위의 네 줄은 밖에서 들어온 요청이고, 아래 두 줄은 엣지를 거치지 않고 내부에서 `/edge/me`로 바로 들어온 요청이다.
|
|
|
|
## 이 학습 환경이 보장하는 범위
|
|
|
|
여기서 확인한 것은 Keycloak 26.7.0과 oauth2-proxy 7.15.2를 한 대에서 돌리는 학습 환경이다. 쿠키 속성과 리다이렉트를 눈으로 보려고 HTTPS 대신 HTTP를 쓴 설정도 있다. 코드를 실행해 봤다고 운영까지 확인한 것은 아니다.
|
|
|
|
### 브라우저에 남는 것은 opaque 쿠키 하나다
|
|
|
|
여기서 opaque는 브라우저가 값을 해석하지 않고 다음 요청에 그대로 돌려준다는 뜻이다. 로그인이 끝나면 브라우저에는 `AP4_SESSION` 쿠키 하나가 남고, 서버 쪽 세션 저장소는 따로 두지 않았다. `session-cookie-minimal=true`를 쓰면 쿠키에는 액세스·리프레시·ID 토큰 대신 엣지에 필요한 최소 정보만 남는다.
|
|
|
|
```text label="AP4_SESSION 쿠키 설정"
|
|
name = AP4_SESSION
|
|
HttpOnly = true
|
|
SameSite = Lax
|
|
Secure = false in local HTTP fixture
|
|
expire = 1 hour in proxy configuration
|
|
```
|
|
|
|
`HttpOnly`가 붙어 있어 JavaScript로 읽을 수 없고, 유효 기간은 프록시 설정에서 1시간이다. `Secure`가 `false`인 것은 로컬에서 HTTP로 띄운 학습 환경 기준이라 그렇고, HTTPS로 올리면 `Secure = true`로 바꿔야 한다. 레플리카를 늘린다면 같은 쿠키를 검증할 시크릿을 어떻게 배포하고 교체할지도 정해야 하는데, 지금 학습 환경에는 정해 둔 것이 없다. 1시간이 지난 뒤 재인증이 어떻게 이어지는지, 쿠키에 담기지 않는 리프레시 토큰의 수명주기가 어떻게 되는지도 확인하지 않았다.
|
|
|
|
코드 교환은 브라우저가 아니라 oauth2-proxy가 컨테이너 안에서 하기 때문에, automatic discovery를 끄고 같은 realm을 가리키는 주소 4개를 각각 관리한다.
|
|
|
|
```text label="같은 realm을 가리키는 주소 4개"
|
|
issuer expected value = http://localhost:8080/realms/keycloak-patterns
|
|
login URL = http://localhost:8080/.../auth
|
|
redeem/token URL = http://keycloak:8080/.../token
|
|
JWKS/userinfo URL = http://keycloak:8080/...
|
|
```
|
|
|
|
issuer는 요청을 보내기 위한 주소가 아니라 Keycloak이 발급한 토큰의 `iss` claim이 기대한 값과 같은지 검증하는 기준값이다. 브라우저는 Docker 내부 호스트명인 `keycloak:8080`에 접근할 수 없어서 로그인에는 `localhost:8080`을 쓴다. 컨테이너 안에서는 자기 `localhost:8080`이 Keycloak이 아니므로 토큰과 JWKS(JSON Web Key Set) 요청에는 `keycloak:8080`을 쓴다.
|
|
|
|
### 업스트림이 믿는 입력
|
|
|
|
앞의 세 구조에서는 Resource Server가 서명된 JWT를 받아 서명과 issuer, audience를 직접 확인한다. `/edge/me`는 JWT를 입력으로 받지 않는다. 대신 요청이 엣지를 거쳐 들어왔다는 네트워크 위치와 `X-Internal-Auth-Token`, 엣지가 넘긴 사용자와 이메일을 믿는다. 믿는 입력이 JWT 1개에서 3개로 늘어난 셈이라, 백엔드 직접 경로나 클라이언트가 보낸 헤더 중 하나만 열려도 다른 사용자처럼 요청을 보낼 수 있다.
|
|
|
|
지금 엣지 응답은 사용자와 이메일만 전달하고 role, groups, tenant, 인증 방식, 토큰 만료는 전달하지 않는다. role을 넘기면 무엇이 달라지는지는 확인하지 않았다. 패턴이 금지하는 것은 아니지만, 헤더를 하나 늘릴 때 아래 6개를 함께 정해야 한다.
|
|
|
|
- claim 출처 : oauth2-proxy나 별도 인증 서비스가 어느 값을 읽는가
|
|
- 허용 목록 : Nginx가 어느 응답 헤더만 복사하는가
|
|
- 덮어쓰기 : 클라이언트가 보낸 동명 헤더를 항상 지우거나 덮어쓰는가
|
|
- 직렬화 : 다중 값, 구분자, 이스케이프, 최대 크기는 무엇인가
|
|
- 업스트림 검증 : 헤더 존재만 볼지 값과 서비스 신원까지 볼지
|
|
- 갱신 : role이 바뀌면 프록시 세션과 다운스트림 인가에 언제 반영되는가
|
|
|
|
### 커밋된 테스트가 확인하도록 정의한 계약
|
|
|
|
이 기록에서 확인했다고 적은 것은 마지막 실행 성적표가 아니다.
|
|
|
|
쿠키 없는 `/`는 302를 받고 쿠키 없는 `/api/edge`는 401을 받는다. authorization request에는 `edge-proxy` 클라이언트와 PKCE(Proof Key for Code Exchange) S256 challenge가 들어 있어야 한다. 로그인 뒤 쿠키는 `AP4_SESSION`이고 `HttpOnly`와 `SameSite=Lax`가 붙어 있어야 한다. 브라우저 요청 목록에는 Keycloak 토큰 엔드포인트가 없어야 하고, Web Storage는 비어 있어야 하며 `document.cookie`로는 세션 쿠키를 읽을 수 없어야 한다. 위조 헤더를 얹은 요청은 실제 사용자로 200을 받고, 외부에서 부른 `/oauth2/auth`는 404, 호스트의 4180과 8081은 접근 불가여야 한다. 사용자 정보 헤더가 없거나 내부 토큰이 없거나 틀리면 401이다.
|
|
|
|
role 전달, 새 엔드포인트에 검사를 공통으로 거는 것, 상태를 바꾸는 요청의 CSRF(Cross-Site Request Forgery), 세션 갱신, 레플리카 사이의 시크릿 공유, 내부 시크릿 교체까지 여섯 가지는 이 계약 밖이라 확인하지 않았다.
|
|
|
|
지금 설정은 `/api/edge`와 `/`를 모두 `/edge/me`로 바꾸기 때문에 `/orders/123` 같은 임의 경로를 보존하는 범용 리버스 프록시가 아니고, 그래서 경로와 메서드, 요청 본문, 스트리밍, 웹소켓, 큰 헤더 동작은 입증하지 못했다.
|
|
|
|
업스트림이 OAuth를 몰라도 되는 대신, 이 구조는 네트워크 경로와 사용자 정보 헤더, 내부 토큰을 믿어야 한다. 그것을 지키려면 내부 토큰 검사를 컨트롤러 밖 공통 경계로 옮기고, 레플리카 사이에서 세션 시크릿을 배포하고 교체하는 방법을 정해야 한다. 둘 다 아직 하지 않았다.
|
|
|
|
<!-- body:end -->
|