작은 설정으로 만드는 암호화된 사설망

WireGuard 기초: 안전한 VPN 구성과 운영의 핵심 흐름도
그림. WireGuard는 키 설정과 운영체제 경로가 함께 맞아야 통신한다.

WireGuard란

WireGuard는 운영체제에 가상 네트워크 인터페이스를 만들고, 그 인터페이스를 통과하는 IP 패킷을 피어 사이에서 암호화하는 VPN이다. 중앙 계정 서버에 로그인하는 구조가 아니라, 각 피어가 개인키를 보관하고 서로의 공개키를 설정한다.

노트북 wg0 10.50.0.2 ── 암호화 UDP ── 서버 wg0 10.50.0.1
                                      └─ 내부망 또는 인터넷

WireGuard는 키 교환과 암호화를 제공하지만 사용자 관리, 설정 배포, DNS, 방화벽 정책을 대신하지 않는다. 어떤 피어가 어떤 주소와 서비스에 접근할지는 운영자가 설계해야 한다.

핵심 용어

용어의미
인터페이스`wg0`처럼 VPN 주소와 개인키를 가진 가상 네트워크 장치다.
피어공개키로 식별되는 통신 상대다.
Endpoint암호화된 UDP 패킷을 보낼 상대의 공인 주소와 포트다.
AllowedIPs그 피어로 보낼 목적지 경로이며, 그 피어에서 받아들일 출발지 주소 범위다.
ListenPort이 인터페이스가 WireGuard UDP 패킷을 받을 포트다.
PersistentKeepaliveNAT 매핑 유지를 위해 일정 간격으로 빈 패킷을 보낸다.

서버와 클라이언트는 프로토콜상 특별히 구분되지 않는다. 고정 주소에서 여러 피어의 연결을 받는 장치를 편의상 서버라고 부른다.

설치와 도구

Linux 배포판에서는 패키지 관리자로 `wireguard-tools`를 설치한다. 핵심 명령은 `wg`와 `wg-quick`이다.

wg              현재 WireGuard 상태와 피어를 관리한다.
wg-quick        설정 파일을 바탕으로 주소와 경로까지 함께 올리고 내린다.
ip address      인터페이스 주소를 확인한다.
ip route        실제 라우팅 결과를 확인한다.

macOS, Windows, Android, iOS에는 공식 WireGuard 앱이 있다. GUI가 설정을 가져와도 설정의 의미는 같다.

개인키와 공개키 만들기

각 피어에서 자신의 개인키를 만든다. 개인키는 그 피어 밖으로 보내지 않는다.

umask 077
wg genkey | tee private.key | wg pubkey > public.key

`umask 077`은 새 파일을 현재 사용자만 읽을 수 있게 한다. 권한도 확인한다.

chmod 600 private.key

서버에는 클라이언트 공개키만, 클라이언트에는 서버 공개키만 전달한다. 개인키가 노출되면 그 키를 설정에서 제거하고 새 키 쌍으로 교체한다. 게시물, 채팅, Git 저장소에 실제 개인키를 붙이지 않는다.

선택적으로 사전 공유키를 피어 쌍에 추가할 수 있다.

wg genpsk > preshared.key
chmod 600 preshared.key

이 키도 비밀이다. 공개키와 달리 양쪽 설정에 같은 값이 들어간다.

예제 주소 계획

이 글에서는 문서용 사설망을 다음처럼 가정한다.

VPN 대역       10.50.0.0/24
WireGuard 서버 10.50.0.1
노트북         10.50.0.2
휴대전화       10.50.0.3
UDP 포트       51820

실제 환경에서는 기존 사내망, 집 공유기, 클라우드 VPC와 겹치지 않는 RFC 1918 사설 대역을 고른다. 양쪽 네트워크가 모두 `192.168.0.0/24`처럼 겹치면 어느 쪽으로 보낼지 경로가 모호해진다.

서버 설정

Linux에서 `wg-quick`의 기본 설정 경로는 `/etc/wireguard/wg0.conf`다.

