서버가 쿠버네티스 환경으로 이전하게 되면서부터 client ip 추출 이슈가 발생하기 시작했습니다.
203.0.113.1 와 같이 클라이언트의 공인 아이피가 잘 추출되었는데 서버 환경이 바뀌면서부터 203.0.113.1, 192.168.1.100 와 같이 두 개의 아이피가 추출되는 현상이였습니다.
아이피 추출 코드
String clientIp = request.getHeader("X-Forwarded-For");
기존 서버 구성은 다음과 같았습니다.
새로운 서버 구성은 다음과 같습니다.
| 문제의 원인
쿠버네티스 환경으로 이전하게 되면서 web server 앞단에 k8s proxy 가 추가되었습니다.
client에서 L4 스위치로 패킷이 전달되면 L4 스위치는 X-Forwarded-For 헤더에 클라이언트 아이피를 추가합니다.
L4 스위치에서 k8s proxy로 패킷이 전달되면 k8s proxy는 X-Forwarded-For 헤더에 L4 스위치의 아이피를 추가합니다.
k8s proxy에서 web server로 패킷이 전달되면 X-Forwarded-For 헤더의 값을 추출하여 사용합니다. 이 때 두 개의 아이피가 출력되어 문제가 발생하게 됩니다.
여기서 잠시 X-Forwarded-For에 대해서 알아보겠습니다.
X-Forwarded-For 개요
X-Forwarded-For(XFF) 헤더는 HTTP 프록시나 로드 밸런서를 통해 웹 서버에 접속하는 클라이언트의 IP 주소를 식별하는 표준 헤더입니다.
대부분의 기업에서는 웹 서버를 외부로 공개하기 보다는 앞단에 로드 밸런서를 두고 서비스 하는 경우가 많습니다.
헤더의 형태는 아래와 같으며, 거쳐가는 장비들의 주소가 더해집니다.
X-Forwarded-For: client, proxy1, proxy2
테스트 해보기
아래 코드를 이용하여 테스트를 해보았습니다.
request.getHeader("X-Forwarded-For");
주어진 값 | 출력 값 |
X-Forwarded-For: 203.0.113.1 | 203.0.113.1 |
X-Forwarded-For: 203.0.113.1 X-Forwarded-For: 192.168.1.100 |
203.0.113.1, 192.168.1.100 |
X-Forwarded-For: 203.0.113.1 X-Forwarded-For: 192.168.1.100 X-Forwarded-For: 192.168.1.200 |
203.0.113.1, 192.168.1.100, 192.168.1.200 |
X-Forwarded-For 값 넘겨주지 않음 | null |
| 해결 방법
웹 서버에 RemoteIpValve 설정을 추가하면 됩니다.
RemoteIpValve는 Tomcat이 프록시 뒤에 배포될 때 클라이언트의 실제 IP를 추출해 주는 도구입니다.
Tomcat에서는 RemoteIpValve 설정을 활성화 시켜줘야 하며, 스프링부트 Embedded Tomcat 을 사용하면 server.tomcat.remote_ip_header 등의 설정이 등록되어 있을 때 자동으로 활성화됩니다.
이후 clientIp 를 추출 할 때에는 request.getHeader("X-Forwarded-For")가 아닌 requets.getRemoteAddr()을 사용해야 합니다.
테스트 해보기
아래 코드를 이용하여 테스트를 해보았습니다.
requets.getRemoteAddr();
주어진 값 | 출력 값 |
X-Forwarded-For: 203.0.113.1 | 203.0.113.1 |
X-Forwarded-For: 203.0.113.1 X-Forwarded-For: 192.168.1.100 |
203.0.113.1 |
X-Forwarded-For: 203.0.113.1 X-Forwarded-For: 192.168.1.100 X-Forwarded-For: 192.168.1.200 |
203.0.113.1 |
RemoteIpValve 설정을 추가한 뒤로 클라이언트 아이피 한 개만 잘 추출되고 있습니다.
| IP Spoofing(속임수) 가정해 보기
만약에 클라이언트가 자신의 아이피를 속이기 위해 X-Forwarded-For 헤더에 203.0.113.2 를 심어 놨다면 어떻게 될까요?
L4, k8s proxy 서버를 경유해서 web server에 도달하게 되면 X-Forwarded-For 헤더에는 다음과 같이 아이피 정보들이 담겨 있게 됩니다.
X-Forwarded-For: 203.0.113.2, 203.0.113.1, 192.168.1.100
203.0.113.2 아이피는 속임수 아이피이고, 203.0.113.1 아이피는 L4가 추출한 실제 client 아이피가 됩니다. 192.168.1.100는 k8s proxy가 추출한 L4 스위치의 아이피가 됩니다.
RemoteIpValve 설정이 적용되어 있는 상태에서 requets.getRemoteAddr(); 코드를 이용하여 아이피를 추출하면 어떤 값이 나올까요?
신기하게도 203.0.113.2 아이피가 아닌 203.0.113.1 아이피가 추출됩니다. 가장 앞에 있는 아이피가 클라이언트 아이피라면 203.0.113.2가 추출되어야 할텐데 왜 203.0.113.1 값이 추출되었을까요? 굉장히 궁금해집니다.
| RemoteIpValve 동작 원리
RemoteIpValve는 X-Forwarded-For 헤더에 있는 아이피를 뒤에서 부터 읽습니다.
X-Forwarded-For 헤더의 값이 203.0.113.2, 203.0.113.1, 192.168.1.100 와 같다면 가장 뒤에 있는 192.168.1.100를 추출합니다.
이후 해당 아이피가 사설 아이피라면 제거하게 됩니다. (내부 proxy 서버들은 대부분 사설 아이피로 설정합니다.)
203.0.113.2, 203.0.113.1 두 개의 값이 남았습니다.
그 다음 203.0.113.1 아이피를 추출합니다. 해당 아이피는 사설 아이피가 아니므로 클라이언트의 아이피로 판단하게 됩니다.
이처럼 X-Forwarded-For 헤더의 값을 뒤에서부터 읽어나가면서 신뢰 가능한 프록시 사설 아이피는 제거하고 나오는 첫 번째 IP를 클라이언트 아이피로 보는 것 입니다.
이와 같이 처리하면 IP Spoofing 공격을 방어 할 수 있기 때문에 뒤에서 부터 읽어나가도록 처리 한 것 같습니다.
ServerProperties.java 파일을 보시면 아래와 같이 신뢰된 프록시 아이피가 등록되어 있습니다. 전부 사설 아이피 또는 로컬 호스트 주소입니다.
private String internalProxies = "10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|192\\.168\\.\\d{1,3}\\.\\d{1,3}|169\\.254\\.\\d{1,3}\\.\\d{1,3}|127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|172\\.1[6-9]{1}\\.\\d{1,3}\\.\\d{1,3}|172\\.2[0-9]{1}\\.\\d{1,3}\\.\\d{1,3}|172\\.3[0-1]{1}\\.\\d{1,3}\\.\\d{1,3}|0:0:0:0:0:0:0:1|::1";
사설 아이피 대역
10.x.x.x
192.168.x.x
172.16.x.x ~ 172.31.x.x
로컬 대역
169.254.x.x
127.x.x.x, 0:0:0:0:0:0:0:1, ::1:
만약 web server 앞단의 프록시 서버가 공인 아이피로 되어 있다면 internalProxies에 해당 공인 아이피를 추가해 주면 됩니다. 추가 할 때 유의 사항은 proxy의 공인 아이피만 추가하는 것이 아니라 위에 등록된 기본 사설, 로컬 아이피와 함께 등록하셔야 합니다.