SSD Nodes Learn
가이드 Matt Connor작성자 Matt Connor · 업데이트됨 2026-07-24

VPS에 WireGuard VPN 구축 및 설정 가이드

Linux VPS에서 WireGuard를 구축하는 상세 방법입니다. wg0.conf 설정부터 IP forwarding, NAT 설정법을 다룹니다. 특히 OpenVZ 환경의 커널 모듈 오류와 키 권한 설정 실수로 인한 보안 취약점 문제를 해결하는 방법을 설명합니다.

구축 목표

자체 서버에 WireGuard VPN을 구축하는 데 필요한 설정은 약 40줄입니다. 하나의 key pair, 하나의 interface 파일, 하나의 sysctl, 하나의 NAT rule, 그리고 하나의 firewall hole이 필요합니다. 설치 과정은 매우 간단하므로, 본 가이드는 주로 오류가 발생하는 부분인 key permissions, AllowedIPs, forwarding 및 DNS를 다룹니다.

WireGuard는 커널 내의 Layer 3 터널입니다. Linux 5.6부터 메인라인에 포함되었으므로, Ubuntu 24.04 및 Debian 13은 외부 모듈 없이 이를 제공합니다. 암호 협상(cipher negotiation), 인증 기관(CA), 사용자 이름/비밀번호 단계가 없습니다. peer는 public key와 해당 key가 사용할 수 있는 IP 주소의 조합입니다. MAC 체크에 실패한 패킷은 응답 없이 폐기되므로, 포트가 스캔에 응답하지 않습니다. 반대로, 별도의 인증 서버가 존재하지 않으므로, 접속 권한을 제거하려면 해당 장비에서 peer를 삭제해야 합니다.

가상화 환경을 먼저 확인하십시오

WireGuard는 모듈을 로드할 수 있는 커널이 필요합니다. KVM VPS에서는 즉시 작동합니다. 호스트 커널을 공유하는 컨테이너 가상화(OpenVZ, LXC) 환경에서는 첫 번째 명령 실행 시 RTNETLINK answers: Operation not supported 오류가 발생합니다. 이 경우 wireguard-go userspace 구현체가 대안으로 사용됩니다. 먼저 sudo modprobe wireguard && echo ok 명령어로 확인하십시오.

키 유출 없이 키 생성하기

모든 사용자가 읽을 수 있는 /etc/wireguard/server.key는 VPN을 사용하지 않는 것과 같습니다. 일반적인 umask 077 && wg genkey | sudo tee ... 방식은 신뢰할 수 없습니다. sudotee가 생성하는 파일에 자체 umask를 적용하기 때문입니다. 권한 모드를 명시적으로 설정하십시오.

sudo apt update && sudo apt install -y wireguard nftables
sudo install -d -m 700 /etc/wireguard
sudo sh -c 'umask 077; wg genkey > /etc/wireguard/server.key'
sudo sh -c 'wg pubkey < /etc/wireguard/server.key > /etc/wireguard/server.pub'
sudo chmod 600 /etc/wireguard/server.key

클라이언트 키 쌍도 동일한 방식으로 생성하십시오. wg genpsk는 각 설정 파일에 한 줄의 선택적 pre-shared key를 추가합니다.

서버 인터페이스: /etc/wireguard/wg0.conf

