- 계약 채택 — 독자 질문, 후보 29건(PROMOTE 24 · MERGE_INTO 4 · KEEP_IN_SSOT 1). 게시 중 17건은 전부 유지. 저장소 keycloak-pattern 은 패턴 넷이 브랜치로 갈라져 있어 revisions 로 tip 넷을 적었다. keycloak-session-store 는 같은 저장소 @ cdac9b8 - 게시된 기록의 redirect_uri 가 SSOT·코드와 달랐다 — OAuth2callback.html → callback.html (frontend/src/app.js 에서 확인). 계약 title 이 기록과 다른 7건도 기록 쪽으로 맞췄다 - 미작성 1건 작성 — 패턴 검증을 실제로 돌릴 때의 안전한 순서(Reference) - 리뷰 100건 반영 — 설명 뒤에 붙은 평가·차례 예고·독자 오해 가정·작성 지시를 지웠다. 삭제가 남긴 조각 4건을 고치고, 원래부터 잘려 있던 로컬 미리보기 라벨 1건도 닫았다 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
114 lines
9.7 KiB
Markdown
114 lines
9.7 KiB
Markdown
---
|
|
id: 004dd0a2-5fb3-4f25-80c9-576f709de331
|
|
kind: REFERENCE
|
|
slug: forward-auth-identity-header-trust
|
|
title: Forward-Auth에서 Identity Header를 신뢰하기 위한 조건
|
|
topic: oauth-oidc-auth-boundary
|
|
topicName: OAuth/OIDC 인증 경계
|
|
project: KeyCloak Patterns
|
|
status: 게시 중
|
|
version: 29
|
|
verifiedOn: 2026-08-30
|
|
studio: "https://hyeonworks.com/studio/documents/004dd0a2-5fb3-4f25-80c9-576f709de331/edit"
|
|
public: "https://hyeonworks.com/references/forward-auth-identity-header-trust"
|
|
sourceRevision: keycloak-patterns-lab@2026-08
|
|
source:
|
|
- final/document.md#검토한-선택지와-막힌-지점-ap4
|
|
- final/document.md#선택의-이유와-지킨-경계-ap4
|
|
- final/document.md#얻은-것-잃은-것-적용하지-않을-때-ap4
|
|
---
|
|
|
|
# Forward-Auth에서 Identity Header를 신뢰하기 위한 조건
|
|
|
|
애플리케이션이 프록시가 붙인 사용자 정보 헤더만 보고 로그인한 사용자를 정하는 구조라면, 그 헤더가 정말 신뢰할 수 있는 프록시에서 온 것인지를 보장해야 한다.
|
|
|
|
그러려면 외부 사용자가 애플리케이션에 직접 닿지 못하도록 네트워크 경로를 좁히고, 사용자가 같은 이름의 헤더를 실어 보내도 프록시가 그 값을 지우거나 자기가 확인한 값으로 덮어써야 한다. 프록시를 거친 요청인지 따로 확인해야 하면 내부용 자격 증명도 같이 검증한다.
|
|
|
|
## 관계
|
|
|
|
- **Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유**
|
|
앞에서 정리한 다섯 가지 조건이 실제 설정에 적용되어 있는지 확인한 결과.
|
|
- **Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가**
|
|
헤더를 어디까지 늘릴지가 이 기준의 미결 항목이다.
|
|
- **OAuth Token과 Application Session을 구분하는 기준**
|
|
사용자 정보 헤더를 JWT나 세션과 같은 이름으로 부르지 않는다.
|
|
|
|
## 목적
|
|
|
|
Forward-auth는 요청을 애플리케이션에 넘기기 전에 별도의 인증 엔드포인트에 이 요청을 통과시켜도 되는지 먼저 묻는 방식이다. 인증에 성공하면 프록시가 사용자 정보 헤더를 붙여 주므로, 애플리케이션은 그 헤더를 기준으로 로그인한 사용자를 판단할 수 있다.
|
|
|
|
문제는 같은 이름의 헤더를 외부 사용자가 직접 만들어 보낼 수도 있다는 것이다. 애플리케이션은 전달받은 헤더만 봐서는 인증을 마친 프록시가 붙인 값인지 외부 사용자가 임의로 넣은 값인지 구분할 수 없다.
|
|
|
|
그래서 사용자 정보 헤더를 인증 근거로 쓰려면 외부 요청이 인증 프록시를 거쳐야만 애플리케이션에 닿도록 경로부터 짜야 한다. 여기에 더해 애플리케이션이 받은 요청과 헤더가 신뢰할 수 있는 프록시를 지나온 것인지 확인할 방법도 같이 정한다.
|
|
|
|
## 규칙
|
|
|
|
### 1. 외부에서 애플리케이션과 인증 프록시에 직접 접근하지 못하게 한다
|
|
|
|
외부에 여는 것은 엣지 하나뿐이고, 애플리케이션과 인증 프록시는 내부 네트워크에만 두어 호스트 포트로 직접 노출하지 않는다.
|
|
|
|
애플리케이션이 외부에 직접 노출되어 있으면 공격자는 엣지의 인증을 거치지 않고 애플리케이션으로 요청을 보낼 수 있고, 이때 사용자 정보 헤더까지 직접 만들어 실을 수 있다. 그러면 애플리케이션은 받은 헤더가 인증을 거쳐 만들어진 값인지 신뢰할 수 없다. 사용자 정보 헤더를 인증 근거로 쓰려면 네트워크 경로부터 좁혀서 모든 외부 요청이 엣지를 지나게 해야 한다.
|
|
|
|
### 2. 클라이언트가 보낸 헤더를 항상 덮어쓴다
|
|
|
|
사용자 정보 헤더는 외부 요청에 실려 온 값과 합치지 않는다. 인증 프록시가 확인한 값으로 기존 헤더를 지우거나 덮어쓴 뒤에 애플리케이션으로 넘긴다.
|
|
|
|
기존 헤더와 인증 결과를 합쳐서 넘기면 공격자가 넣은 값과 프록시가 붙인 값이 한 헤더에 같이 담길 수 있다. 이때 애플리케이션이 둘 중 어느 값을 사용자 정보로 읽을지는 헤더 처리 방식에 따라 달라질 수 있으므로, 인증된 값만 남겨서 넘긴다.
|
|
|
|
신뢰할 프록시의 범위도 필요한 대상까지만 열어 둔다. 이 범위가 넓으면 같은 내부 네트워크에 있는 다른 서비스도 신뢰받는 프록시처럼 요청을 보낼 수 있다. 특히 Forwarded나 X-Forwarded-* 헤더를 믿는 구조라면 어느 프록시의 요청까지 믿을지를 먼저 좁혀 둔다.
|
|
|
|
### 3. 인증 엔드포인트는 하위 요청 전용으로 둔다
|
|
|
|
이 엔드포인트는 외부 사용자가 직접 부르는 API가 아니라, 인증 과정에서 프록시가 내부에서 부르려고 둔 것이다. 그래서 외부 요청으로는 닿을 수 없게 하고 프록시가 만든 내부 요청만 받는다.
|
|
|
|
Nginx에서는 해당 location에 internal을 설정해 외부에서 직접 부르는 것을 막을 수 있다.
|
|
|
|
### 4. 업스트림이 헤더 존재만 보지 않는다
|
|
|
|
요청이 신뢰할 수 있는 프록시에서 온 것인지 보려고, 배포할 때 설정한 내부용 자격 증명과 요청에 실려 온 값을 비교한다. 이때 값의 일부가 얼마나 일치하는지에 따라 비교에 걸리는 시간이 크게 달라지지 않는 안전한 비교 방식을 쓴다.
|
|
|
|
이 검증을 컨트롤러마다 따로 넣으면 새 엔드포인트를 추가할 때 빠뜨리기 쉽다. 운영 환경에서는 필터나 인터셉터, 시큐리티 체인처럼 대상 요청이 모두 지나는 곳 한 군데에 두어 같은 검증이 걸리게 한다.
|
|
|
|
### 5. 네트워크 격리와 헤더 검증을 모두 적용한다
|
|
|
|
네트워크 격리는 외부 사용자가 인증 경로를 건너뛰고 애플리케이션에 직접 접근하는 것을 막는다. 헤더 검증은 내부 네트워크에서 온 요청이라도 사용자 정보 헤더가 신뢰할 수 있는 값인지 확인한다.
|
|
|
|
### 6. 인증된 사용자 정보 헤더만 전달한다
|
|
|
|
인증 프록시가 애플리케이션으로 넘길 사용자 정보 헤더를 미리 정해 두고, 목록에 없는 헤더는 넘기지 않는다. 헤더를 하나 더 늘릴 때는 그 값이 어떤 클레임에서 만들어지는지, 값이 여럿일 때 어떤 형식으로 넘길지, 특수 문자를 어떻게 처리할지, 허용할 최대 크기는 얼마인지, 애플리케이션에서 그 값을 어떻게 검증하고 사용할지를 같이 정해야 한다.
|
|
|
|
사용자 이름과 이메일만 넘기는 지금 구조로는 로그인한 사용자가 누구인지까지만 알 수 있고, 그 사용자가 어떤 권한을 가졌는지는 알 수 없다. 역할(role)로 인가까지 처리하려면 역할을 어떤 방식으로 넘길지에 더해, 사용자의 역할이 바뀌었을 때 이미 열려 있는 프록시 세션과 애플리케이션의 인가 결과에 언제 반영할지도 따로 정해야 한다.
|
|
|
|
### 7. 요청 성공 여부가 아니라 전달된 사용자 정보를 확인한다
|
|
|
|
정상적으로 로그인된 세션에서 사용자 정보 헤더만 위조해 요청했다면 세션 자체는 유효하므로, 요청이 200 OK로 처리되는 것은 정상이다.
|
|
|
|
테스트에서 확인할 것은 요청의 성공이나 실패가 아니라 애플리케이션이 누구를 인증된 사용자로 인식했는지다. 응답 코드만으로는 알 수 없으니 응답에 실려 나온 사용자 정보까지 열어서, 공격자가 임의로 넣은 값이 아니라 인증 프록시가 확인한 실제 사용자 정보가 쓰였는지 확인한다.
|
|
|
|
### 8. 지금 확인한 것과 운영에서 더 필요한 것을 나눠 적는다
|
|
|
|
지금 테스트 환경에서 확인한 것은 네 가지다. 외부에서 애플리케이션으로 직접 접근할 수 없는지, 외부 사용자가 넣은 사용자 정보 헤더를 인증된 값으로 덮어쓰는지, 인증 엔드포인트를 내부 요청으로만 부를 수 있는지, 애플리케이션이 내부 자격 증명을 검증하는지까지다.
|
|
|
|
운영 환경에서는 여기에 더 얹어야 한다. 내부 자격 증명 같은 비밀값은 Secret Manager 등으로 안전하게 주입하고 주기적으로 교체할 수 있어야 하고, Network Policy 등으로 모든 요청이 정해진 인증 경로를 지나게 제한해야 한다. 서비스 사이를 더 강하게 묶어야 한다면 mTLS나 Workload Identity를 적용하는 방법도 검토할 수 있다.
|
|
|
|
## 적용 조건
|
|
|
|
- 업스트림에 OAuth 클라이언트나 JWT 검증 코드를 넣기 어려울 때
|
|
- 레거시 서비스 여러 개 앞에 같은 로그인 정책을 걸 때
|
|
- 엣지에서 정책을 강제할 수 있을 때
|
|
- 이미 forward-auth로 돌아가는 구조를 점검할 때
|
|
|
|
## 예외
|
|
|
|
- 백엔드로 바로 가는 경로나 헤더 덮어쓰기를 닫을 수 없는 환경이면 이 구조를 쓰지 않는다.
|
|
- 애플리케이션이 사용자별 API 조합과 세밀한 인가를 직접 맡아야 하면 BFF(Backend For Frontend) 구조를 검토한다.
|
|
- 임의 경로와 본문, 스트리밍을 그대로 넘기는 범용 리버스 프록시가 필요하면 URI 재작성과 타임아웃, 응답 헤더 처리를 따로 설계해야 한다.
|
|
|
|
## 예시
|
|
|
|
- 외부에 여는 것은 엣지뿐이고 애플리케이션과 인증 프록시의 포트는 호스트에 열지 않는다
|
|
- 정상 세션에 위조 헤더를 얹은 요청은 200을 받지만 응답에 실린 사용자는 실제 사용자다
|
|
- 외부에서 인증 엔드포인트를 직접 부르면 404가 돌아온다
|
|
- 업스트림은 사용자 헤더와 내부 토큰을 같이 확인하고 하나라도 틀리면 401을 반환한다
|
|
- 내부 검사가 특정 컨트롤러에만 있으면 새로 추가한 엔드포인트는 보호되지 않는다
|