본문 바로가기
Operating System

WireGuard로 두 거점을 site-to-site VPN으로 잇기 - wg0 AllowedIPs 라우팅과 핸드셰이크 디버깅

나크나로·2026년 8월 29일·조회 3

사무실 두 곳을 잇거나, 온프레미스 서버실과 클라우드 VPC를 한 네트워크처럼 쓰고 싶다는 요청은 꾸준히 들어온다. 예전에는 IPsec으로 터널을 잡던 자리인데, 설정 파라미터가 많고 로그가 불친절해서 한 번 막히면 오래 걸렸다. 요즘은 같은 일을 WireGuard로 처리하는 경우가 많아졌다. 설정 파일이 짧고, 커널에 들어가 있어 빠르고, 무엇보다 디버깅할 게 몇 개 안 된다.

결론부터 말하자면, site-to-site 연결의 핵심은 두 가지다. 각 거점의 wg0에 터널용 IP를 주고, AllowedIPs에 상대 거점의 내부 LAN 대역을 적어 라우팅을 설계하는 것. 그리고 한쪽이 NAT 뒤에 있으면 그쪽에 PersistentKeepalive = 25를 넣어 세션을 유지한다. 상시화는 systemctl enable --now wg-quick@wg0 한 줄로 끝난다. 아래에서 키 생성부터 라우팅 설계, 상시화, 핸드셰이크 디버깅까지 차례로 살펴본다.

1. WireGuard와 AllowedIPs가 무엇인가

WireGuard는 리눅스 커널에 들어 있는 VPN이다. 인터페이스 하나(wg0)를 만들고, 상대의 공개키와 접속 주소만 알려주면 암호화된 UDP 터널이 생긴다. 관리 도구는 두 개다. 저수준 명령 wg와, 설정 파일을 읽어 인터페이스와 라우팅을 한 번에 세워주는 wg-quick이다.

여기서 반드시 이해하고 넘어갈 개념이 AllowedIPs다. WireGuard는 이 값을 두 가지 용도로 동시에 쓴다. 이걸 크립토키 라우팅(cryptokey routing)이라고 부른다.

  • 수신 필터: 이 피어에서 들어온 패킷의 출발지 IP가 AllowedIPs에 없으면 버린다.
  • 송신 라우팅: 목적지 IP가 어느 피어의 AllowedIPs에 속하는지 보고 그 피어로 보낸다.

wg-quick은 이 AllowedIPs 목록을 그대로 시스템 라우팅 테이블에 넣는다. 즉 site-to-site에서 상대 거점의 LAN 대역을 AllowedIPs에 적는 것이 곧 라우팅 설계다. 여기가 어긋나면 터널은 붙어도 내부 통신이 안 된다.

2. 예제 토폴로지와 IP 설계

거점 두 곳을 가정한다. 본사(A)는 고정 공인 IP가 있고 UDP 포트를 열 수 있다. 지사(B)는 공유기 NAT 뒤에 있고 공인 IP가 유동이다.

  • 본사 A - 게이트웨이 서버, 내부 LAN 192.168.10.0/24
  • 지사 B - 게이트웨이 서버, 내부 LAN 192.168.20.0/24
  • 터널 대역 - 10.10.0.0/24 (A는 10.10.0.1, B는 10.10.0.2)

터널 대역은 양쪽 LAN과 겹치지 않는 새 대역으로 잡는다. 겹치면 라우팅이 꼬인다. VPC를 붙일 때도 방식은 같다. VPC의 서브넷 대역을 상대 거점 입장의 AllowedIPs에 넣으면 된다.

3. 키 생성

두 서버 각각에서 개인키와 공개키를 만든다. 개인키는 밖으로 내보내지 않고, 공개키만 상대에게 전달한다.

$ sudo apt install -y wireguard        # 데비안/우분투
$ sudo dnf install -y wireguard-tools  # RHEL/Rocky/Alma

$ umask 077
$ wg genkey | tee privatekey | wg pubkey > publickey
$ cat privatekey
yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=
$ cat publickey
HIgo9xNzJMWLKASShiTqIybxZ0U3wGLiUeJ1PKf8ykw=

키는 base64 문자열이다. A의 공개키는 B 설정에, B의 공개키는 A 설정에 들어간다. 여기서 자기 공개키와 상대 공개키를 헷갈리는 실수가 잦다. 나중에 핸드셰이크가 안 될 때 제일 먼저 의심할 자리다.