[Interface]
Address = 10.50.0.1/24
ListenPort = 51820
PrivateKey = SERVER_PRIVATE_KEY

[Peer]
# 노트북
PublicKey = LAPTOP_PUBLIC_KEY
AllowedIPs = 10.50.0.2/32

[Peer]
# 휴대전화
PublicKey = PHONE_PUBLIC_KEY
AllowedIPs = 10.50.0.3/32

`Address`는 wg0 인터페이스에 붙을 VPN 주소다. 서버의 각 피어 `AllowedIPs`는 그 피어에게 배정한 주소를 `/32`로 제한한다. 두 피어에 같은 주소 범위를 중복 지정하지 않는다.

실제 개인키 문자열을 설정 파일에 직접 둘 수 있지만 파일 권한을 반드시 제한한다.

sudo chmod 600 /etc/wireguard/wg0.conf

환경과 배포 도구에 따라 `PrivateKey =` 값을 별도 비밀 파일에서 안전하게 주입할 수 있다. 일반 셸 환경변수는 프로세스와 로그에 노출될 수 있으므로 사용 중인 서비스 관리자의 비밀 기능을 검토한다.

클라이언트 설정

노트북이 VPN 안의 주소만 이용하는 분할 터널 설정이다.

[Interface]
Address = 10.50.0.2/24
PrivateKey = LAPTOP_PRIVATE_KEY

[Peer]
PublicKey = SERVER_PUBLIC_KEY
Endpoint = vpn.example.com:51820
AllowedIPs = 10.50.0.0/24
PersistentKeepalive = 25

`Endpoint`는 클라이언트가 도달할 수 있는 서버의 공인 도메인과 UDP 포트다. 서버가 NAT 뒤에 있다면 공유기에서 UDP 51820을 서버로 포트 포워딩해야 한다.

`PersistentKeepalive = 25`는 NAT 뒤 피어가 25초마다 패킷을 보내 매핑을 유지한다. 고정 공인 주소에서 직접 수신하는 서버에는 보통 필요 없다. 모든 피어에 무조건 넣을 필요도 없다.

설정을 앱에 가져오거나 Linux에서 인터페이스를 올린다.

sudo wg-quick up wg0
sudo wg show

내릴 때는 다음과 같다.

sudo wg-quick down wg0

AllowedIPs의 두 가지 역할

`AllowedIPs`는 이름 때문에 단순 방화벽 허용 목록으로 오해하기 쉽다. 보내기와 받기에서 두 역할을 한다.

  1. 송신할 때 목적지 주소가 어느 피어의 AllowedIPs에 들어가는지 보고 그 피어에게 암호화해 보낸다.
  2. 수신할 때 복호화한 패킷의 출발지 주소가 그 피어의 AllowedIPs에 들어가는지 검사한다.

클라이언트에서 `AllowedIPs = 10.50.0.0/24`이면 그 VPN 대역으로 가는 패킷만 서버 피어로 보낸다. 서버에서 노트북 피어를 `10.50.0.2/32`로 제한하면 노트북이 10.50.0.3인 척 보내는 패킷은 허용되지 않는다.

라우팅 목적과 피어 주소 검증이 같은 설정에 들어 있으므로 넓은 범위를 무심코 지정하지 않는다.

분할 터널

분할 터널은 특정 사설망만 VPN으로 보내고 일반 인터넷은 기존 연결을 사용한다.

AllowedIPs = 10.50.0.0/24, 10.60.0.0/16
10.50.0.0/24 → WireGuard
10.60.0.0/16 → WireGuard
그 밖의 인터넷 → 기존 Wi-Fi 또는 유선망

회사 내부 서비스나 집 서버에만 접속할 때 효율적이다. 그러나 일반 인터넷 트래픽은 VPN 서버를 통과하지 않으므로 공용 Wi-Fi에서 모든 트래픽 보호를 기대하면 안 된다.

전체 터널

전체 터널은 기본 경로를 WireGuard 피어로 보낸다.

AllowedIPs = 0.0.0.0/0, ::/0

