network / knowledge

VPS를 경유해 공인 IP 없는 홈서버를 외부에 열기

paul 2026.01.18 455words (2m)

집 회선에는 고정 공인 IP가 없고 포트를 열기도 마땅치 않다. 공인 IP가 있는 VPS를 하나 두고, 거기로 온 요청을 WireGuard 터널을 타고 집으로 넘기는 구조로 만들었다.

VPS는 트래픽만 흘려보내므로 가장 싼 것으로 충분하다.

패킷이 지나가는 길

VPS를 경유하는 요청의 헤더 변환 순서

두 번 주소가 바뀐다. VPS에서 한 번, 집 안의 VPN 서버에서 한 번이다.

PREROUTING이어야 하는 이유

목적지를 바꾸는 DNAT는 PREROUTING에서 해야 한다.

커널은 패킷을 받으면 목적지를 보고 "이건 내가 처리할 것"인지 "다른 데로 보낼 것"인지 정한다. 이 판단이 끝난 뒤에 주소를 바꾸면 이미 늦었다. 커널은 이미 로컬 처리로 결정했으므로 포워딩이 일어나지 않는다.

PREROUTING은 그 판단 전에 실행된다. 여기서 목적지를 집 쪽으로 바꿔놓으면 커널이 "다른 데로 보낼 것"으로 결정하고 FORWARD 체인을 태운다.

MASQUERADE가 없으면 응답이 사라진다

출발지를 바꾸는 것도 필요하다. 이게 없으면 요청은 집까지 잘 가는데 응답이 안 돌아온다.

집 안의 NPM이 받은 패킷의 출발지가 사용자의 공인 IP 그대로라면, NPM은 그 주소로 직접 답하려 한다. 그런데 집 네트워크의 라우팅 테이블에는 그리로 가는 경로가 없다. 응답이 그냥 유실된다.

MASQUERADE로 출발지를 터널 쪽 주소로 바꿔두면 NPM은 항상 그 주소로 답한다. 되돌아온 응답은 NAT 추적을 따라 원래 사용자에게 전달된다.

VPS 쪽 규칙

bash
# postup.sh
sysctl -w net.ipv4.ip_forward=1

# 목적지를 VPN 서버로
iptables -t nat -I PREROUTING 1 -i eth0 -p tcp --dport 80 -j DNAT --to-destination 10.10.11.1:80
iptables -t nat -I PREROUTING 1 -i eth0 -p tcp --dport 443 -j DNAT --to-destination 10.10.11.1:443

# 출발지를 VPS의 터널 주소로
iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE
bash
# postdown.sh
iptables -t nat -D PREROUTING -i eth0 -p tcp --dport 80 -j DNAT --to-destination 10.10.11.1:80
iptables -t nat -D PREROUTING -i eth0 -p tcp --dport 443 -j DNAT --to-destination 10.10.11.1:443
iptables -t nat -D POSTROUTING -o wg0 -j MASQUERADE

-I PREROUTING 1맨 앞에 끼워넣는다. -A 로 뒤에 붙이면 앞선 규칙에 먼저 걸릴 수 있다.

80과 443만 넘긴다. VPS의 SSH 포트까지 넘기면 VPS에 들어갈 방법이 없어진다.

VPN 서버 쪽 규칙

bash
# postup.sh
#!/bin/bash
sysctl -w net.ipv4.ip_forward=1
iptables -t nat -I PREROUTING 1 -i $1 -s 10.10.11.99 -p tcp --dport 80 -j DNAT --to-destination <집 NPM 주소>:80
iptables -t nat -I PREROUTING 1 -i $1 -s 10.10.11.99 -p tcp --dport 443 -j DNAT --to-destination <집 NPM 주소>:443
iptables -t nat -A POSTROUTING -o eth0 -s 10.10.11.99 -j MASQUERADE

-s 10.10.11.99 조건이 붙어 있다. VPS의 터널 주소다. 이 조건이 없으면 터널 안의 다른 peer가 보낸 것까지 집 안 NPM으로 넘어간다. VPS에서 온 것만 통과시킨다.

-i $1 은 WireGuard가 넘겨주는 인터페이스 이름이다. 스크립트를 인터페이스에 묶지 않는다.

NPM

집 쪽에서 실제로 받는 건 Nginx Proxy Manager다.

yaml
services:
  app:
    image: 'jc21/nginx-proxy-manager:latest'
    restart: always
    ports:
      - '80:80'
      - '443:443'
      - '81:81'
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt

여기까지 오면 그 다음은 평범한 reverse proxy다. 도메인을 보고 내부 서비스로 넘긴다. 인증서도 NPM이 받는다.

81번 관리 포트는 VPS에서 넘기지 않는다. 내부에서만 접근한다.