Files
document-haness/.run/keycloak-four-patterns/records/reference-forward-auth-header-trust.json
T

61 lines
5.8 KiB
JSON

{
"kind": "REFERENCE",
"title": "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건",
"slug": "forward-auth-identity-header-trust",
"summary": "upstream이 사용자를 판단하는 근거가 헤더 하나뿐인 구조에서, 그 헤더를 믿을 수 있게 만드는 조건을 모았다. 외부 경로 차단, 동명 헤더 덮어쓰기, internal credential 검증이 서로 다른 곳에 함께 있어야 한다.",
"purpose": "외부 요청이 edge를 지나 인증되고 upstream으로 가는 구조에서, upstream이 사용자를 판단하는 근거는 헤더 하나다.\n\n같은 이름의 헤더를 인증을 마친 edge가 만들 수도 있고 공격자가 요청에 직접 적어 보낼 수도 있다. upstream이 받는 요청에서 이 둘은 구분되지 않는다.\n\nidentity header를 upstream에서 사용하려면 먼저 그 헤더가 edge를 통해 생성됐음을 보장하는 경로와 검증 방법을 정한다.",
"rules": [
{
"title": "외부에서 upstream과 auth proxy에 직접 닿지 못하게 한다",
"body": "edge만 공개하고 나머지는 내부 network에 두면서 host port로 노출하지 않는다.\n\n이걸 안 하면 공격자가 edge를 건너뛰고 upstream을 직접 부른다. 그때는 헤더를 아무리 검사해도 공격자가 그 헤더를 마음대로 쓸 수 있어서 의미가 없다."
},
{
"title": "client가 보낸 동명 헤더를 항상 덮어쓴다",
"body": "merge가 아니라 덮어쓰기로 채우고, 인증 결과에서 복사한 값만 upstream으로 보낸다. merge로 두면 client가 보낸 값이 앞이나 뒤에 함께 붙고, 어느 쪽을 읽을지는 upstream 구현에 달려 있다.\n\ntrusted proxy 범위도 같이 좁힌다. 넓게 잡으면 같은 network 안의 다른 workload가 edge인 척할 수 있고, forwarded 계열 헤더를 믿는 설정에서는 그 범위가 곧 신뢰 경계다."
},
{
"title": "auth endpoint는 subrequest 전용으로 둔다",
"body": "이 endpoint는 외부 client가 쓰라고 만든 것이 아니다. proxy가 만드는 subrequest만 들어가게 하고 외부 호출에는 응답하지 않게 둔다. Nginx라면 `internal` location이 그 역할을 한다."
},
{
"title": "upstream이 헤더 존재만 보지 않는다",
"body": "배포 시 주입한 internal credential과 요청 값을 비교한다. 비교 구현은 입력값의 일치 길이에 따라 실행 시간이 크게 달라지지 않는 방식을 사용한다.\n\ninternal credential 검증을 controller마다 반복하면 새 endpoint에서 누락될 수 있다. 운영에서는 filter, interceptor, security chain 등 공통 처리 경로에 적용한다."
},
{
"title": "격리와 헤더 검증은 서로 대신하지 않는다",
"body": "격리는 밖에서 들어오는 직접 접근을 막고 헤더 검증은 안에서 만들어진 위조를 막는다. 막는 대상이 달라서 하나로 다른 하나를 대체했다고 쓸 수 없다."
},
{
"title": "전달할 헤더를 allowlist로 고정한다",
"body": "복사할 응답 헤더 목록을 정해 두고 그 밖은 버린다. 늘릴 때마다 claim 출처와 다중 값 구분자, escaping, 최대 크기, upstream 검증 계약을 다시 정해야 한다.\n\nuser와 email만 전달하는 구조는 누가 왔는지만 말하고 무엇을 해도 되는지는 말하지 않는다. role이 바뀌었을 때 proxy session과 downstream 인가가 언제 따라가는지도 따로 정한다."
},
{
"title": "검사 지점은 요청 실패가 아니라 응답의 사용자다",
"body": "위조 헤더를 얹은 정상 session 요청은 정상 session이니 200이 되는 것이 맞다. 확인할 값은 그 응답의 사용자가 위조 값인지 실제 인증된 사용자인지다. 요청이 실패하는지만 보면 덮어쓰기가 동작하는지 알 수 없다."
},
{
"title": "지금 확인한 것과 운영에서 더 필요한 것을 나눠 적는다",
"body": "이 기준에서 실제 fixture로 확인한 것은 외부 경로 차단, 헤더 덮어쓰기, auth endpoint 내부 전용 지정, upstream의 internal credential 확인이다.\n\n운영에서는 여기에 더 필요하다. 공유 secret을 secret manager에서 주입하고 교체 절차를 두는 것, network policy로 경로를 강제하는 것, 그리고 더 강하게 묶으려면 mTLS나 workload identity를 쓰는 것이다. 두 묶음을 같은 문단에 섞어 적지 않는다."
}
],
"verifiedOn": null,
"applyWhen": [
"upstream에 OAuth client나 JWT 검증 코드를 넣기 어려울 때",
"여러 legacy service 앞에 같은 로그인 정책을 둘 때",
"edge에서 정책을 강제할 수 있을 때",
"이미 forward-auth를 쓰고 있는 구조를 점검할 때"
],
"exceptions": [
"backend 직접 경로나 헤더 덮어쓰기를 닫을 수 없는 환경이면 이 구조를 쓰지 않는다.",
"애플리케이션이 사용자별 API 조합과 세밀한 인가를 직접 맡아야 하면 BFF 구조가 더 자연스럽다.",
"임의 경로와 body, streaming을 그대로 넘기는 범용 reverse proxy가 필요하면 URI rewrite와 timeout, 응답 헤더 처리를 따로 설계해야 한다."
],
"examples": [
"외부에는 edge만 공개하고 app과 auth proxy의 port는 host에 publish하지 않는다",
"정상 session에 위조 헤더를 얹은 요청은 200을 받지만 응답의 사용자는 실제 사용자다",
"외부에서 auth endpoint를 직접 부르면 404가 된다",
"upstream은 user 헤더와 internal token을 함께 확인하고 하나라도 어긋나면 401을 돌려준다",
"내부 검사가 controller 하나에만 있으면 새 endpoint에는 보호가 따라오지 않는다"
]
}