멀티리전 Audit 기능 구현
리전마다 다른 요청 경로에서도, 신뢰할 수 있는 접속 정보를 남기는 방법
한국·일본 감사 로그의 접속 정보 차이를 XFF 신뢰 검증과 리전별 진입 지점에서 같은 형식으로 정리했습니다.
- Java 21
- Spring Boot
- Spring Security
- Nginx
- Playwright
감사 이벤트는 남고 있었지만 일부 요청의 ‘누가’와 ‘어디서’가 비어 있었습니다. 한국과 일본의 앞단 경로가 서로 달라 하나의 프록시 설정으로 해결할 수 없었고, 네트워크 경계에서 신뢰할 값을 만든 뒤 애플리케이션에는 같은 형식의 사용자·접속 정보로 전달해야 했습니다.
문제의 시작
감사 이벤트는 단순히 작업 이름만 남긴 로그가 아닙니다. 누가, 어디서, 무엇을 변경했는지가 함께 있어야 고객이 리소스 변경 이력을 확인하고 사고가 났을 때 요청의 출처를 추적할 수 있습니다. 기존 흐름에서는 작업 종류는 기록됐지만 일부 상태 변경 요청의 사용자 정보와 실제 접속 위치가 비어 있었습니다.
같은 화면에서 발생한 요청도 진입 경로에 따라 기록 품질이 달랐습니다. 중앙 게이트웨이를 거치는 요청은 검증된 사용자와 접속 정보가 붙었지만, 고객별 실행 환경으로 직접 들어가는 요청은 그 게이트웨이를 지나지 않았습니다. 로드밸런서와 Kubernetes 네트워크를 거치는 동안 애플리케이션이 보는 원격 주소도 사용자 주소가 아니라 중간 네트워크 주소로 바뀌었습니다.
이 상태에서는 특정 요청이 어느 사용자와 접속지에서 시작됐는지 설명할 수 없고, 이상 행위가 특정 접속 범위에 몰렸는지도 찾기 어렵습니다. 감사 화면에 이벤트가 존재하더라도 핵심 필드가 비어 있으면 고객 관점에서는 절반짜리 기록입니다. 특히 한국과 일본의 앞단 구성이 달라 한쪽에서 맞는 전달 규칙을 다른 쪽에 그대로 적용할 수 없었습니다.
애플리케이션에서 원격 주소를 읽는 것만으로는 이미 네트워크에서 사라진 값을 복원할 수 없습니다. 전달 헤더에는 원래 주소가 남을 수 있지만 클라이언트도 같은 이름의 값을 직접 넣을 수 있어, 헤더가 존재한다는 이유만으로 신뢰하면 감사 로그 위조로 이어집니다. 모든 중간 주소나 hop 수를 애플리케이션 코드에 고정하는 대안도 네트워크가 바뀔 때마다 깨지므로 제외했습니다. 신뢰 가능한 첫 진입 지점에서 값을 확정하고 리전 차이는 가장자리에서 흡수해야 했습니다.
검증 기준은 정상 요청에서 주소가 채워지는 것보다 엄격하게 잡았습니다. 두 리전의 비운영 경로를 통과한 뒤 애플리케이션이 같은 형식의 사용자·접속 정보를 받는지, 클라이언트가 가짜 전달 헤더를 넣어도 감사 값으로 채택되지 않는지, 요청이 다른 노드로 전달돼도 내부망 판정이 달라지지 않는지, 토큰이나 비밀 값 없이 필요한 최소 사용자 식별만 전달되는지를 확인했습니다.
같은 기능이라도 진입 경로가 다르면 감사 데이터의 품질이 달라졌습니다.
브라우저 요청이 콘솔 경로에서는 감사 정보와 함께 기록되지만 직접 서비스 경로에서는 네트워크 주소 변환 뒤 사용자와 접속 정보가 비게 됩니다.
왜 애플리케이션 코드만으로 풀 수 없었나
한국과 일본은 요청을 처음 받는 네트워크 구성과 헤더 생성 능력이 달랐습니다. 애플리케이션이 보는 원격 주소는 이미 변환된 뒤였고, 토큰 검증 응답에도 접속 IP는 없었습니다.
모든 내부 대역과 중간 프록시(hop)를 코드에 나열하면 네트워크 구성이 바뀔 때마다 신뢰 규칙이 깨집니 다. 차이는 리전별 진입 지점에서 흡수하고 앱에는 검증된 접속 정보만 넘기는 원칙을 세웠습니다.
애플리케이션에 도착한 뒤에는 원래 접속 주소를 복원할 근거가 부족합니다.
실제 클라이언트 주소가 앞단과 클러스터 주소 변환을 지나며 중간 주소로 바뀌고 프록시 전달 규칙이 없으면 원래 주소가 사라집니다.
한국과 일본의 접속 정보 검증
한국 경로에서는 실제 연결을 관측하는 게이트웨이가 직접 확인한 접속 주소를 새로 설정했습니다. 변화가 잦은 내부 경유지 목록을 계속 관리하는 대신, 통제 가능한 첫 진입 지점에서 값을 확정했습니다.
일본 경로에서는 앞단이 만든 XFF(X-Forwarded-For) 주소 목록을 프록시가 오른쪽부터 해석하고 신뢰하는 내부 프록시 주소를 건너뛴 뒤 실제 주소를 선택했습니다. 서로 다른 방법이지만 이후 애플리케이션이 받는 정보 형식은 동일했습니다.
한국에서는 처음 신뢰할 수 있는 진입 지점이 직접 관측한 값을 이후 경유지와 분리했습니다.
한국 게이트웨이가 실제 연결 주소로 확인한 접속 주소를 설정하고 네트워크 변환 뒤에도 애플리케이션까지 유지해 감사용 사용자·접속 정보를 만듭니다.
일본에서는 XFF 전체를 믿지 않고 신뢰할 수 있는 네트워크 지점을 기준으로 실제 주소를 선택했습니다.
일본 앞단이 XFF 목록을 만들고 애플리케이션 프록시가 오른쪽부터 신뢰하는 프록시 주소를 건너뛰어 확인된 사용자 접속 주소를 전달합니다.
애플리케이션은 리전 분기 없이
인증 필터가 검증된 접속 값을 요청 단위 정보에 넣고, 내부 호출을 담당하는 코드가 다음 서비스로 전달했습니다. 감사 기록 계층은 접속 정보가 없을 때의 기존 대체 처리도 유지하면서 같은 정보만 읽습니다.
리전 분기를 비즈니스 코드에 두지 않았기 때 문에 이후 경로가 추가돼도 진입 지점의 변환 로직만 바꾸면 됩니다.
리전 차이는 사라지고 사용자와 접속 정보가 하나의 요청 단위 정보로 흐릅니다.
검증된 접속 값과 사용자가 인증 정보에 합쳐져 내부 호출을 거쳐 감사 이벤트와 저장소로 전달됩니다.
XFF 위조를 실험으로 확인하기
신뢰 규칙은 문서만 보고 안전하다고 판단하지 않았습니다. 테스트 클라이언트가 가짜 XFF 값을 앞에 넣은 상태로 전체 경로를 통과하게 하고 최종 감사 값이 무엇인지 확인했습니다.
프록시는 XFF 목록의 오른쪽에서 신뢰할 수 있는 내부 경유지를 제외하고, 그 밖에서 직접 넣은 값은 채택하지 않았습니다. 직접 주입한 값이 감사 로그에 들어가지 않는 것을 확인했습니다.
보안 가정은 구성 설명이 아니라 실제 위조 요청의 결과로 검증했습니다.
테스트 클라이언트가 가짜 XFF를 넣어도 신뢰 앞단이 관측 주소를 추가하고 프록시가 클라이언트가 직접 넣은 값을 무시해 검증된 주소만 기록합니다.
내부망 판정의 숨은 가정
검증 중 요청이 어느 노드로 전달되느냐에 따라 내부망 판정이 달라지는 문제도 찾았습니다. 특정 사설 주소 접두어 하나만 내부망으로 보는 코드가 다른 표준 사설 범위를 외부로 오판했습니다.
구체적인 클러스터 주소를 나열하는 대신 표준 사설 주소 범위 전체를 판정하게 바꾸고 노드가 달라지는 요청을 반복해 결과가 같음을 확인했습니다.
환경의 한 주소 형태를 전체 내부망 규칙으로 일반화하면 확장에서 깨집니다.
내부 요청이 서로 다른 사설 주소 범위로 관측돼도 표준 사설 범위 판정으로 같은 결과를 냅니다.
사용자 정보도 같은 방식으로 전달하기
접속 주소를 채운 뒤에도 일부 이 벤트의 사용자 식별이 비어 있었습니다. 이미 권한 확인 과정에서 얻고 있던 사용자 정보를 검증 응답에 포함하고 요청이 끝날 때까지 보관해 내부 호출로 전달했습니다.
토큰 원문이나 키는 감사 이벤트에 남기지 않았습니다. 감사에 필요한 최소 사용자 식별자만 전달하고 비밀 값은 전체를 숨기는 원칙을 적용했습니다.
기존 권한 검증 결과를 재사용해 비밀 값 없이 사용자를 식별했습니다.
서비스가 권한 검증에서 감사에 필요한 최소 사용자 식별자를 받아 내부 API로 전달하고 감사 로그에 사용자와 접속 정보를 기록합니다.
검증과 배운 점
한국과 일본의 비운영 경로에서 정상 요청, 여러 값이 담긴 XFF와 직접 주입 요청을 비교했습니다. 리전마다 다른 앞단 구현을 거쳐도 애플리케이션은 같은 감사 기록 정보를 받았고 위조 값은 채택되지 않았습니다.
네트워크에서 이미 사라진 값은 애플리케이션 코드로 복원할 수 없습니다. 코드로 풀 문제와 네트워크 진입 지점에서 풀 문제를 먼저 구분하고, 차이는 가장자리에서 흡수해야 내부 설계를 단순하게 유지할 수 있습니다.