IPv4와 IPv6의 모든 목적지가 VPN으로 간다. 서버는 받은 패킷을 인터넷으로 전달하고 보통 NAT도 수행해야 한다. IPv4만 준비한 서버에 `::/0`까지 넣으면 IPv6가 작동하지 않거나 누출 방지 정책에 따라 연결이 끊길 수 있다. IPv6를 함께 설계하거나 의도적으로 제외한다.

`wg-quick`은 전체 터널에서 정책 라우팅을 사용해 WireGuard의 Endpoint 자체가 다시 터널 안으로 들어가는 순환을 피한다. 수동 라우팅을 구성한다면 이 예외 경로도 고려해야 한다.

서버의 IP 포워딩

VPN 피어가 서버 너머의 네트워크나 인터넷으로 가려면 서버가 패킷을 전달해야 한다.

sudo sysctl -w net.ipv4.ip_forward=1

재부팅 뒤에도 유지하려면 `/etc/sysctl.d/` 아래 설정 파일에 넣는다.

net.ipv4.ip_forward = 1

IPv6 전달이 필요하면 별도로 `net.ipv6.conf.all.forwarding = 1`을 검토한다. 포워딩을 켜는 것만으로 접근이 모두 허용되는 것은 아니다. 방화벽의 FORWARD 정책이 허용해야 한다.

인터넷 공유를 위한 NAT

전체 터널 클라이언트가 서버의 공인 인터페이스를 통해 인터넷에 나가려면, 목적지에서 돌아오는 경로를 모르는 환경에서 보통 source NAT를 사용한다.

nftables의 개념 예시는 다음과 같다. 실제 인터페이스 이름은 서버에서 확인한다.

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

포워딩 필터도 별도로 허용한다.

 table inet wg_filter {
    chain forward {
        type filter hook forward priority filter; policy drop;
        iifname "wg0" oifname "eth0" accept
        iifname "eth0" oifname "wg0" ct state established,related accept
    }
}

배포판이 firewalld, ufw, 클라우드 방화벽을 사용한다면 그 관리 계층과 섞어 규칙을 중복시키지 않는다.

iptables를 사용하는 환경의 흔한 예시는 다음과 같지만, 시스템이 nftables 백엔드를 쓰는지 먼저 확인한다.

iptables -A FORWARD -i wg0 -o eth0 -j ACCEPT
iptables -A FORWARD -i eth0 -o wg0 -m conntrack \
  --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -t nat -A POSTROUTING -s 10.50.0.0/24 -o eth0 -j MASQUERADE

방화벽 규칙을 `PostUp`과 `PostDown`에 넣는 예제도 많지만, 인터페이스 재시작 때 중복되거나 다른 방화벽 관리 도구와 충돌할 수 있다. 운영에서는 지속적인 방화벽 설정으로 관리하고 변경 이력을 남기는 편이 안전하다.

내부 LAN으로 접근하기

서버가 `192.0.2.0/24` 같은 내부망에 연결되어 있고 VPN 피어가 그 장치에 접근하게 하려면 클라이언트 AllowedIPs에 내부망을 추가한다.

AllowedIPs = 10.50.0.0/24, 192.0.2.0/24

서버는 wg0과 LAN 사이의 포워딩을 허용해야 한다. LAN 장치가 `10.50.0.0/24`로 돌아가는 경로를 알아야 한다.

가장 명확한 방식은 LAN 라우터에 다음 정적 경로를 넣는 것이다.

목적지 10.50.0.0/24 → WireGuard 서버의 LAN 주소

라우터를 바꿀 수 없다면 WireGuard 서버에서 LAN 방향 NAT를 사용할 수 있지만, LAN 서버에서는 실제 VPN 피어 주소 대신 WireGuard 서버 주소만 보인다. 감사와 피어별 접근 제어가 필요하면 정적 경로가 낫다.

피어끼리 통신시키기

