개발자

docker container 내부의 spring boot 서버에서 client 의 ip 주소 알아내는 방법

2024년 03월 19일조회 1,217

안녕하세요 지금 만들고 있는 서비스가 설치형 어플리케이션에, 리눅스 파일 시스템을 사용해야해서 처음부터 docker 환경으로 spring boot 서버 환경을 세팅하고 시작했습니다. 순조롭게 진행하다가 한 부분에서 막히게 되었는데요, spring boot 서버 내에서 현재 요청한 클라이언트의 ip를 기존에 등록한 아이피와 비교하여 다른 아이피일 경우 요청을 거부하는 보안 로직을 구현해야하는 부분입니다. 문제가 되는 부분은 요청한 클라이언트의 ip를 알아내는 부분인데요 원래 하던것과 같이 HttpServletRequest 객체에서 getRemoteAddr() 메소드를 호출하여 아이피를 출력해보니 클라이언트의 아이피가 아닌 다른 아이피가 잡히는겁니다. 정황상 host 에서 컨테이너로 포트포워딩을 하다보니 본래 클라이언트의 아이피가 아니라 Docker 네트워크의 아이피가 나온 것 같습니다. 혹시라도 포워딩 헤더가 있을까 하여 헤더를 까봤지만 헤더에 있지도 않더라구요.. GPT에게 자문을 구해보니 두가지를 추천해주더군요. 1. container의 network를 host로 설정하라 이건 알아보니 리눅스에서만 작동하는거랍니다 제 어플리케이션은 리눅스에 설치될수도 있고 윈도우에 설치될 수도 있는데 말이죠.. 2. Nginx 같은 프록시 서버를 둬라 호스트에 프록시를 둬서 본래의 아이피를 헤더에 추가하든 어떻게든 해서 본래의 아이피를 스프링에 전달해주라는 말로 이해했습니다. 플랫폼 독립적으로 설치하기 위해서 Docker를 채용했는데 호스트에 추가적인 서버를 설치하라뇨… 이것도 좀 아닌 것 같습니다.. 막막합니다… 괜히 Docker 들여왔나 싶기도 하고, 보안 인증때문에 이 기능을 지원 안할수도 없고.. 도와주십쇼..!

이 질문이 도움이 되었나요?
'추천해요' 버튼을 누르면 좋은 질문이 더 많은 사람에게 노출될 수 있어요. '보충이 필요해요' 버튼을 누르면 질문자에게 질문 내용 보충을 요청하는 알림이 가요.

답변 1

인기 답변

장성호님의 프로필 사진

두 가지 방법 중 하나를 선택해야할 것 같습니다. 1. Docker NAT가 TCP 소켓 정보를 처리하기 전에 미리 클라이언트 IP 정보를 따서 Header에 남겨둔다. (X-Forwarded-For) 2. Docker NAT를 거치지 않고 트래픽을 서버에다가 직접 넘겨준다. 작성자님이 겪고 계신 문제점의 원인입니다. 1. HttpServletRequest 객체에서 getRemoteAddr() 메소드는 TCP 소켓의 클라이언트 IP 정보를 가져옵니다. 2. Docker는 컨테이너를 외부와 연결하기 위해 NAT를 사용합니다. 해당 과정에서 TCP 소켓 정보 중 클라이언트 IP 정보가 호스트의 IP 주소로 마스킹 됩니다. 그리고 작성자님이 남겨주신 1번, 2번 방법에 대해서도 알아본 바입니다. 1. container network를 host로 설정하라. host 네트워크 모드를 사용하면 컨테이너가 호스트의 네트워크 네임스페이스를 직접 사용합니다. 그래서 NAT 없이 트래픽을 직접 처리할 수 있습니다. 원인 2번이 진행되지 않으므로 TCP 소켓에서도 클라이언트 IP 원본을 알 수 있습니다. 2. Nginx 같은 프록시 서버를 둬라 이 방식은 방법 1번입니다. 도커 NAT가 클라이언트 IP 원본을 호스트 IP로 마스킹하기 전에 미리 HTTP Header에 추가하는 방법입니다. 그렇다보니 Docker 외부에다가 만들어야 합니다. 첨부한 건 문제점 원인 1번에 대한 내부 구현입니다.