[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = <contents of /etc/wireguard/server.key>

[Peer]
PublicKey = <laptop public key>
PresharedKey = <psk, optional>
AllowedIPs = 10.8.0.2/32

chmod 600 메시지가 나타나면 파일이 모든 사용자에게 공개되어 있다는 시작 경고이며, 설정을 누락했음을 의미합니다. Address은 터널 내부의 서버 주소이며, 전체 VPN 서브넷 마스크를 포함합니다. 외부 네트워크와 겹치지 않는 범위를 선택하십시오. 192.168.1.0/24은 클라이언트가 사용하는 홈 라우터의 절반과 충돌하며, 이 경우 터널은 로컬 경로에 밀려 조용히 연결이 끊깁니다.

server 측의 피어 AllowedIPs은 해당 클라이언트가 소유한 유일한 터널 주소인 /32입니다. 두 피어에 동일한 allowed IP를 할당하면 마지막에 설정된 피어로 이동하며, 첫 번째 피어는 오류 메시지 없이 트래픽 수신이 중단됩니다. SaveConfig을 설정하지 않으면 wg-quick down이 실행 중인 상태를 기준으로 이 파일을 다시 작성합니다.

Turn the box into a router

Linux server는 자신에게 지정되지 않은 패킷을 폐기합니다. 기본적으로 Forwarding과 source NAT가 설정되어 있지 않습니다.

printf 'net.ipv4.ip_forward = 1\nnet.ipv6.conf.all.forwarding = 1\n' \
  | sudo tee /etc/sysctl.d/99-wireguard.conf
sudo sysctl --system
sysctl net.ipv4.ip_forward

기본 sysctl -w 설정은 다음 reboot 시까지는 작동하지만, 이후에는 작동이 중단됩니다. NAT에는 egress interface가 필요합니다. 즉, wg0가 아니라 인터넷에 연결되는 NIC가 필요합니다. eth0을 가정하지 마십시오. 현재 image는 enp1s0 또는 ens3와 같은 이름을 사용하므로, ip route show default에서 인터페이스 이름을 확인하십시오.

Firewall: 포트 및 포워딩 경로

nftables 파일 하나에서 필터링과 NAT를 모두 처리합니다. /etc/nftables.conf을 실행하면 기존 ruleset이 삭제(flush)되므로, 이미 ufw나 Docker를 사용하는 서버에서는 실행하지 마십시오.

#!/usr/sbin/nft -f
flush ruleset

table inet filter {
  chain input {
    type filter hook input priority filter; policy drop;
    ct state established,related accept
    iif lo accept
    tcp dport 22 accept
    udp dport 51820 accept
  }
  chain forward {
    type filter hook forward priority filter; policy drop;
    ct state established,related accept
    iifname "wg0" oifname "enp1s0" accept
  }
}

table ip nat {
  chain postrouting {
    type nat hook postrouting priority srcnat; policy accept;
    ip saddr 10.8.0.0/24 oifname "enp1s0" masquerade
  }
}

SSH 세션을 하나 유지한 상태에서 sudo systemctl enable --now nftables로 적용하십시오. policy drop 실행 시 SSH 규칙에 오타가 있으면 서버 접속이 차단됩니다. forward chain에서 허용되지 않는 항목을 확인하십시오 — wg0부터 wg0까지입니다. 피어(Peer)는 인터넷에 접속할 수 있지만, 서로에게는 접속할 수 없습니다. 피어 간 VPN을 구축하려면 iifname "wg0" oifname "wg0" accept을 추가하십시오. 동일한 chain이 피어가 서버 내부의 자원에 접근하는 권한을 제어합니다. 이는 해당 서버를 tmux에서 Claude Code를 실행하는 원격 개발 서버로 사용하는 경우, 해당 환경을 외부에 노출하지 않기 위해 중요합니다.

ufw를 사용하는 서버의 경우: ufw allow 51820/udp, /etc/default/ufw 내의 DEFAULT_FORWARD_POLICY="ACCEPT", 그리고 /etc/ufw/before.rules 최상단의 *nat POSTROUTING MASQUERADE 규칙이 필요합니다.

systemd로 실행하기

sudo systemctl enable --now wg-quick@wg0
sudo wg show

wg-quick는 interface를 생성하고, address를 추가하며, AllowedIPs에서 파생된 route를 설치합니다. enable --now가 중요한 이유가 있습니다. 수동으로 실행한 wg-quick up wg0는 재부팅 후 사라집니다. kernel upgrade 시에도 재부팅이 필요합니다.

클라이언트 설정 및 흔히 발생하는 실수

[Interface]
PrivateKey = <laptop private key>
Address = 10.8.0.2/32
DNS = 10.8.0.1

[Peer]
PublicKey = <server public key>
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25

AllowedIPs는 두 가지 역할을 동시에 수행합니다. 이 두 역할을 혼동하는 것이 WireGuard 사용 시 발생하는 대부분의 혼란 원인입니다.

Outbound 측면에서 이는 routing table입니다. 패킷의 목적지가 peer의 AllowedIPs와 일치하면, 해당 패킷은 암호화되어 해당 peer로 전송됩니다. 0.0.0.0/0, ::/0은 터널을 통해 모든 트래픽을 보냅니다. 즉, 서버를 default route로 사용하는 full tunnel 방식입니다. split tunnel은 더 제한적인 목록을 사용합니다. AllowedIPs = 10.8.0.0/24, 10.20.0.0/16는 VPN 트래픽과 서버 뒤에 있는 하나의 private network만 포함하며, 나머지 모든 트래픽은 기존의 local route를 유지합니다. 이 제한적인 목록 덕분에 특정 서비스를 공용 인터넷으로부터 완전히 격리할 수 있습니다. 예를 들어, 터널 주소에 연결된 VPS의 private Nextcloud instance나 동일한 장비에서 실행 중인 nested-virtualisation lab VMs를 peer에게는 노출하면서 외부에는 숨길 수 있습니다.

Inbound 측면에서 이는 access-control list입니다. 소스 주소가 해당 peer의 AllowedIPs에 포함되지 않은 peer로부터 온 복호화된 패킷은 폐기됩니다. 서버가 노트북에 대해 10.8.0.2/32을 설정하는 이유가 이것입니다. 만약 여기에 0.0.0.0/0 항목이 있다면, 해당 클라이언트는 터널 내의 어떤 주소로도 spoofing을 할 수 있습니다.

PersistentKeepalive는 NAT 뒤에 있는 클라이언트를 위한 설정입니다. NAT 환경의 라우터는 패킷이 흐를 때만 UDP mapping을 유지합니다. 이 연결이 만료되면 서버는 클라이언트에 도달할 수 없습니다. PersistentKeepalive = 25은 mapping을 계속 유지합니다. 이 설정은 서버가 아닌 클라이언트에 적용하십시오.

DNS, and the leak nobody notices

With AllowedIPs = 0.0.0.0/0 and no DNS = line, the client keeps the resolver it learned from the local network — the café router at 192.168.1.1. That route is more specific than the default route, so DNS queries leave over the local link in cleartext while everything else is tunnelled. The traffic is private; the list of names is not.

Two honest options. Point DNS at a public resolver (DNS = 9.9.9.9) and the queries ride the tunnel and exit from your server, though that resolver still sees them. Or run unbound or dnsmasq bound to 10.8.0.1, set DNS = 10.8.0.1, and add udp dport 53 iifname "wg0" accept to the input chain — set that line and forget the resolver, and nothing resolves at all.

On Linux clients wg-quick applies DNS through resolvconf; if it is absent you get resolvconf: command not found. Install openresolv, or set PostUp = resolvectl dns %i 10.8.0.1 on a systemd-resolved client.

터널을 끊지 않고 peer를 추가하거나 제거하기

사용자를 추가하기 위해 interface를 재시작하면 연결된 모든 사용자의 연결이 끊깁니다. wg0.conf[Peer] 블록을 추가한 다음, 해당 위치에서 peer set을 다시 로드하십시오.

sudo bash -c 'wg syncconf wg0 <(wg-quick strip wg0)'

wg-quick stripAddress, DNS, PostUp와 같은 wg-quick-only 키를 제외한 설정을 출력합니다. syncconf를 사용하면 활성 세션을 유지하면서 변경 사항을 적용할 수 있습니다. 이 명령은 peer만 업데이트합니다. Address가 변경된 경우에는 여전히 interface를 down/up 해야 합니다. sudo wg set wg0 peer <public key> remove로 권한을 취소한 후, 파일에서 해당 블록을 삭제하십시오. 삭제하지 않으면 다음 로드 시 다시 적용됩니다.

Failure modes, with the strings you will see

Handshake never completes. wg showlatest handshake이 없는 peer가 표시되며, 클라이언트 로그에는 다음과 같이 나타납니다:

Handshake for peer 1 (10.0.0.10:51820) did not complete after 5 seconds, retrying (try 2)

데이터가 수신되지 않거나 수신된 데이터가 없습니다. 다음 사항을 순서대로 확인하십시오. VPS 방화벽과 제공업체 네트워크 방화벽(대부분의 패널에서 별도로 관리됨) 모두에서 UDP 51820 포트가 열려 있는지 확인하십시오. Endpoint 주소와 포트가 정확한지 확인하십시오. 키 설정이 서로 바뀌지 않았는지 확인하십시오. 클라이언트의 [Peer] 블록에는 반드시 서버의 public 키가 있어야 하며, 그 반대도 마찬가지입니다. private 키를 붙여넣거나 클라이언트 자신의 public 키를 넣으면 이 문제가 발생합니다. 서버의 sudo tcpdump -ni any udp port 51820을 통해 패킷 수신 여부를 확인할 수 있습니다. 기본적으로 커널 모듈은 로그를 남기지 않습니다. WireGuard 메시지는 dynamic debug(echo module wireguard +p | sudo tee /sys/kernel/debug/dynamic_debug/control)를 활성화한 후에만 dmesg에 나타납니다. 이 기능이 활성화된 상태에서 키가 일치하지 않으면 invalid-MAC drop으로 표시됩니다.

Handshake works, no internet. ping 10.8.0.1은 성공하지만 ping 1.1.1.1에서 타임아웃이 발생합니다. 이는 forwarding 또는 NAT 설정이 누락되었음을 의미합니다. sysctl net.ipv4.ip_forward1을 읽는지 확인한 후, sudo nft list ruleset 또는 sudo iptables -t nat -L POSTROUTING -n -v을 사용하여 클라이언트가 ping을 보내는 동안 카운터 변화를 관찰하십시오. masquerade rule의 패킷 카운터가 0이면 egress interface 이름이 잘못된 것입니다. 응답이 없는데 카운터만 증가한다면 forward chain policy를 확인하십시오.

Internet works, names do not. ping 1.1.1.1은 성공하지만 curl https://example.comCould not resolve host을 반환합니다. DNS 라인이 누락되었거나, 터널 내부에서 도달할 수 없는 resolver 주소가 지정된 경우입니다.

Some HTTPS sites hang. SSH와 ping은 정상 작동하지만, 용량이 큰 페이지 로딩이 멈춥니다. 이는 path MTU 문제입니다. 터널링으로 인해 오버헤드가 발생하며, 중간 경로의 링크가 ICMP 메시지 없이 대용량 패킷을 드롭하기 때문입니다. 클라이언트의 [Interface]에서 MTU 값을 낮추십시오. 1420을 시도한 후, 1380, 1280 순으로 시도하십시오.

Interface refuses to start. Address already in use은 다른 프로세스가 UDP 51820을 이미 사용 중임을 의미합니다. up 실패 후 발생하는 Cannot find device wg0은 대개 설정이 거부되었음을 의미합니다. journalctl -u wg-quick@wg0 -n 50를 확인하십시오.

Streisand 또는 OpenVPN에서의 마이그레이션

Streisand는 유지보수가 중단되었으며 repository가 archive 상태입니다. 관리되지 않는 automation을 사용하여 VPN을 운영하는 것은 보안 문제를 야기합니다. 인플레이스(in-place) 업그레이드는 불가능합니다. OpenVPN의 PKI는 변환되지 않습니다. WireGuard는 certificate, CA, 만료 기한이 없으므로 모든 client는 새로운 key pair를 생성해야 합니다.

병렬로 마이그레이션을 진행하십시오. 동일한 서버에서 UDP 51820 포트의 WireGuard와 1194 포트의 OpenVPN을 동시에 실행할 수 있습니다. wg0를 설치하고, client를 하나씩 이동시킨 후 기존 서비스를 중단하십시오. OpenVPN의 username/password 및 revocation 모델은 그대로 유지되지 않습니다. 계정 관리나 audit trail이 필요한 경우, WireGuard 상단에 해당 기능을 추가하십시오.

Backups, upgrades, and what strains at scale

/etc/wireguard가 서버입니다. 백업을 수행하십시오 (sudo tar czf wg-backup.tgz -C /etc wireguard, mode 600, 외부 저장소에 보관). 그러면 새로운 VPS에서 몇 분 안에 서버를 재구축할 수 있습니다. 서버의 private key를 분실하면 모든 클라이언트 설정을 재발급해야 합니다. 클라이언트가 서버의 public key를 고정(pin)하기 때문입니다. 업그레이드는 일반적인 apt upgrade 과정이며, kernel 업데이트를 위해서는 재부팅이 필요합니다. wg-quick@wg0를 활성화했다면 자동으로 복구됩니다.

Peer별 상태 데이터는 작고 암호화 연산은 kernel에서 실행됩니다. 따라서 성능 한계는 이 설정값이 아니라 VPS의 CPU 및 bandwidth 할당량에 의해 결정됩니다. 발표된 수치를 신뢰하지 말고, tunnel을 통해 iperf3로 성능을 측정하십시오. 규모가 커질 때 문제가 되는 것은 운영 측면입니다. 모든 peer는 고유한 tunnel IP를 가져야 합니다. 60개의 [Peer] 블록을 수동으로 편집하면 중복된 AllowedIPs가 발생할 수 있습니다. 스크립트를 사용하여 설정을 생성하십시오. 서버 한 대는 하나의 UDP endpoint이자 단일 장애 지점(single point of failure)입니다. WireGuard는 클러스터링을 지원하지 않습니다. 중복성을 확보하려면 별도의 key를 가진 두 번째 서버가 필요합니다. Key rotation은 수동으로 이루어지므로, 누가 어떤 key를 보유하고 있는지와 key를 취소(revoke)하는 방법을 기록해 두십시오.

이 모든 과정에는 사용자가 제어할 수 있는 Linux 시스템이 필요합니다. 즉, public IP, 모듈을 로드할 수 있는 kernel, 그리고 전체를 제어할 수 있는 firewall이 필요합니다.

FAQ

Why does the WireGuard handshake never complete?

wg showlatest handshake가 없는 peer가 표시되면 패킷이 도착하지 않거나 수락되지 않는 상태입니다. VPS 방화벽과 서비스 제공업체의 네트워크 방화벽 모두에서 UDP 51820 포트를 확인하십시오. Endpoint 호스트와 포트가 올바른지 확인하고, 키가 서로 바뀌지 않았는지 점검하십시오. 클라이언트의 [Peer] 블록에는 서버의 public key가 있어야 합니다. 서버의 sudo tcpdump -ni any udp port 51820를 통해 패킷 수신 여부를 확인할 수 있습니다. dmesg는 dynamic debug(echo module wireguard +p | sudo tee /sys/kernel/debug/dynamic_debug/control)를 활성화한 후에만 WireGuard의 handshake 실패를 보고하며, 키 불일치는 invalid-MAC drop으로 나타납니다.

The tunnel connects but I have no internet. What is missing?

ping 10.8.0.1는 작동하지만 ping 1.1.1.1에서 타임아웃이 발생하면 forwarding 또는 NAT 설정 문제입니다. sysctl net.ipv4.ip_forward에서 1를 확인하고, 이것이 sysctl -w가 아닌 /etc/sysctl.d/에 설정되어 있는지 확인하십시오. 그 다음, ip route show default에서 실제 egress interface의 masquerade rule 이름을 확인하십시오. 보통 enp1s0 또는 ens3이며, eth0인 경우는 드뭅니다.

Do I need the DNS = line in my client config?

Full tunnel 설정에서 DNS = 라인이 없으면, 클라이언트는 로컬 네트워크에서 학습한 resolver를 그대로 사용합니다. 이 경우 다른 모든 트래픽은 터널을 통과하지만, DNS 쿼리는 로컬 링크를 통해 평문(cleartext)으로 전송됩니다. DNS를 public resolver로 지정하거나, 10.8.0.1에 바인딩된 unbound/dnsmasq를 실행하고 input chain에서 udp dport 53 iifname "wg0"를 허용하십시오.

What does AllowedIPs actually control?

이 설정은 두 가지 역할을 수행합니다. Outbound에서는 라우팅 테이블 역할을 합니다. peer의 AllowedIPs와 일치하는 트래픽은 암호화되어 해당 peer로 전송됩니다. Inbound에서는 액세스 제어 목록(ACL) 역할을 합니다. 소스 주소가 해당 peer의 AllowedIPs 외부인 복호화된 패킷은 드롭됩니다. 서버 측에는 클라이언트당 하나의 /32가 나열되지만, 클라이언트 측에는 0.0.0.0/0가 나열될 수 있는 이유입니다.

Will WireGuard run on any VPS?

KVM VPS에서는 커널 모듈을 사용하여 추가 설정 없이 작동합니다. OpenVZ 또는 LXC와 같이 호스트 커널을 공유하는 컨테이너 가상화 환경에서는 modprobe wireguardOperation not supported 오류를 일으키며, 이 경우 wireguard-go userspace 구현체를 대안으로 사용해야 합니다. 다른 작업을 수행하기 전에 sudo modprobe wireguard && echo ok를 먼저 실행하십시오.

#wireguard#vpn#linux-networking#nftables#systemd#self-hosting