The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9.8 KiB
id, kind, slug, title, topic, topicName, project, status, version, verifiedOn, studio, public
| id | kind | slug | title | topic | topicName | project | status | version | verifiedOn | studio | public |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 004dd0a2-5fb3-4f25-80c9-576f709de331 | REFERENCE | forward-auth-identity-header-trust | Forward-Auth에서 Identity Header를 신뢰하기 위한 조건 | oauth-oidc-auth-boundary | OAuth/OIDC 인증 경계 | KeyCloak Patterns | 게시 중 | 29 | 2026-08-30 | https://hyeonworks.com/studio/documents/004dd0a2-5fb3-4f25-80c9-576f709de331/edit | https://hyeonworks.com/references/forward-auth-identity-header-trust |
Forward-Auth에서 Identity Header를 신뢰하기 위한 조건
애플리케이션이 프록시가 전달한 사용자 정보 헤더만으로 사용자를 판단하는 구조에서는, 해당 헤더가 실제로 신뢰할 수 있는 프록시에서 전달되었다는 것을 보장해야 한다.
이를 위해 외부 사용자가 애플리케이션에 직접 접근하지 못하도록 네트워크 경로를 제한하고, 사용자가 같은 이름의 헤더를 임의로 보내더라도 프록시가 이를 제거하거나 올바른 값으로 덮어써야 한다. 또한 필요한 경우 프록시에서 전달된 요청임을 확인할 수 있는 내부용 Credential도 함께 검증한다.
세 가지는 각각 다른 구간을 막으므로 하나만 적용하지 않고 같이 구성한다.
관계
- Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유 앞에서 정리한 다섯 가지 조건이 실제 설정에 적용되어 있는지 확인한 결과.
- Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가 헤더를 어디까지 늘릴지가 이 기준의 미결 항목이다.
- OAuth Token과 Application Session을 구분하는 기준 identity 헤더를 JWT나 session과 같은 이름으로 부르지 않는다.
목적
외부 요청이 인증 프록시를 거쳐 애플리케이션으로 전달되는 구조에서는, 애플리케이션이 프록시가 추가한 사용자 정보 헤더를 기준으로 로그인한 사용자를 판단할 수 있다.
문제는 같은 이름의 헤더를 외부 사용자가 직접 만들어서 보낼 수도 있다는 점이다. 애플리케이션 입장에서는 전달받은 헤더만 보고 이것이 인증을 완료한 프록시가 추가한 값인지, 외부 사용자가 임의로 넣은 값인지 구분할 수 없다.
그래서 사용자 정보 헤더를 인증 근거로 사용하려면 먼저 외부 요청이 반드시 인증 프록시를 거쳐서만 애플리케이션에 도달하도록 구성해야 한다. 또한 애플리케이션이 받은 요청과 헤더가 신뢰할 수 있는 프록시를 통해 전달된 것인지 확인할 수 있는 방법도 같이 생각해야 한다.
규칙
1. 외부에서 애플리케이션과 인증 프록시에 직접 접근하지 못하게 한다
외부에서는 Edge에만 접근할 수 있도록 하고, 애플리케이션과 인증 프록시는 내부 네트워크에만 두어 Host Port로 직접 노출하지 않는다.
애플리케이션이 외부에 직접 노출되어 있으면 공격자가 Edge의 인증 과정을 거치지 않고 애플리케이션으로 요청을 보낼 수 있다. 이 경우 공격자가 사용자 정보 헤더까지 직접 만들어 보낼 수 있으므로, 애플리케이션은 해당 헤더가 인증을 거쳐 생성된 값인지 신뢰할 수 없게 된다. 그래서 사용자 정보 헤더를 인증 근거로 사용하려면 먼저 모든 외부 요청이 반드시 Edge를 거치도록 네트워크 경로부터 제한해야 한다.
2. client가 보낸 헤더를 항상 덮어쓴다
사용자 정보 헤더는 외부 요청에 들어 있던 값과 합치지 않고, 인증 프록시가 확인한 값으로 기존 헤더를 제거하거나 덮어쓴 뒤 애플리케이션에 전달한다.
기존 헤더와 인증 결과를 합쳐서 전달하면 공격자가 넣은 값과 프록시가 추가한 값이 하나의 헤더에 함께 포함될 수 있다. 이때 애플리케이션이 어떤 값을 사용자 정보로 사용할지는 헤더 처리 방식에 따라 달라질 수 있으므로, 인증된 값만 전달되도록 해야 한다.
또한 신뢰할 수 있는 프록시의 범위도 필요한 대상만 포함하도록 제한한다. 이 범위를 너무 넓게 설정하면 같은 내부 네트워크에 있는 다른 서비스가 신뢰받는 프록시처럼 요청을 보낼 수 있다. 특히 Forwarded나 X-Forwarded-* 헤더를 신뢰하는 구조에서는 어떤 프록시의 요청까지 신뢰할지를 먼저 좁혀 둔다.
3. auth endpoint는 subrequest 전용으로 둔다
이 Endpoint는 외부 사용자가 직접 호출하는 API가 아니라, 인증 과정에서 Proxy가 내부적으로 호출하기 위한 Endpoint다. 따라서 외부 요청으로는 접근할 수 없게 하고 Proxy가 생성한 내부 요청만 허용해야 한다.
Nginx에서는 해당 Location에 internal을 설정해 외부에서 직접 호출하는 것을 차단할 수 있다.
4. upstream이 헤더 존재만 보지 않는다
요청이 신뢰할 수 있는 Proxy에서 전달된 것인지 확인하기 위해, 배포할 때 설정한 내부용 Credential과 요청에 포함된 Credential을 비교한다. 이때 Credential 값의 일부가 얼마나 일치하는지에 따라 비교 시간이 크게 달라지지 않는 안전한 비교 방식을 사용한다.
이 검증을 각 Controller에서 개별적으로 처리하면 새로운 Endpoint를 추가할 때 검증 로직을 빠뜨릴 수 있기 때문에 운영 환경에서는 Filter, Interceptor, Security Chain과 같은 공통 처리 지점에서 모든 대상 요청에 동일한 검증이 적용되도록 구성해야 한다.
5. Network 격리와 헤더 검증을 모두 적용한다
네트워크 격리는 외부 사용자가 인증 경로를 우회해 애플리케이션에 직접 접근하는 것을 막는다. 헤더 검증은 내부 네트워크에서 전달된 요청이라도 사용자 정보 헤더가 신뢰할 수 있는 값인지 확인한다.
두 방식은 보호하는 구간과 대상이 다르므로 둘 다 구성한다.
6. 인증된 사용자 정보 헤더만 전달한다
인증 프록시가 애플리케이션으로 전달할 사용자 정보 헤더를 미리 정해 두고, 허용하지 않은 헤더는 전달하지 않는다. 새로운 헤더를 추가할 때는 해당 값이 어떤 Claim에서 만들어지는지, 여러 값이 있을 때 어떤 형식으로 전달할지, 특수 문자를 어떻게 처리할지, 허용할 최대 크기는 얼마인지, 애플리케이션에서는 그 값을 어떻게 검증하고 사용할지를 함께 정해야 한다.
현재처럼 사용자 이름과 이메일만 전달하는 구조에서는 로그인한 사용자가 누구인지는 알 수 있지만, 해당 사용자가 어떤 권한을 가지고 있는지까지 알 수는 없다. Role을 이용해 인가까지 처리하려면 Role 정보를 어떤 방식으로 전달할지뿐만 아니라, 사용자의 Role이 변경되었을 때 기존 Proxy Session과 애플리케이션의 인가 결과에 언제 반영할지도 별도로 정해야 한다.
7. 요청 성공 여부가 아니라 전달된 사용자 정보를 확인한다
정상적으로 로그인된 세션에서 사용자 정보 헤더만 위조해 요청했다면, 세션 자체는 유효하므로 요청이 200 OK로 처리되는 것은 정상이다.
테스트에서 확인해야 하는 것은 요청의 성공이나 실패가 아니라 애플리케이션이 어떤 사용자를 인증된 사용자로 인식했는지다. 공격자가 임의로 넣은 사용자 정보가 아니라, 인증 프록시가 확인한 실제 사용자 정보가 사용되어야 한다. 응답 코드만으로는 알 수 없으므로 실제 응답에 사용된 사용자 정보까지 확인한다.
8. 지금 확인한 것과 운영에서 더 필요한 것을 나눠 적는다
현재 테스트 환경에서는 외부에서 애플리케이션으로 직접 접근할 수 없는지, 외부 사용자가 넣은 사용자 정보 헤더를 인증된 값으로 덮어쓰는지, 인증 Endpoint를 내부 요청으로만 호출할 수 있는지, 그리고 애플리케이션이 내부 Credential을 검증하는지까지 확인했다.
다만 실제 운영 환경에서는 추가적인 보안 구성이 필요하다. 내부 Credential과 같은 Secret은 Secret Manager 등을 통해 안전하게 주입하고 주기적으로 교체할 수 있어야 한다. 또한 Network Policy 등을 이용해 모든 요청이 정해진 인증 경로를 거치도록 제한해야 한다. 더 강한 서비스 간 인증이 필요하다면 mTLS나 Workload Identity를 적용하는 방법도 고려할 수 있다.
적용 조건
- upstream에 OAuth client나 JWT 검증 코드를 넣기 어려울 때
- 여러 legacy service 앞에 같은 로그인 정책을 둘 때
- edge에서 정책을 강제할 수 있을 때
- 이미 forward-auth를 쓰고 있는 구조를 점검할 때
예외
- backend 직접 경로나 헤더 덮어쓰기를 닫을 수 없는 환경이면 이 구조를 쓰지 않는다.
- 애플리케이션이 사용자별 API 조합과 세밀한 인가를 직접 맡아야 하면 BFF 구조를 검토한다.
- 임의 경로와 body, streaming을 그대로 넘기는 범용 reverse proxy가 필요하면 URI rewrite와 timeout, 응답 헤더 처리를 따로 설계해야 한다.
예시
- 외부에는 edge만 공개하고 app과 auth proxy의 port는 host에 publish하지 않는다
- 정상 session에 위조 헤더를 얹은 요청은 200을 받지만 응답의 사용자는 실제 사용자다
- 외부에서 auth endpoint를 직접 부르면 404가 된다
- upstream은 user 헤더와 internal token을 같이 확인하고 하나라도 틀리면 401을 반환한다
- 내부 검사가 특정 controller에만 있으면 새 endpoint에는 보호되지 않는다