1/**
2* 흐름 중 가장 중요한 것은 아래와 같습니다. 
3* HttpServletRequest 클래스의 getRemoteAddr() 함수는 TCP 소켓으로부터 remoteAddr을 가져옵니다.
4* 
5* 1. HttpServletRequest
6* 2. org.apache.catalina.connector.Request
7* 3. org.apache.coyote.Request
8* 4. org.apache.coyote.AbstractProcessor
9* 5. org.apache.tomcat.util.net.NioSocketWrapper
10*/
11
12package org.apache.catalina.connector;
13
14/**
15 * HttpServletRequest 구현체입니다.
16 */
17public class Request implements HttpServletRequest {
18	protected String remoteAddr = null;
19
20	@Override
21	public String getRemoteAddr() {
22	    if (remoteAddr == null) {
23	        coyoteRequest.action(ActionCode.REQ_HOST_ADDR_ATTRIBUTE, coyoteRequest);
24	        remoteAddr = coyoteRequest.remoteAddr().toString();
25	    }
26	    return remoteAddr;
27	}
28}
29
30package org.apache.coyote;
31
32/**
33 * Request 인데 왜 분리되어 있는 건지는 아직 모르겠네요
34 */
35public final class Request {
36	// remote address/host
37	private final MessageBytes remoteAddrMB = MessageBytes.newInstance();
38	
39	public MessageBytes remoteAddr() {
40       return remoteAddrMB;
41   }
42}
43
44package org.apache.tomcat.util.buf;
45
46/**
47 * HTTP 헤더나 쿠키 같은 메세지 데이터를 다루기 위해 사용하는 클래스입니다.
48 */
49public final class MessageBytes {
50
51}
52
53package org.apache.coyote;
54
55/**
56 * HTTP 요청을 파싱하고 처리 과정을 디자인합니다.
57 */
58public abstract class AbstractProcessor extends AbstractProcessorLight implements ActionHook {
59	protected final org.apache.coyote.Request request;
60	
61	/**
62	 * 소켓 정보를 끌고 와서 소켓의 remote addr를 request remote addr에 추가합니다.
63	 */
64	@Override
65  public final void action(ActionCode actionCode, Object param) {
66		// Request attribute support
67		case REQ_HOST_ADDR_ATTRIBUTE: {
68		    if (getPopulateRequestAttributesFromSocket() && socketWrapper != null) {
69		        request.remoteAddr().setString(socketWrapper.getRemoteAddr());
70		    }
71		    break;
72		}
73	}
74}
75
76package org.apache.tomcat.util.net;
77
78/**
79* 소켓 정보에 관한 base 클래스 입니다.
80*/
81public abstract class SocketWrapperBase<E> {
82	public String getRemoteAddr() {
83	    if (remoteAddr == null) {
84	        populateRemoteAddr();
85	    }
86	    return remoteAddr;
87	}
88	
89	protected abstract void populateRemoteAddr();
90}
91
92package org.apache.tomcat.util.net;
93
94/**
95* 소켓 구현체 중 하나입니다.
96*/
97public static class NioSocketWrapper extends SocketWrapperBase<NioChannel> {
98
99	/**
100	* 소켓에서 가장 유명한 remote addr을 가져옵니다.
101	*/
102	@Override
103	protected void populateRemoteAddr() {
104	    SocketChannel sc = getSocket().getIOChannel();
105	    if (sc != null) {
106	        InetAddress inetAddr = sc.socket().getInetAddress();
107	        if (inetAddr != null) {
108	            remoteAddr = inetAddr.getHostAddress();
109	        }
110	    }
111	}
112}
113
114package java.net;
115
116/**
117* 소켓 구현체입니다.
118*/
119public class Socket implements java.io.Closeable {
120	/**
121	* IP address를 가져옵니다.
122	*/
123	public InetAddress getInetAddress() {
124	    if (!isConnected())
125	        return null;
126	    try {
127	        return getImpl().getInetAddress();
128	    } catch (SocketException e) {
129	    }
130	    return null;
131	}
132}
133
134package java.net;
135
136/**
137	* 소켓 구현체입니다.
138	*/
139public abstract class SocketImpl implements SocketOptions {
140	protected InetAddress getInetAddress() {
141	    return address;
142	}
143}
144
145package java.net;
146
147/**
148	* IP addres 구현체입니다.
149	*/
150public sealed class InetAddress implements Serializable permits Inet4Address, Inet6Address {
151	int address;
152
153	int getAddress() {
154		return address;
155	}
156}
이정윤님의 프로필 사진