클라이언트 A가 클라이언트 B에 접속하려면 세 조건이 필요하다.

  • A의 경로가 B의 VPN 주소를 서버 피어로 보내야 한다.
  • 서버가 wg0에서 wg0로 패킷을 포워딩하도록 방화벽이 허용해야 한다.
  • 서버의 각 피어 AllowedIPs가 A와 B 주소를 정확히 구분해야 한다.

모든 피어가 서로 통신할 필요가 없다면 방화벽에서 필요한 조합만 허용한다. 같은 VPN 대역이라는 이유로 자동으로 신뢰하지 않는다.

DNS 설정

클라이언트가 VPN 내부 도메인을 해석해야 하면 WireGuard 앱 또는 `wg-quick` 설정에 DNS 서버를 지정할 수 있다.

[Interface]
Address = 10.50.0.2/24
DNS = 10.50.0.1

Linux의 DNS 적용 방식은 배포판과 resolvconf/systemd-resolved 구성에 따라 다르다. `wg` 자체는 DNS를 설정하지 않고 `wg-quick`이나 GUI가 처리한다.

분할 DNS가 필요하면 특정 도메인만 내부 DNS로 보내도록 systemd-resolved나 운영체제의 VPN DNS 기능을 구성한다. 단일 `DNS =` 설정이 모든 질의를 VPN으로 바꿀 수 있는지 확인한다.

전체 터널에서 DNS만 기존 네트워크로 나가면 접속 도메인이 노출될 수 있다. IP 경로와 DNS 경로를 함께 검사한다.

MTU 문제

WireGuard는 원래 IP 패킷에 암호화 헤더를 더한다. 경로 MTU가 작은데 인터페이스 MTU가 크면 일부 큰 패킷만 멈추는 현상이 생길 수 있다.

증상은 작은 ping과 SSH 접속은 되는데 큰 웹 응답이나 파일 전송이 멈추는 형태다. `wg-quick`이 자동으로 정하지만, 클라우드·PPPoE·다른 VPN 위에서 문제가 있으면 낮춰 시험한다.

[Interface]
MTU = 1380

무조건 1280이나 1380으로 고정하기보다 실제 경로와 증상을 측정한다. `ping`의 do-not-fragment 옵션은 운영체제마다 문법이 다르다.

인터페이스 자동 시작

systemd를 사용하는 Linux에서 부팅 시 wg0를 올릴 수 있다.

sudo systemctl enable --now wg-quick@wg0

상태와 로그를 확인한다.

systemctl status wg-quick@wg0
journalctl -u wg-quick@wg0

설정을 바꾸고 단순 재시작하면 현재 원격 관리 연결이 끊길 수 있다. 원격 서버의 기본 경로와 방화벽을 바꿀 때는 콘솔 복구 수단을 준비하고, 기존 SSH 세션을 유지한 채 새 연결을 시험한다.

실행 중 피어 설정만 안전하게 동기화하려면 `wg syncconf`를 사용할 수 있지만, 주소와 라우팅·방화벽은 별도 계층이다. 무엇이 `wg` 설정이고 무엇이 운영체제 네트워크 설정인지 구분한다.

상태 확인

sudo wg show

주요 항목은 다음과 같다.

항목해석
latest handshake마지막으로 키 교환에 성공한 시각이다.
endpoint현재 알려진 상대의 공인 주소와 UDP 포트다.
transfer주고받은 암호화 트래픽 바이트다.
persistent keepaliveNAT 유지를 위한 전송 간격이다.

핸드셰이크가 없으면 공개키, Endpoint, UDP 방화벽, 포트 포워딩을 먼저 본다. 핸드셰이크는 있는데 통신이 안 되면 AllowedIPs, 라우팅, 포워딩, 방화벽, NAT를 본다.

ip address show wg0
ip route
ip route get 10.50.0.1
ss -lunp | rg ':51820'

`ip route get`은 특정 목적지에 실제 어떤 인터페이스와 출발지 주소를 선택하는지 보여 준다.

tcpdump로 경로 나누기

서버에서 암호화 외부 패킷과 복호화 내부 패킷을 각각 볼 수 있다.

sudo tcpdump -ni eth0 udp port 51820
sudo tcpdump -ni wg0