4. IP 포워딩 활성화

게이트웨이 서버가 자기 앞을 지나는 패킷을 다른 인터페이스로 넘기려면 커널 포워딩을 켜야 한다. 이걸 안 켜면 터널은 붙는데 LAN 대 LAN 통신이 조용히 막힌다. 양쪽 서버 모두에서 설정한다.

$ echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-wireguard.conf
net.ipv4.ip_forward = 1
$ sudo sysctl --system
...
* Applying /etc/sysctl.d/99-wireguard.conf ...
net.ipv4.ip_forward = 1

5. 본사 A 설정 파일

설정 파일 경로는 /etc/wireguard/wg0.conf다. 파일 이름의 wg0이 인터페이스 이름이 된다.

# /etc/wireguard/wg0.conf  (본사 A)
[Interface]
Address = 10.10.0.1/24
ListenPort = 51820
PrivateKey = <A_개인키>
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT

[Peer]
# 지사 B
PublicKey = <B_공개키>
AllowedIPs = 10.10.0.2/32, 192.168.20.0/24

A에는 B의 Endpoint를 적지 않았다. B가 유동 IP라 A는 접속 주소를 모르기 때문이다. 대신 B가 먼저 접속해 오면 WireGuard가 그 출발지 주소를 기억해 응답한다. PostUp/PostDown은 인터페이스가 오르내릴 때 방화벽의 FORWARD 규칙을 넣고 빼는 역할이다.

6. 지사 B 설정 파일

# /etc/wireguard/wg0.conf  (지사 B)
[Interface]
Address = 10.10.0.2/24
PrivateKey = <B_개인키>
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT

[Peer]
# 본사 A
PublicKey = <A_공개키>
Endpoint = 203.0.113.10:51820
AllowedIPs = 10.10.0.1/32, 192.168.10.0/24
PersistentKeepalive = 25

B는 공인 IP가 있는 A로 접속해야 하므로 Endpoint에 A의 공인 IP와 포트를 적는다. 그리고 NAT 뒤에 있으므로 PersistentKeepalive = 25를 넣는다. 이유는 8절에서 설명한다.

AllowedIPs 대칭을 다시 확인한다. A에서 B로 가는 쪽은 192.168.20.0/24, B에서 A로 가는 쪽은 192.168.10.0/24. 자기 쪽 LAN을 자기 설정에 적는 게 아니라, 상대 LAN을 적는다는 점을 놓치지 않는다.

7. wg-quick과 systemd로 상시화

인터페이스를 한 번 올려보고, 문제없으면 부팅 시 자동으로 뜨게 등록한다. wg-quick@wg-quick을 감싸는 systemd 템플릿 서비스다. @ 뒤에 인터페이스 이름을 붙인다.

$ sudo systemctl enable --now wg-quick@wg0
Created symlink /etc/systemd/system/multi-user.target.wants/wg-quick@wg0.service → /usr/lib/systemd/system/wg-quick@.service.

$ systemctl status wg-quick@wg0
● wg-quick@wg0.service - WireGuard via wg-quick(8) for wg0
     Loaded: loaded (/usr/lib/systemd/system/wg-quick@.service; enabled)
     Active: active (exited) since Fri 2026-08-29 10:12:03 KST; 4s ago
    Process: 1180 ExecStart=/usr/bin/wg-quick up wg0 (code=exited, status=0/SUCCESS)

서비스 상태가 active (exited)인 것은 정상이다. wg-quick은 인터페이스를 세팅하고 종료하는 일회성 명령이라, 데몬처럼 계속 떠 있지 않는다. 실제 터널은 커널 안에서 유지된다.

wg-quick이 AllowedIPs를 라우팅 테이블에 넣었는지 확인한다.

$ ip route show dev wg0
10.10.0.0/24 proto kernel scope link src 10.10.0.2
10.10.0.1/32
192.168.10.0/24

상대 LAN 192.168.10.0/24wg0으로 잡혀 있으면 라우팅 설계가 파일대로 반영된 것이다.

8. NAT 뒤에서 PersistentKeepalive가 필요한 이유

공유기 NAT는 안에서 밖으로 나간 UDP 세션을 기억해 두었다가, 그 응답만 안으로 들여보낸다. 이 매핑은 일정 시간 트래픽이 없으면 만료된다. 지사 B가 조용히 있는 동안 매핑이 사라지면, 본사 A가 B의 LAN으로 먼저 패킷을 보내려 해도 NAT를 뚫고 들어갈 수 없다.