이정윤

작성자

소프트웨어 개발자2024년 03월 19일

결국 두 방법 말고는 답이 없군요 ㅠㅠ

장성호님의 프로필 사진

장성호

현대오토에버 백엔드2024년 03월 19일

프록시가 제일 안전할 거 같아요ㅠㅠ

장성호님의 프로필 사진

장성호

현대오토에버 백엔드2024년 03월 19일

인프라 하는 친구가 iptables 건들면 해결가능할 것 같다고 하네요!?

이정윤님의 프로필 사진

이정윤

작성자

소프트웨어 개발자2024년 03월 19일

그런데 그것도 결국엔 리눅스에서만 가능한거겠죠..? 저는 윈도우에서도 작동이 되야하는데 말이죠 ㅠ 좀 생각을 해봤는데 스프링 부트(자바)자체도 결국엔 os를 타지 않도록 만들어졌으니.. 스프링을 컨테이너 밖으로 빼서 호스트에서 실행을 시키고 리눅스 파일시스템이 필요한 부분만 docker-java 같은 api로 실행시키면 되지 않을까 하네요. 내일 출근해서 시도해보렵니다..ㅎ 답변 주셔서 감사합니다.

장성호님의 프로필 사진

장성호

현대오토에버 백엔드2024년 03월 19일

윈도우가 어렵군요🥲 transparent 모드로 해서 컨테이너가 외부 IP를 할당받을 수 있도록 하면 NAT를 거치지 않는다고 하네요! 도움이 됐으면 좋겠습니다

지금 가입하면 모든 질문의 답변을 볼 수 있어요!

현직자들의 명쾌한 답변을 얻을 수 있어요.

또는

이미 회원이신가요?

AI 답변
커리어리 AI 봇 프로필 이미지

커리어리 AI 봇

GPT가 달아주는 답변입니다.

2024년 03월 19일

해당 문제에 대해서 몇 가지 가능한 해결책을 제시드리겠습니다. 1. `X-Forwarded-For` 헤더 사용하기 Spring Boot 어플리케이션은 기본적으로 `X-Forwarded-*` 헤더를 인식하지 않습니다. 따라서 Docker를 사용할 때 이러한 현상이 발생하곤 합니다. 하지만, Spring Boot가 프록시 서버 뒤에서 실행될 때 이용자의 실제 IP 주소를 얻기 위해 'X-Forwarded-For' 헤더를 사용할 수 있도록 설정할 수 있습니다. ```java @Bean public FilterRegistrationBean<Filter> filterRegistrationBean() { FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>(); ForwardedHeaderFilter filter = new ForwardedHeaderFilter(); registrationBean.setFilter(filter); registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE); return registrationBean; } ``` 위의 코드는 Spring Boot에서 X-Forwarded-* 헤더를 사용하도록 설정하는 코드입니다. 즉, 웹 서버나 리버스 프록시가 클라이양트의 실제 IP 주소와 함께 `X-Forwarded-*` 헤더를 추가하여 요청을 전달하면, 해당 정보를 활용하여 클라이언트의 실제 IP 주소를 알아낼 수 있습니다. 2. AJP 프로토콜 사용하기: Tomcat 같은 서버가 별도로 존재한다면, AJP 프로토콜을 사용하는 것이 좋다. 이 기법은 원격 IP를 포함하는 요청을 프록시와 애플리케이션 서버 간에 전송할 수 있습니다. 3. Docker 네트워크 모드: Docker compose를 사용하신다면, network_mode를 'host' 로 설정하는 것은 어떨까요? 다만 이 방법은 Docker의 격리 기능을 손상시킬 수 있고 보안 문제가 생길수 있으니 주의해야 합니다. 위의 해결책 중에서 가장 적합한 것을 선택하여 적용해보세요. 해당 시나리오는 일반적인 상황을 가정하고 제안된 것으로, 특정 환경에 따라 결과가 다르게 나올 수 있습니다.

목록으로

지금 가입하면 모든 질문의 답변을 볼 수 있어요!