외부 인터페이스에는 WireGuard UDP 패킷이, wg0에는 복호화된 IP 패킷이 보인다. 외부 UDP만 보이고 wg0에 아무것도 없으면 키나 AllowedIPs를 확인한다. wg0에서 요청이 보이는데 목적지 인터페이스로 나가지 않으면 포워딩과 방화벽을 본다.

실제 패킷 캡처에는 내부 주소와 서비스 내용이 포함될 수 있다. 공유하기 전에 개인정보와 토큰을 제거한다.

자주 생기는 문제

핸드셰이크가 전혀 없다

  • 서버 Endpoint 주소와 UDP 포트가 맞는지 확인한다.
  • 서버의 UDP 포트가 방화벽과 클라우드 보안 그룹에서 열렸는지 확인한다.
  • NAT 뒤 서버라면 공유기 포트 포워딩을 확인한다.
  • 양쪽 공개키가 서로의 실제 개인키와 짝인지 확인한다.
  • 서버 시간이 크게 틀어지지 않았는지 확인한다.

핸드셰이크는 있지만 ping이 안 된다

  • 양쪽 AllowedIPs가 서로의 VPN 주소를 포함하는지 확인한다.
  • 같은 주소를 두 피어에 중복 배정하지 않았는지 확인한다.
  • 로컬 방화벽이 wg0 입력을 막는지 확인한다.
  • `ip route get`으로 다른 네트워크가 더 구체적인 경로를 차지하는지 본다.

VPN 서버까지 되지만 인터넷이 안 된다

  • 서버의 IPv4/IPv6 포워딩을 확인한다.
  • FORWARD 방화벽 규칙을 확인한다.
  • NAT 또는 반환 정적 경로를 확인한다.
  • DNS 문제인지 IP 연결 문제인지 숫자 IP와 도메인을 나눠 시험한다.

일정 시간이 지나면 끊긴다

NAT 뒤 피어에 `PersistentKeepalive = 25`를 설정하고, 중간 방화벽의 UDP idle timeout을 확인한다. 핸드셰이크가 최신이어도 애플리케이션 세션은 별도 timeout이 있을 수 있다.

키 교체와 피어 제거

키를 교체할 때는 새 공개키를 상대 설정에 배포한 뒤 해당 피어 설정을 원자적으로 바꿀 절차를 정한다. 개인키를 여러 장치가 공유하면 어느 장치가 접속했는지 구분하기 어렵고 하나가 노출될 때 모두 교체해야 한다. 장치마다 별도 키와 VPN 주소를 준다.

퇴사자나 분실 장치의 피어는 서버 설정에서 공개키와 AllowedIPs를 즉시 제거한다. 단순히 클라이언트 설정만 지우는 것으로 서버 접근 권한이 취소되지 않는다.

WireGuard에는 인증서 만료나 사용자 계정 만료가 기본으로 없으므로, 키 인벤토리와 정기 검토가 중요하다.

보안 점검

  • 각 장치에 고유한 개인키와 VPN 주소를 준다.
  • 개인키와 PSK 파일은 최소 권한으로 보관한다.
  • 피어 AllowedIPs를 필요한 주소 범위로 제한한다.
  • VPN에 들어왔다고 모든 내부 서비스 접근을 허용하지 않는다.
  • 서버의 UDP 포트만 공개하고 관리 서비스는 VPN 주소에 묶는다.
  • 전체 터널의 IPv4, IPv6, DNS 누출 여부를 함께 확인한다.
  • 로그와 설정을 공유할 때 개인키, PSK, 실제 공인 주소를 가린다.
  • 방화벽과 라우팅 변경 전에 원격 복구 경로를 준비한다.

WireGuard 구성은 줄 수가 적어서 쉬워 보이지만, 실제 동작은 키, AllowedIPs, 운영체제 라우팅, 포워딩, 방화벽, NAT의 결합이다. 핸드셰이크와 데이터 경로를 분리해 확인하면 문제를 빠르게 찾을 수 있다.