이번 파이프라인 작업과 무관하게 작업 트리에 남아 있던 것을 그대로 올린다. 사용자가 「전부 커밋」으로 정했고, 이번 작업과 섞이지 않게 커밋만 나눴다. 대부분은 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>
114 lines
9.9 KiB
Markdown
114 lines
9.9 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를 적용하는 방법도 검토할 수 있다.
|
|
|
|
이 네 가지를 확인한 것은 Nginx 설정 하나에서다. 같은 조건이 Traefik ForwardAuth 같은 다른 forward-auth 구현에서도 그대로 성립하는지는 확인하지 못했다. 대안 설정으로 같은 속성을 내려면 전달 헤더를 믿지 않도록 끄는 설정, 인증 응답 헤더를 허용 목록대로만 복사하는 처리, 별도의 로그인 리다이렉트 흐름, 업스트림 내부 자격 증명이나 더 강한 Workload Identity 주입이 함께 필요했다. 살펴본 대안 설정에는 마지막 항목이 없어서 같은 속성을 낸다고 입증하지 못했다.
|
|
|
|
## 적용 조건
|
|
|
|
- 업스트림에 OAuth 클라이언트나 JWT 검증 코드를 넣기 어려울 때
|
|
- 레거시 서비스 여러 개 앞에 같은 로그인 정책을 걸 때
|
|
- 엣지에서 정책을 강제할 수 있을 때
|
|
- 이미 forward-auth로 돌아가는 구조를 점검할 때
|
|
|
|
## 예외
|
|
|
|
- 백엔드로 바로 가는 경로나 헤더 덮어쓰기를 닫을 수 없는 환경이면 이 구조를 쓰지 않는다.
|
|
- 애플리케이션이 사용자별 API 조합과 세밀한 인가를 직접 맡아야 하면 BFF(Backend For Frontend) 구조를 검토한다.
|
|
- 임의 경로와 본문, 스트리밍을 그대로 넘기는 범용 리버스 프록시가 필요하면 URI 재작성과 타임아웃, 응답 헤더 처리를 따로 설계해야 한다.
|
|
|
|
## 예시
|
|
|
|
- 외부에 여는 것은 엣지뿐이고 애플리케이션과 인증 프록시의 포트는 호스트에 열지 않는다
|
|
- 정상 세션에 위조 헤더를 얹은 요청은 200을 받지만 응답에 실린 사용자는 실제 사용자다
|
|
- 외부에서 인증 엔드포인트를 직접 부르면 404가 돌아온다
|
|
- 업스트림은 사용자 헤더와 내부 토큰을 같이 확인하고 하나라도 틀리면 401을 반환한다
|
|
- 내부 검사가 특정 컨트롤러에만 있으면 새로 추가한 엔드포인트는 보호되지 않는다
|