새로 투입된 공공기관 프로젝트에서 배포 직전에 문제가 생겼습니다. 앱 서버에서 SMS 발송 API를 호출하는데 연결이 안 됩니다. 개발 환경에서는 잘 됐는데 운영 환경은 "망 분리 구성"이라고 합니다. "Proxy 서버를 통해서 연결해야 해요"라는 말을 들었지만 — 환경변수를 설정해도 Java 앱은 여전히 실패하고, curl은 된다고 하는데 이유를 모르겠습니다.
이 모듈에서는 폐쇄망 구조와 Proxy/NAT의 동작 원리를 이해하고, Squid Proxy 설정과 Java 포함 다양한 클라이언트에서 Proxy를 사용하는 방법을 다룹니다.
- 1Forward Proxy와 Reverse Proxy의 방향 차이를 구분하고 설명할 수 있다
- 2Squid Proxy의 기본 설정(http_port, acl, http_access)을 작성하고 동작을 확인할 수 있다
- 3curl, Java JVM, 시스템 환경변수 각각에서 Proxy를 설정하는 방법을 적용할 수 있다
- 4SNAT/DNAT 차이를 이해하고 iptables로 포트 포워딩을 설정할 수 있다
- 5NO_PROXY 설정으로 내부 통신이 Proxy를 우회하도록 구성할 수 있다
망 분리 환경의 통신 구조
폐쇄망에서 외부로 나가는 트래픽 흐름
보안 요구가 높은 공공기관이나 금융 환경에서는 내부 서버가 인터넷에 직접 접속할 수 없도록 망을 분리합니다. 이 구조에서 외부 API를 호출하려면 허가된 Proxy 서버를 통해야만 트래픽이 흘러나갈 수 있습니다. 내부망, DMZ, 인터넷 구간 사이의 흐름을 이해하지 못하면 방화벽 오픈 요청도, Proxy 설정도 어디서 막혔는지 진단조차 하기 어렵습니다.
확대
공공기관 운영 서버에서 SMS API를 호출했더니 "연결 거부"가 납니다. 개발 서버에서는 됐는데 운영만 안 됩니다. 방화벽을 확인했더니 "직접 외부 통신은 차단, Proxy를 통해야 한다"고 합니다. 망 분리 환경을 처음 접하는 순간입니다. 어떤 경로로 트래픽이 흘러야 하는지를 먼저 이해해야 Proxy 설정도, 방화벽 오픈 요청도 제대로 할 수 있습니다.
망 분리는 보안 요구사항이 높은 환경(금융, 공공기관, 병원 등)에서 내부 업무망과 인터넷 구간을 물리적 또는 논리적으로 분리하는 구성입니다. 내부 서버는 인터넷에 직접 접속할 수 없고, 허가된 경로(Proxy 서버)를 통해서만 외부 통신이 가능합니다.
3계층 망 분리 구조:
확대
Proxy 서버가 내부 서버 대신 외부 API를 호출합니다. 외부 API 서버 입장에서는 Proxy 서버의 IP만 보이므로 Proxy IP를 외부 기관에 IP 화이트리스트로 등록합니다.
내부 패킷이 프록시·NAT를 지나 외부로 나갔다 오는 법
앞에서 폐쇄망의 큰 그림을 봤다면, 이번엔 내부 서버가 쏜 패킷 하나가 프록시 또는 NAT 게이트웨이를 지나 외부로 나갔다가 돌아오는 경로를 단계로 봅니다. 나가는 길이 두 가지(애플리케이션 레벨 프록시 · L3 레벨 NAT)라, "외부가 안 된다"를 진단할 때 어느 경로인지부터 갈라야 원인이 보입니다.
내부 서버(192.168.10.11)가 외부 API를 호출 — 나가는 경로는 둘
[A] 포워드 프록시(Squid) 경로 ── 앱이 프록시를 알 때(HTTP)
① 내부 서버 → Squid(3128)로 요청 (HTTPS면 CONNECT 터널)
② Squid가 ACL 검사 · 필요 시 프록시 인증 → 캐시 있으면 캐시로 응답
③ 없으면 Squid가 자기 공인 IP로 외부 API에 대신 접속
④ 외부 응답 → Squid → 내부 서버 (외부엔 Squid 공인 IP만 보임)
[B] NAT 게이트웨이 경로 ── L3에서 기본 게이트웨이로 그냥 나갈 때
① 내부 서버 → 기본 게이트웨이로 송신 (출발지 = 사설 IP)
② 게이트웨이가 conntrack에 연결 기록 + SNAT: 출발지를 공인IP:임시포트로 변환
③ 변환된 공인 IP로 외부 API에 도달
④ 응답이 공인IP:임시포트로 회신 → conntrack 역참조 → 원래 사설 IP로 역변환
두 경로의 단계별 확인 지점과 막히는 증상:
| 단계 | 하는 일 | 여기서 막히면 |
|---|---|---|
| 경로 결정 | 앱이 프록시 설정을 읽고 [A]로 갈지, 아니면 [B]로 그냥 나갈지 | Java가 -Dhttp.proxyHost 없이 http_proxy만 → 프록시 무시하고 직접 나가려다 방화벽에 막혀 실패 |
| [A]② ACL·인증 | Squid가 목적지 허용·프록시 인증 확인 | ACL 미허용 → TCP_DENIED/403 · 인증 필요한데 자격 없음 → 407 Proxy Authentication Required |
| [A]② 캐시 조회 | 캐시 히트면 외부로 안 나감 | 캐시하면 안 되는 API 응답이 캐시됨 → 낡은 응답 (그래서 API는 no_cache) |
| [B]② SNAT 변환 | 출발지를 공인IP:임시포트로 바꿔 다중화 | 동시 연결 폭증으로 임시포트 고갈 → 신규 연결만 실패 (아래 심화 참조) |
| [B]② conntrack 기록 | 연결 상태를 테이블에 적어 응답을 되돌릴 근거 확보 | conntrack 테이블 가득 참 → nf_conntrack: table full, dropping packet |
| 응답·우회 | 응답을 원래 사설 IP로 역변환/회신 | no_proxy 미설정 → 내부 통신까지 프록시 경유해 불필요한 지연 |
정리하면 "외부가 안 된다"는 먼저 어느 경로인지부터 가릅니다. [A] 프록시 경로면 Squid 로그가 어디서 막혔는지 알려주고(TCP_DENIED=ACL, 407=인증), curl은 되는데 Java만 실패하면 경로 결정 단계(JVM 프로퍼티 누락)입니다. [B] NAT 경로면 평소엔 되다가 특정 시간대(동시 호출 폭증)에만 신규 연결이 실패하는 패턴이 SNAT 임시포트·conntrack 고갈의 신호입니다. 그리고 외부에서 보이는 출발지는 늘 Squid·게이트웨이의 공인 IP 하나 — 그래서 외부 기관 화이트리스트엔 그 IP만 등록하면 되고, 동시에 그 한 IP의 포트가 유한 자원이 됩니다(아래 심화로 이어집니다).
Squid Proxy 설치와 설정
Squid — Forward Proxy의 대표 소프트웨어
폐쇄망 환경에서 내부 서버가 특정 외부 API에만 접근하도록 제어하고 감사 로그를 남겨야 할 때, 가장 빠르게 구축할 수 있는 선택이 Squid입니다. ACL(Access Control List)로 허용 목적지를 세밀하게 제한하고, 어떤 서버가 어느 URL을 호출했는지 실시간으로 추적할 수 있어 보안 감사 요구사항을 충족하기에 적합합니다. 오픈소스이면서도 운영 환경에서 충분히 검증된 소프트웨어로, 국내 공공·금융 프로젝트에서 표준적으로 사용됩니다.
확대
Proxy 서버를 구축해달라는 요청을 받았습니다. 폐쇄망 내부 서버들이 특정 외부 기관 API에만 접근하도록 제어해야 하고, 어떤 서버가 어느 URL을 얼마나 호출하는지 감사 로그도 남겨야 합니다. 이 요구사항을 가장 빠르고 실용적으로 구현하는 도구가 Squid입니다.
Squid는 Linux에서 가장 널리 쓰이는 Forward Proxy 소프트웨어입니다. HTTP/HTTPS 요청을 대신 수행하고, ACL(Access Control List)로 어떤 클라이언트가 어떤 목적지에 접근할 수 있는지 제어합니다. 캐싱 기능도 있어 동일 외부 리소스 반복 요청 시 성능을 높입니다.
Squid 설치:
# RHEL/CentOS
sudo dnf install -y squid
# Ubuntu/Debian
sudo apt-get install -y squid
# 서비스 시작 및 자동 시작 등록
sudo systemctl enable --now squid
# 버전 및 상태 확인
squid -v
sudo systemctl status squid
기본 설정 파일 (/etc/squid/squid.conf):
# 핵심 설정 항목만 추출
grep -v '^#' /etc/squid/squid.conf | grep -v '^$'
운영 환경에 맞는 squid.conf 설정:
# /etc/squid/squid.conf
# -- Proxy 수신 포트 --
http_port 3128
# -- ACL 정의 --
# 내부 네트워크 대역 정의
acl localnet src 192.168.0.0/16
acl localnet src 10.0.0.0/8
acl localnet src 172.16.0.0/12
# 허용할 목적지 도메인/IP (외부 기관 API 서버)
acl allowed_destinations dstdomain .sms-gateway.example.com
acl allowed_destinations dstdomain .cert-authority.go.kr
acl allowed_destinations dst 203.x.x.x/32 # 특정 IP만 허용 시
# HTTP/HTTPS 포트
acl SSL_ports port 443
acl Safe_ports port 80
acl Safe_ports port 443
# -- 접근 제어 규칙 --
# 위에서 아래로 순서대로 평가됨 (첫 매칭에서 결정)
http_access deny !Safe_ports
http_access allow localnet allowed_destinations
http_access deny all # 나머지는 모두 차단
# -- HTTPS CONNECT 터널 허용 --
# HTTPS 요청 시 CONNECT 메서드로 터널 요청이 옴
http_access allow CONNECT SSL_ports
# -- 로그 --
access_log /var/log/squid/access.log squid
cache_log /var/log/squid/cache.log
# 캐시 비활성화 (API 서버 연동 시 캐시 불필요한 경우)
no_cache deny all
# Proxy 헤더에 내부 IP 노출 방지
forwarded_for delete
via off
# 설정 파일 문법 검증
sudo squid -k parse
# 설정 재로드 (서비스 중단 없이)
sudo squid -k reconfigure
# 또는
sudo systemctl reload squid
클라이언트별 Proxy 설정
curl, Java, Python — 각각 다르게 설정한다
export http_proxy=...를 설정했더니 curl은 Proxy를 타는데 Java 앱은 여전히 실패합니다. 환경변수를 설정하면 다 되는 줄 알았는데 Java JVM은 다르게 동작합니다. 같은 서버에서도 클라이언트 종류마다 Proxy 설정 방법이 다르다는 것을 모르면 이 문제를 몇 시간 동안 헤맬 수 있습니다.
Proxy를 설정하는 방법은 클라이언트마다 다릅니다. OS 환경변수를 자동으로 읽는 것도 있고, 별도 설정이 필요한 것도 있습니다. 특히 Java JVM은 OS 환경변수를 무시하는 경우가 많아 별도 처리가 필요합니다.
1. 환경변수 방식 (curl, wget, Python requests 자동 적용):
# HTTP/HTTPS Proxy 설정
export http_proxy=http://10.10.1.100:3128
export https_proxy=http://10.10.1.100:3128
export HTTP_PROXY=http://10.10.1.100:3128 # 대문자도 설정 (앱마다 다름)
export HTTPS_PROXY=http://10.10.1.100:3128
# 내부 통신은 Proxy 우회 (매우 중요)
export no_proxy=localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,.internal.company.com
export NO_PROXY=$no_proxy
# 영구 적용 (시스템 전체)
echo 'http_proxy=http://10.10.1.100:3128' | sudo tee -a /etc/environment
echo 'https_proxy=http://10.10.1.100:3128' | sudo tee -a /etc/environment
echo 'no_proxy=localhost,127.0.0.1,10.0.0.0/8' | sudo tee -a /etc/environment
2. curl — 명시적 Proxy 옵션:
# -x 옵션으로 Proxy 지정 (환경변수 무관하게 동작)
curl -x http://10.10.1.100:3128 -v https://sms-gateway.example.com/health
# Proxy 인증이 있는 경우
curl -x http://10.10.1.100:3128 -U proxyuser:proxypass https://api.example.com/
# Proxy 경유 시 연결 과정 상세 확인
curl -x http://10.10.1.100:3128 --trace-ascii /dev/stderr https://api.example.com/ 2>&1 | head -30
3. Java JVM — 환경변수가 아닌 시스템 프로퍼티로 설정:
# WAS 기동 스크립트에 JVM 옵션 추가
JAVA_OPTS="$JAVA_OPTS \
-Dhttp.proxyHost=10.10.1.100 \
-Dhttp.proxyPort=3128 \
-Dhttps.proxyHost=10.10.1.100 \
-Dhttps.proxyPort=3128 \
-Dhttp.nonProxyHosts=localhost|127.0.0.1|10.*|*.internal.company.com"
# Tomcat의 경우 setenv.sh에 추가
# /opt/tomcat/bin/setenv.sh
export CATALINA_OPTS="$CATALINA_OPTS -Dhttp.proxyHost=10.10.1.100 -Dhttp.proxyPort=3128"
# Spring Boot 내장 서버의 경우
java -Dhttp.proxyHost=10.10.1.100 -Dhttp.proxyPort=3128 -jar app.jar
4. Python requests — 코드 또는 환경변수:
import requests
# 코드에서 직접 설정
proxies = {
'http': 'http://10.10.1.100:3128',
'https': 'http://10.10.1.100:3128',
}
response = requests.get('https://api.example.com/', proxies=proxies, timeout=10)
# 또는 환경변수 http_proxy, https_proxy 설정 시 자동 적용
NAT — 주소 변환과 포트 포워딩
SNAT/DNAT와 iptables 포트 포워딩
사설 IP를 쓰는 내부 서버가 인터넷과 통신하는 원리가 궁금할 때, 또는 외부에서 특정 포트로 들어오는 트래픽을 내부 서버로 보내야 할 때 NAT를 만납니다. "왜 내부 서버가 외부 API를 호출할 수 있는 거지?" 또는 "공인 IP 8080으로 들어오는 요청을 내부 WAS로 전달하려면 어떻게 하지?" — 이 두 가지 질문이 SNAT와 DNAT입니다.
NAT(Network Address Translation)는 IP 패킷의 주소를 변환하는 기술입니다. 사설 IP를 쓰는 내부 서버가 인터넷과 통신할 때(SNAT), 또는 외부에서 들어오는 트래픽을 내부 특정 서버로 포워딩할 때(DNAT) 사용합니다.
SNAT — 내부 → 외부 (출발지 변환):
# 내부 서버(192.168.10.0/24)가 eth0(공인 IP)을 통해 외부로 나갈 때
# 출발지 IP를 eth0 IP로 변환 (MASQUERADE: 동적 IP에 적합)
sudo iptables -t nat -A POSTROUTING -s 192.168.10.0/24 -o eth0 -j MASQUERADE
# 정적 공인 IP가 있는 경우 SNAT 명시
sudo iptables -t nat -A POSTROUTING -s 192.168.10.0/24 -o eth0 -j SNAT --to-source 203.x.x.x
# IP 포워딩 활성화 (라우터/NAT 서버에서 필수)
echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward
# 영구 적용
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-ip-forward.conf
sudo sysctl -p
DNAT — 외부 → 내부 포트 포워딩 (목적지 변환):
# 외부에서 이 서버의 8080 포트로 들어오는 트래픽을 내부 WAS로 포워딩
sudo iptables -t nat -A PREROUTING -p tcp --dport 8080 -j DNAT --to-destination 192.168.10.11:8080
# 특정 IP에서 오는 트래픽만 포워딩
sudo iptables -t nat -A PREROUTING -s 203.x.x.x -p tcp --dport 443 -j DNAT --to-destination 192.168.10.11:443
# 현재 NAT 규칙 확인
sudo iptables -t nat -L -n -v
# 규칙 영구 저장 (RHEL/CentOS)
sudo service iptables save
# Ubuntu
sudo netfilter-persistent save
실습 — Proxy 경유 외부 통신 확인
Proxy 환경변수를 설정하고 curl로 외부 API에 접속합니다. -v 옵션으로 Proxy와의 CONNECT 터널 수립 과정을 확인할 수 있습니다.
# Proxy 환경변수 설정
export http_proxy=http://10.10.1.100:3128
export https_proxy=http://10.10.1.100:3128
export no_proxy=localhost,127.0.0.1,10.0.0.0/8
# Proxy 경유 접속 테스트
curl -v https://external-api.example.com/health
# CONNECT 터널 수립 확인 (HTTPS 요청 시)
# 출력에서 다음 라인이 보여야 정상:
# * Connected to 10.10.1.100 (10.10.1.100) port 3128 (#0)
# * Establish HTTP proxy tunnel to external-api.example.com:443
# * Proxy replied 200 to CONNECT request
# Proxy 없이 직접 접속 시도 (차단되어야 정상)
unset http_proxy https_proxy
curl -m 5 https://external-api.example.com/health
# Expected: curl: (28) Connection timed out
export http_proxy=http://proxy-server:3128 && curl -v https://external-api.example.com/health- curl -v 출력에서 먼저 "Connected to 10.10.1.100 port 3128" 메시지로 Proxy 경유 확인, 그 다음 HTTPS이면 "Proxy replied 200 to CONNECT" 확인, 마지막으로 최종 응답 코드 확인 — 이 순서로 Proxy→목적지 경로 검증
- 응답 기준: "Connected to Proxy IP"가 없으면 http_proxy 환경변수 미설정 또는 Proxy 서버 미기동 / "Proxy replied 200 to CONNECT"가 없으면 HTTPS CONNECT 터널 실패 / 최종 200이지만 Squid 로그에 없으면 Proxy를 경유하지 않은 것
- Proxy replied 200인데 최종 응답이 403이면 → Squid ACL이 해당 목적지를 차단 중 — grep TCP_DENIED /var/log/squid/access.log | grep 목적지URL 로 차단 규칙 확인
Squid 로그에서 각 요청의 상태를 확인합니다. TCP_MISS는 캐시 미스(실제 외부 요청 수행), TCP_DENIED는 ACL에 의해 차단된 요청입니다.
# Squid 액세스 로그 실시간 확인
tail -f /var/log/squid/access.log
# 로그 형식:
# 타임스탬프 응답시간 클라이언트IP 결과코드/HTTP상태 바이트 메서드 URL 계층정보
# 1748556000.123 342 192.168.10.11 TCP_MISS/200 1234 CONNECT external-api.example.com:443 - DIRECT/203.x.x.x -
# 차단된 요청 필터링
grep TCP_DENIED /var/log/squid/access.log | tail -20
# 특정 클라이언트 요청 추적
grep 192.168.10.11 /var/log/squid/access.log | tail -20
tail -f /var/log/squid/access.log- Squid 로그에서 먼저 TCP_MISS/200 항목으로 외부 요청 성공 확인, 그 다음 TCP_DENIED 항목으로 차단 규칙 동작 확인 — TCP_MISS는 캐시 없이 외부 직접 요청(정상), TCP_HIT는 캐시 응답
- 로그 코드 기준: TCP_MISS/200=정상 외부 요청 성공, TCP_DENIED/403=ACL 차단(허용 목록 추가 필요), TCP_MISS/502=Proxy가 외부 서버 연결 실패(외부 망 연결 점검), NONE/400=요청 형식 오류
- curl에서 200이 나오는데 Squid 로그에 기록이 없으면 → no_proxy 설정이 해당 목적지를 포함하거나 직접 연결 중 — echo $no_proxy 로 제외 목록 확인, curl -v 에서 "Connected to Proxy IP" 없으면 직접 연결 상태
트러블슈팅
원인: Java JVM은 OS 수준의 http_proxy 환경변수를 자동으로 읽지 않습니다. export http_proxy=...는 Shell 환경변수로 curl 같은 일반 프로그램에는 적용되지만, Java 프로세스는 JVM 시스템 프로퍼티(-Dhttp.proxyHost 등)로 Proxy를 설정해야 합니다.
# Java 앱의 실제 JVM 옵션 확인
ps aux | grep java | grep -v grep
# 출력에 -Dhttp.proxyHost 가 없으면 Proxy 미설정 상태
# 확인: curl은 되는가?
curl -x http://10.10.1.100:3128 https://external-api.example.com/health
# → 200 OK이면 Proxy 자체는 정상, Java 설정 문제
# 해결: JVM 옵션 추가
# Tomcat setenv.sh 또는 systemd unit의 Environment= 에 추가
JAVA_OPTS="$JAVA_OPTS \
-Dhttp.proxyHost=10.10.1.100 \
-Dhttp.proxyPort=3128 \
-Dhttps.proxyHost=10.10.1.100 \
-Dhttps.proxyPort=3128 \
-Dhttp.nonProxyHosts=localhost|127.0.0.1|10.*"
# WAS 재시작 후 확인
sudo systemctl restart tomcat
# 앱 로그에서 외부 API 연결 성공 여부 확인
원인: http_proxy를 설정했지만 no_proxy(또는 NO_PROXY)를 설정하지 않아 내부 서버(DB, 캐시 서버, 다른 WAS)로의 요청도 Proxy를 경유합니다. 내부 트래픽이 Proxy→내부 서버로 우회하면서 불필요한 지연이 발생합니다.
# 현재 Proxy 관련 환경변수 확인
env | grep -iE "proxy|no_proxy"
# no_proxy가 없거나 내부 대역이 누락된 경우
# 내부 통신 대역 전체를 no_proxy에 추가
export no_proxy=localhost,127.0.0.1,::1,10.0.0.0/8,192.168.0.0/16,172.16.0.0/12,.internal.company.com
export NO_PROXY=$no_proxy
# Java의 경우 nonProxyHosts (파이프로 구분, CIDR 미지원)
-Dhttp.nonProxyHosts="localhost|127.0.0.1|10.*|192.168.*|*.internal.company.com"
# 확인: 내부 서버로 curl 시 Proxy 미경유 확인
curl -v http://192.168.10.11:8080/health 2>&1 | grep "Connected to"
# 기대 출력: * Connected to 192.168.10.11 (192.168.10.11) port 8080
# (Proxy IP가 아닌 실제 서버 IP로 연결)
심화 — 하나의 공인 IP 뒤에서 포트가 마른다
심화: SNAT의 내부 — PAT 변환 테이블과 임시 포트라는 유한 자원
"사설 IP를 공인 IP로 바꿔 내보낸다"까지는 봤습니다. 그런데 게이트웨이 하나가 수백 대의 내부 서버를 같은 공인 IP로 내보내면서, 돌아오는 응답을 어떻게 원래 서버에 정확히 되돌릴까요? 그 답이 SNAT의 진짜 동작이자 한계입니다.
- 포트로 다중화한다(PAT): 게이트웨이는 나가는 연결마다 자신의
공인IP:임시포트를 하나 배정하고, 변환 테이블에내부IP:내부포트 ↔ 공인IP:임시포트 (목적지)를 적어둡니다. 응답이 그 임시포트로 오면 테이블을 역참조해 원래 내부 서버로 되돌립니다. 그래서 여러 서버가 IP 하나를 공유할 수 있습니다. - 연결의 유일성 조건: 같은 목적지(
같은 IP:같은 포트)로 나가는 동시 연결들은 서로 다른 임시포트를 배정받아야 구분됩니다. 즉 "같은 외부 목적지로의 동시 연결 수"는 사용 가능한 임시포트 수를 넘을 수 없습니다. - 임시포트는 유한: 리눅스
net.ipv4.ip_local_port_range는 보통 3만~6만 개 남짓입니다. 목적지가 하나로 몰릴수록(예: 단일 결제 API로 대량 호출) 이 범위가 빠르게 소진되고, 배정할 포트가 없으면 신규 연결이 실패합니다. - 헤어핀이라는 함정: 내부 서버가 자기 쪽 공인 IP(DNAT로 내부에 매핑된)를 향해 접속하면, 나갔다 되돌아오는 경로가 비대칭이 되어 별도 hairpin NAT 설정이 없으면 연결이 끊깁니다.
핵심은 "공인 IP 하나 = 목적지별 동시 연결 약 6만"이라는 상한이 실재한다는 것입니다. 폐쇄망처럼 모든 외부 트래픽이 게이트웨이 한 점을 지나는 구조에서는 이 상한이 장애로 먼저 드러납니다.
상황: 특정 외부 집계·결제 API로 수천 건을 동시 호출하는 야간 배치가 돌 때만 connect 실패가 산발합니다. 같은 코드가 낮에는 문제없고, 이미 열린 연결도 정상입니다. 방화벽·Proxy 설정은 바뀐 적이 없습니다.
원인: 폐쇄망 게이트웨이가 모든 내부 서버를 하나의 공인 IP로 SNAT(PAT)하는데, 배치가 같은 목적지(외부API IP:443)로 동시 연결을 폭증시키자 그 목적지에 배정할 임시포트가 고갈됐습니다. NAT는 공인IP:임시포트 조합으로 연결을 구분하므로, 같은 목적지로의 동시 연결이 임시포트 범위를 넘으면 더 배정할 조합이 없어 신규 연결을 드랍합니다. 종료된 연결도 TIME_WAIT 동안 포트를 물고 있어 회수가 느립니다.
진단: 게이트웨이에서 ss -s로 소켓·time-wait 수를, sysctl net.ipv4.ip_local_port_range로 가용 포트 폭을 확인합니다. cat /proc/net/nf_conntrack | grep <외부API IP> | wc -l로 그 목적지로 몰린 연결 수가 포트 범위에 근접했는지 봅니다.
해결: 가장 효과적인 처방은 동시 연결 수 자체를 낮추는 것 — 배치 클라이언트에 커넥션 풀·keep-alive를 적용해 소수의 연결을 재사용합니다. 임시포트 범위(ip_local_port_range)를 넓히고 TIME_WAIT 타임아웃을 조정해 회수를 빠르게 합니다. 그래도 한 목적지에 수만 동시 연결이 필요하면 SNAT 공인 IP를 여러 개 풀로 두어(--to-source 범위) 다중화 용량을 늘립니다. "목적지 하나에 동시 연결을 무한정 늘리지 않는다"가 원칙입니다.
실제 업무에서 이 지식이 쓰이는 상황:
공공기관이나 금융 프로젝트에서 배포 직전 또는 운영 중에 외부 연계 오류가 발생하는 경우가 많습니다. 대부분은 Proxy 설정 누락이나 방화벽 미오픈 문제입니다.
외부 API 연계 불가 시 점검 순서:
# 1단계: 네트워크 경로 확인
ping -c 3 external-api.example.com # 내부 → 외부 ICMP (보통 차단)
nc -zv external-api.example.com 443 # TCP 포트 열렸는지
# 2단계: Proxy 경유 테스트
curl -x http://10.10.1.100:3128 -v https://external-api.example.com/health
# 3단계: Squid 로그에서 차단 여부 확인
grep external-api.example.com /var/log/squid/access.log | tail -5
# 4단계: Java 앱 JVM 옵션 확인
ps aux | grep java | tr ' ' '\n' | grep proxy
Proxy Chain — Proxy를 통해 또 다른 Proxy로:
# 내부 Proxy → 상위 Proxy → 인터넷 구조가 필요한 경우
# /etc/squid/squid.conf
cache_peer 10.10.0.1 parent 3128 0 no-query default
never_direct allow all
새로운 외부 연계 서비스가 추가될 때마다 Squid ACL에 목적지를 추가하고, Java 앱의 nonProxyHosts에서 내부 서버 대역이 빠지지 않도록 관리하는 것이 Proxy 운영의 핵심입니다. ---
명령어·단축키 빠른 참조
폐쇄망에서 Proxy/NAT를 설정하고 클라이언트별로 프록시를 태울 때 쓴 명령을 모았습니다. "예" 열의 조합을 그대로 써도 됩니다.
| 명령어/단축키 | 용도 | 자주 쓰는 예 |
|---|---|---|
export http_proxy / no_proxy | 셸·curl·wget·Python용 프록시 지정과 우회 대상 설정 | export http_proxy=http://10.10.1.100:3128 / export no_proxy=10.0.0.0/8,localhost |
curl -x | 환경변수와 무관하게 프록시 명시해 접속 | curl -x http://10.10.1.100:3128 -v https://api/health |
-Dhttp.proxyHost (JVM) | Java는 env 무시 → 시스템 프로퍼티로 프록시 지정 | java -Dhttp.proxyHost=10.10.1.100 -Dhttp.proxyPort=3128 -jar app.jar |
squid -k parse / reconfigure | Squid 설정 문법검증·무중단 재적용 | sudo squid -k parse 뒤 sudo squid -k reconfigure |
tail -f access.log | Squid 요청 실시간 추적 | tail -f /var/log/squid/access.log |
grep TCP_DENIED | ACL에 막힌 요청만 필터 | grep TCP_DENIED /var/log/squid/access.log |
iptables -t nat -A POSTROUTING | SNAT·마스커레이드(내부→외부 출발지 변환) | -s 192.168.10.0/24 -o eth0 -j MASQUERADE |
iptables -t nat -A PREROUTING | DNAT 포트포워딩(외부→내부 목적지 변환) | --dport 8080 -j DNAT --to-destination 192.168.10.11:8080 |
iptables -t nat -L -n -v | 현재 NAT 규칙·패킷 카운터 확인 | sudo iptables -t nat -L -n -v |
sysctl net.ipv4.ip_forward | 라우터·NAT 서버 IP 포워딩 활성화 | echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward |
env | grep -i proxy | 현재 프록시 관련 환경변수 점검 | env | grep -iE "proxy|no_proxy" |
ps aux | grep java | 앱 JVM에 프록시 옵션이 실렸는지 확인 | ps aux | grep java | tr ' ' '\n' | grep proxy |
sysctl net.ipv4.ip_local_port_range | SNAT 임시포트 고갈 진단(포트 범위 확인) | ss -s로 time-wait 수와 함께 비교 |
관련 모듈로 더 깊이:
- 네트워크 방화벽 정책과 요청서 작성 실무 — 아웃바운드 허용 정책과 방화벽 규칙으로 외부 연계 경로를 통제하는 법
- 써드파티 API와 공공 인프라 연계 실무 — Proxy를 거쳐 호출하는 외부 API 연계의 운영 관점
- 트래픽 제어와 이벤트 기반 연계 구조 — Proxy 앞단에서 트래픽을 중계하는 게이트웨이 구조
다음 모듈에서는 API Gateway의 역할과 Webhook을 통한 이벤트 기반 외부 연계 구조를 다룹니다.