PersistentKeepalive는 지정한 초마다 빈 패킷을 보내 이 매핑을 살려 둔다. 공식 문서가 권장하는 값이 25초다. 다양한 방화벽에서 무난히 동작하는 간격이다. 기본값은 0(비활성)이다.

keepalive는 NAT 뒤에 있는 쪽에만 넣으면 된다. 여기서는 B에만 넣었다. 양쪽 다 NAT 뒤라면 양쪽 모두에 넣는다. 공인 IP로 노출된 A에는 굳이 넣지 않아도 된다.

9. 핸드셰이크 확인과 디버깅

연결 상태는 wg show로 본다. 이 명령의 출력 한두 줄이 대부분의 문제를 가려낸다.

$ sudo wg show
interface: wg0
  public key: HIgo9xNzJMWLKASShiTqIybxZ0U3wGLiUeJ1PKf8ykw=
  private key: (hidden)
  listening port: 51820

peer: xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=
  endpoint: 203.0.113.7:41890
  allowed ips: 10.10.0.2/32, 192.168.20.0/24
  latest handshake: 38 seconds ago
  transfer: 1.24 MiB received, 984.50 KiB sent
  persistent keepalive: every 25 seconds

latest handshake 줄이 보이고 시각이 갱신되면 터널은 살아 있다. 이 줄이 아예 없거나 transfer가 계속 0이면 핸드셰이크가 안 되는 것이다. 순서대로 확인한다.

  • UDP 포트: A의 51820/udp가 열려 있는가. 클라우드면 보안그룹 인바운드 규칙, 온프레미스면 방화벽과 포트포워딩을 본다. WireGuard는 UDP다. TCP 규칙만 열어두는 실수가 흔하다.
  • 공개키 방향: A 설정의 Peer PublicKey가 B의 공개키인가. 자기 키를 잘못 넣으면 조용히 실패한다.
  • Endpoint 주소: B 설정의 A 주소와 포트가 맞는가.
  • 시각 동기: 양쪽 서버 시계가 크게 어긋나면 핸드셰이크가 거절될 수 있다. timedatectl로 NTP 동기 여부를 본다.

패킷이 실제로 도착하는지 A에서 직접 잡아본다.

$ sudo tcpdump -ni eth0 udp port 51820
10:20:41.102 IP 203.0.113.7.41890 > 203.0.113.10.51820: UDP, length 148
10:20:41.103 IP 203.0.113.10.51820 > 203.0.113.7.41890: UDP, length 92

양방향 패킷이 보이면 네트워크 경로는 열려 있고, 문제는 키나 AllowedIPs 쪽이다. 들어오는 패킷만 있고 나가는 응답이 없으면 키 불일치를 의심한다. 아예 아무것도 안 잡히면 방화벽에서 막힌 것이다.

피어별 핸드셰이크 시각만 스크립트로 뽑을 때는 이 형식이 편하다.

$ sudo wg show wg0 latest-handshakes
xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=	1756437641

값이 0이면 아직 한 번도 핸드셰이크가 없었다는 뜻이다.

10. 터널은 붙는데 LAN 통신이 안 될 때

핸드셰이크는 되고 터널 IP끼리 ping도 되는데, 상대 LAN의 실제 호스트로는 안 갈 때가 있다. 이건 터널 문제가 아니라 라우팅 또는 되돌아오는 경로 문제다.

$ ping -c2 10.10.0.1          # 터널 IP - 여기까지는 됨
64 bytes from 10.10.0.1: icmp_seq=1 ttl=64 time=8.4 ms

$ ping -c2 192.168.10.5       # 본사 LAN 호스트 - 여기서 막힘
(응답 없음)

확인할 지점은 세 곳이다.

  • AllowedIPs에 그 LAN 대역이 있는가. 없으면 그 목적지로 가는 라우팅 자체가 안 생긴다. 9절의 ip route show dev wg0로 재확인한다.
  • IP 포워딩이 켜져 있는가. 4절을 건너뛰면 게이트웨이가 패킷을 다음 홉으로 넘기지 않는다.
  • 되돌아오는 경로. 본사 LAN 호스트(192.168.10.5)는 192.168.20.0/24로 가는 길을 모른다. 이걸 해결하는 방법은 두 가지다.

첫째, 각 거점의 내부 라우터에 상대 LAN 대역을 WireGuard 게이트웨이로 보내는 정적 경로를 추가한다. 출발지 IP가 보존되어 site-to-site에서는 이 방식이 깔끔하다. 둘째, 라우터를 건드릴 수 없으면 게이트웨이에서 나가는 트래픽을 마스커레이드한다.

# 정적 경로를 못 넣을 때, 본사 A에서 지사 LAN 방향 마스커레이드
$ sudo iptables -t nat -A POSTROUTING -s 192.168.20.0/24 -o eth0 -j MASQUERADE

마스커레이드를 쓰면 상대 LAN 호스트 입장에서 출발지가 게이트웨이 IP로 바뀌므로 되돌아오는 경로 걱정이 없다. 다만 원래 출발지 IP가 가려지니, 접근 제어나 로그에서 실제 IP가 필요하면 정적 경로 방식을 택한다.

11. MTU 때문에 큰 전송만 멈출 때

핸드셰이크도 되고 ping도 되는데 파일 전송이나 SSH 세션이 중간에 멎으면 MTU를 의심한다. wg-quickwg0 MTU를 1420으로 잡는다. 그런데 경로 중간에 PPPoE나 또 다른 터널이 끼면 이 값도 커서 큰 패킷이 조각나거나 버려진다. 증상이 특징적이다. 작은 ping은 되고 큰 전송만 죽는다.

# MTU를 낮춰서 확인. 문제가 사라지면 설정 파일 [Interface]에 MTU 고정
$ sudo ip link set mtu 1380 dev wg0

낮춘 값으로 안정되면 설정 파일 [Interface]MTU = 1380을 넣어 고정한다. 값은 경로에 따라 다르니, 문제가 사라지는 선까지 낮춰가며 잡는다.

자주 묻는 질문

PersistentKeepalive는 항상 넣어야 하나?

아니다. NAT나 상태 기반 방화벽 뒤에 있는 거점에만 넣는다. 공인 IP로 포트를 열어둔 쪽은 없어도 된다. 양쪽 다 NAT 뒤라면 양쪽 모두에 넣는다. 권장값은 25초이고 기본값은 0(비활성)이다.

AllowedIPs에 0.0.0.0/0을 넣는 것과 특정 서브넷을 넣는 것의 차이는?

0.0.0.0/0은 모든 트래픽을 그 피어로 보내는 설정으로, 전 트래픽을 VPN으로 넘기는 원격 접속(로드워리어)에 쓴다. site-to-site에서는 상대 거점의 LAN 대역만 정확히 넣는다. 그래야 각 거점의 인터넷 트래픽은 자기 회선으로 나가고, 상대 LAN 대상 트래픽만 터널로 간다.

핸드셰이크는 되는데 LAN 호스트끼리 통신이 안 된다.

터널이 아니라 라우팅 문제다. 세 가지를 본다. 상대 LAN 대역이 AllowedIPs에 들어 있는지, 게이트웨이의 net.ipv4.ip_forward가 1인지, 상대 LAN 호스트가 이쪽 대역으로 되돌아오는 경로를 아는지. 되돌아오는 경로는 내부 라우터에 정적 경로를 넣거나 게이트웨이에서 마스커레이드로 해결한다.

wg show에 latest handshake 줄이 아예 안 보인다.

핸드셰이크가 한 번도 성립하지 않은 상태다. A의 UDP 51820 포트가 열려 있는지(WireGuard는 UDP다), 피어 PublicKey 방향이 맞는지, Endpoint 주소와 포트가 정확한지, 양쪽 시계가 동기화됐는지 순서대로 확인한다. tcpdump로 UDP 패킷이 실제 도착하는지 잡아보면 원인이 좁혀진다.

wg-quick 대신 systemd-networkd로 관리할 수 있나?

가능하다. systemd-networkd는 .netdev와 .network 파일로 WireGuard를 직접 다룬다. 다만 site-to-site 상시화 목적이면 wg-quick@wg0 서비스 하나로 충분하고 설정도 단순하다. wg-quick은 AllowedIPs 기반 라우팅을 자동으로 넣어주므로 라우팅 설계가 파일 그대로 반영된다.

관련 글

댓글 0

로그인 후 댓글을 남길 수 있습니다.

아직 댓글이 없습니다.