Tự host WireGuard VPN trên VPS Linux
Hướng dẫn dựng WireGuard trên VPS Linux: tạo key, viết wg0.conf, bật IP forwarding và NAT, hiểu AllowedIPs, cấu hình DNS, xử lý lỗi handshake và modprobe.
Bạn đang xây dựng gì
Một VPN WireGuard trên server do bạn sở hữu chỉ cần khoảng 40 dòng cấu hình: một cặp key, một file interface, một sysctl, một rule NAT và một lỗ trên firewall. Cài đặt rất đơn giản, nên phần lớn hướng dẫn này tập trung vào những lỗi thường gặp, quyền của key, AllowedIPs, forwarding và DNS.
WireGuard là tunnel Layer 3 trong kernel, đã có trong mainline từ Linux 5.6. Vì vậy, Ubuntu 24.04 và Debian 13 có sẵn WireGuard mà không cần module bên ngoài. Không có bước thương lượng cipher, không có certificate authority và không có bước nhập username/password: một peer gồm public key cùng các địa chỉ IP mà key đó được phép sử dụng. Packet không vượt qua kiểm tra MAC sẽ bị drop mà không có phản hồi, nên port không trả lời các lượt scan. Mặt khác, không có auth server, nên muốn thu hồi quyền truy cập thì phải xóa peer trên máy.
Kiểm tra công nghệ ảo hóa trước
WireGuard cần một kernel cho phép load module. Trên VPS KVM, WireGuard hoạt động ngay sau khi cài. Với công nghệ ảo hóa dạng container dùng chung kernel của host như OpenVZ hoặc LXC, lệnh đầu tiên sẽ lỗi với RTNETLINK answers: Operation not supported. Khi đó, dùng implementation userspace wireguard-go. Trước tiên, kiểm tra bằng sudo modprobe wireguard && echo ok.
Tạo key mà không làm lộ chúng
/etc/wireguard/server.key có quyền đọc cho mọi người tương đương với việc không có VPN. Dòng umask 077 && wg genkey | sudo tee ... phổ biến này không đáng tin cậy, vì sudo áp dụng umask riêng cho file mà tee tạo. Hãy đặt mode một cách rõ ràng.
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.keyTạo cặp key cho client theo cách tương tự. wg genpsk thêm một pre-shared key tùy chọn, mỗi config một dòng.
Giao diện server: /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/32chmod 600 file này; cảnh báo lúc khởi động cho biết file có quyền truy cập toàn hệ thống nghĩa là bạn đã bỏ qua bước đó. Address là địa chỉ của server bên trong tunnel, kèm theo subnet mask của toàn bộ subnet VPN. Hãy chọn một dải địa chỉ không trùng với các mạng bạn có thể gặp bên ngoài; 192.168.1.0/24 xung đột với gần một nửa số router gia đình mà client của bạn có thể đang đứng sau, khiến tunnel âm thầm thua route cục bộ.
AllowedIPs của một peer ở phía server là /32, địa chỉ tunnel duy nhất mà client đó sở hữu. Nếu cấp cùng một allowed IP cho hai peer, địa chỉ này sẽ được chuyển sang peer được cấu hình sau cùng. Peer đầu tiên sẽ ngừng nhận traffic mà không có lỗi nào được ghi ở bất kỳ đâu. Để SaveConfig ở trạng thái chưa đặt, nếu không wg-quick down sẽ ghi đè file này từ trạng thái đang chạy.
Biến máy chủ thành router
Linux server sẽ loại bỏ các packet không được gửi đến chính nó. Mặc định, server chưa bật forwarding và 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_forwardMột cấu hình sysctl -w đơn giản chỉ hoạt động đến lần reboot tiếp theo rồi âm thầm ngừng hoạt động. NAT cần egress interface, tức NIC kết nối đến Internet, không phải wg0. Không được mặc định dùng eth0; hãy lấy tên interface của bạn từ ip route show default, vì các image hiện tại thường dùng những tên như enp1s0 hoặc ens3.
Tường lửa: cổng và đường chuyển tiếp
Một file nftables chứa cả filter và NAT. Khi chạy /etc/nftables.conf, lệnh này xóa sạch ruleset hiện có, vì vậy không dùng trên máy đã được ufw hoặc Docker quản lý.
#!/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
}
}Áp dụng bằng sudo systemctl enable --now nftables và giữ mở một phiên SSH thứ hai: policy drop cộng với lỗi đánh máy trong rule SSH có thể khiến bạn tự khóa mình khỏi server. Lưu ý forward chain không cho phép điều gì, wg0 đến wg0. Các peer có thể truy cập Internet nhưng không truy cập lẫn nhau; thêm iifname "wg0" oifname "wg0" accept nếu cần VPN peer-to-peer. Chain này cũng quyết định peer được phép truy cập gì trên chính server, điều này quan trọng khi máy còn được dùng làm máy phát triển từ xa chạy Claude Code trong tmux và bạn không muốn công khai phần đó.
Trên máy dùng ufw: ufw allow 51820/udp, DEFAULT_FORWARD_POLICY="ACCEPT" trong /etc/default/ufw, và thêm một rule POSTROUTING MASQUERADE bằng *nat ở đầu /etc/ufw/before.rules.
Khởi động bằng systemd
sudo systemctl enable --now wg-quick@wg0
sudo wg showwg-quick tạo interface, thêm các địa chỉ và cài route dựa trên AllowedIPs. enable --now là phần quan trọng: wg-quick up wg0 chạy thủ công sẽ mất sau lần reboot tiếp theo, và việc nâng cấp kernel cũng yêu cầu reboot. Một unit không khởi động lại sau một trong các lần reboot đó sẽ không phát cảnh báo cho đến khi có người thử kết nối. Vì vậy, một cấu hình OnFailure= trong wg-quick@wg0 trỏ đến ntfy server của bạn là cách rẻ nhất để nhận thông báo trên điện thoại, thay vì phải chờ một user bị mất quyền truy cập báo lại.
Cấu hình client và thiết lập mà mọi người thường cấu hình sai
[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 = 25AllowedIPs thực hiện đồng thời hai chức năng khác nhau. Việc nhầm lẫn hai chức năng này là nguyên nhân của phần lớn vấn đề với WireGuard.
Ở chiều outbound, đây là một routing table. Packet có destination khớp với AllowedIPs của một peer sẽ được mã hóa và gửi đến peer đó. 0.0.0.0/0, ::/0 gửi mọi thứ qua tunnel: full tunnel, với server làm default route. Split tunnel là một danh sách hẹp hơn: AllowedIPs = 10.8.0.0/24, 10.20.0.0/16 chuyển traffic VPN cùng một private network phía sau server, còn mọi traffic khác tiếp tục dùng local route. Danh sách hẹp này cho phép bạn hoàn toàn không đưa các service lên public internet, chẳng hạn một instance Nextcloud riêng trên VPS bind vào địa chỉ tunnel hoặc các VM của lab nested-virtualisation chạy trên cùng máy vẫn có thể được các peer truy cập nhưng không hiển thị với những người khác.
Ở chiều inbound, đây là một access-control list. Packet đã giải mã từ một peer sẽ bị drop nếu source address của nó không nằm trong AllowedIPs của peer đó. Vì vậy, server khai báo 10.8.0.2/32 cho laptop: nếu đặt 0.0.0.0/0 ở đó, client này có thể spoof mọi địa chỉ trong tunnel.
PersistentKeepalive dùng cho client nằm sau NAT, nơi router chỉ giữ UDP mapping mở khi vẫn có packet truyền qua. Khi mapping hết hạn, server không thể reach client nữa. PersistentKeepalive = 25 giữ mapping luôn mở; hãy đặt tùy chọn này trên client, không phải trên server có public IP.
DNS và rò rỉ mà không ai nhận ra
Với AllowedIPs = 0.0.0.0/0 và không có dòng DNS =, client tiếp tục dùng resolver nhận được từ mạng cục bộ, tức router của quán cà phê tại 192.168.1.1. Route này cụ thể hơn default route, nên các truy vấn DNS đi qua liên kết cục bộ dưới dạng plaintext, trong khi mọi lưu lượng khác đi qua tunnel. Lưu lượng được giữ riêng tư; danh sách hostname thì không.
Có 2 lựa chọn rõ ràng. Trỏ DNS đến một public resolver (DNS = 9.9.9.9); các truy vấn sẽ đi qua tunnel và thoát ra từ server của bạn, nhưng resolver đó vẫn nhìn thấy chúng. Hoặc chạy unbound hoặc dnsmasq bind vào 10.8.0.1, đặt DNS = 10.8.0.1 và thêm udp dport 53 iifname "wg0" accept vào input chain; đặt dòng đó rồi bỏ qua resolver, khi đó sẽ không có gì resolve được.
Trên client Linux, wg-quick áp dụng DNS qua resolvconf; nếu thiếu, bạn sẽ nhận resolvconf: command not found. Cài đặt openresolv hoặc đặt PostUp = resolvectl dns %i 10.8.0.1 trên client dùng systemd-resolved.
Thêm và xóa peer mà không làm gián đoạn tunnel
Restart interface để thêm user sẽ ngắt kết nối của mọi người. Thêm block [Peer] vào wg0.conf, sau đó reload tập peer trực tiếp.
sudo bash -c 'wg syncconf wg0 <(wg-quick strip wg0)'wg-quick strip in cấu hình mà không có các key chỉ dùng cho wg-quick (Address, DNS, PostUp), còn syncconf áp dụng phần khác biệt trong khi các session đang hoạt động vẫn được giữ nguyên. Lệnh này chỉ cập nhật peer: nếu thay đổi Address thì vẫn phải down/up hoàn toàn. Revoke bằng sudo wg set wg0 peer <public key> remove, sau đó xóa block khỏi file; nếu không, block sẽ xuất hiện lại trong lần reload tiếp theo.
Các lỗi thường gặp và chuỗi bạn sẽ thấy
Handshake không bao giờ hoàn tất. wg show liệt kê peer nhưng không có latest handshake, còn log của client ghi:
Handshake for peer 1 (10.0.0.10:51820) did not complete after 5 seconds, retrying (try 2)Không có gói tin nào đến, hoặc không có gói nào được chấp nhận. Kiểm tra theo thứ tự: UDP 51820 đã được mở trên firewall của VPS và firewall mạng của nhà cung cấp chưa; trên hầu hết panel đây là hai phần cấu hình riêng; địa chỉ và port Endpoint có đúng không; các key có bị tráo không. Key trong block [Peer] của client phải là public key của server và ngược lại. Dán private key hoặc public key của chính client sẽ gây đúng triệu chứng này. sudo tcpdump -ni any udp port 51820 trên server cho biết packet có đến hay không. Kernel module mặc định không ghi log; thông báo của WireGuard chỉ xuất hiện trong dmesg sau khi bật dynamic debug (echo module wireguard +p | sudo tee /sys/kernel/debug/dynamic_debug/control). Khi đã bật, lỗi không khớp key sẽ hiện dưới dạng packet bị loại vì MAC không hợp lệ.
Handshake hoạt động nhưng không có Internet. ping 10.8.0.1 thành công nhưng ping 1.1.1.1 timeout: chưa cấu hình forwarding hoặc NAT. Kiểm tra sysctl net.ipv4.ip_forward có giá trị 1, rồi theo dõi các counter trong lúc client ping bằng sudo nft list ruleset hoặc sudo iptables -t nat -L POSTROUTING -n -v. Nếu rule masquerade có 0 packet thì tên interface egress bị sai. Nếu counter tăng nhưng không có reply thì nguyên nhân nằm ở policy của forward chain.
Có Internet nhưng không phân giải được tên miền. ping 1.1.1.1 thành công và curl https://example.com trả về Could not resolve host. Dòng DNS bị thiếu hoặc chỉ đến một resolver không thể truy cập từ bên trong tunnel.
Một số site HTTPS bị treo. SSH và ping vẫn hoạt động nhưng các trang lớn bị đứng. Đây là vấn đề path MTU: tunnel thêm overhead, và một link ở giữa có thể loại các packet quá lớn mà không gửi được thông báo ICMP về. Giảm MTU trên [Interface] của client, thử 1420, sau đó 1380, rồi 1280. Nếu giảm MTU làm hết hiện tượng treo nhưng throughput vẫn thấp, không nên tiếp tục đoán các giá trị tròn. Hãy làm theo cách tìm path MTU thực tế bằng phương pháp chia đôi và giới hạn TCP MSS. Cách này cũng loại trừ các nguyên nhân hoàn toàn không liên quan đến tunnel.
Interface từ chối khởi động. Address already in use nghĩa là một process khác đang giữ UDP 51820. Cannot find device wg0 sau một lần up thất bại thường nghĩa là cấu hình bị từ chối; hãy đọc journalctl -u wg-quick@wg0 -n 50.
Di chuyển từ Streisand hoặc OpenVPN
Streisand không còn được duy trì và repository của dự án đã bị lưu trữ. Chạy VPN trên hệ thống automation bị bỏ ngỏ là một rủi ro bảo mật tích lũy chậm. Không có cách nâng cấp tại chỗ. PKI của OpenVPN không thể chuyển đổi: WireGuard không dùng certificate, CA hay thời hạn hết hạn, nên mỗi client cần một key pair mới.
Hãy di chuyển song song. WireGuard trên UDP 51820 có thể chạy cùng OpenVPN trên 1194 trên cùng một máy. Dựng wg0, chuyển từng client một, rồi dừng service cũ. Mô hình username/password và thu hồi của OpenVPN không được giữ nguyên khi chuyển sang WireGuard. Nếu cần quản lý account hoặc audit trail, hãy triển khai lớp đó phía trên WireGuard.
Sao lưu, nâng cấp và những điểm gây áp lực khi mở rộng
/etc/wireguard chính là server. Hãy sao lưu nó (sudo tar czf wg-backup.tgz -C /etc wireguard, mode 600 và lưu bên ngoài server), rồi bạn có thể dựng lại trên một VPS mới trong vài phút. Nếu mất private key của server, bạn phải cấp lại cấu hình cho mọi client vì client ghim public key của server. Nâng cấp chỉ là một apt upgrade thông thường rồi reboot để cập nhật kernel, và wg-quick@wg0 sẽ tự khởi động lại nếu bạn đã bật nó.
State của từng peer rất nhỏ và crypto chạy trong kernel, nên giới hạn chủ yếu là CPU và băng thông mà VPS của bạn được cấp, không phải nội dung trong config này. Hãy đo bằng iperf3 qua tunnel thay vì tin vào một con số được công bố. Khi mở rộng, vấn đề nằm ở vận hành. Mỗi peer cần một tunnel IP duy nhất. Việc sửa thủ công 60 block [Peer] rất dễ làm xuất hiện AllowedIPs trùng lặp: hãy sinh config bằng script. Một server tương ứng với một UDP endpoint và một điểm lỗi duy nhất, còn WireGuard không có clustering. Muốn có dự phòng, bạn cần một server thứ hai với bộ key riêng. Việc xoay vòng key vẫn phải làm thủ công, nên hãy ghi lại ai đang giữ key nào và cách revoke từng key. Khi việc quản lý này vượt quá khả năng của một text file, cách thường dùng là đặt một control plane lên trên cùng data plane trong kernel. Một server NetBird tự host sẽ xử lý việc cấp phát địa chỉ, phân phối peer và setup key mà nếu không bạn phải làm thủ công. Nếu tự chạy control plane khiến bạn phải quản lý thêm một box, Tailscale có thể host control plane đó cho bạn. Gói miễn phí của Tailscale hỗ trợ 6 user với số lượng device không giới hạn, đủ để hầu hết hệ thống cá nhân không phải trả phí. Sau mốc đó, chi phí phụ thuộc vào số người vận hành hơn là số máy. Vì vậy, chi phí thực tế mà một hộ gia đình hoặc team nhỏ phải trả phụ thuộc vào số người có login, không phải số peer mà bạn phải sửa thủ công trong wg0.conf. Ở mô hình đó, việc quản lý AllowedIPs cho split tunnel trở thành quảng bá các private range từ một subnet router, được announce một lần từ một VPS và phê duyệt tập trung, thay vì paste vào file của từng client. Quyết định có nên chuyển sang mô hình này hay không phụ thuộc vào phạm vi mà control plane được host thực sự có thể truy cập. Control plane đó không bao giờ giữ các key dùng để mã hóa traffic của bạn, nhưng nó quyết định peer nào biết về nhau.
Tất cả mô hình này cần một Linux box do bạn kiểm soát, một public IP, một kernel có thể load module và một firewall mà bạn quản lý từ đầu đến cuối.
FAQ
Vì sao handshake của WireGuard không bao giờ hoàn tất?
wg show hiển thị một peer không có latest handshake nghĩa là các packet không đến được hoặc không được chấp nhận. Kiểm tra UDP 51820 trên cả firewall của VPS và firewall mạng riêng của nhà cung cấp, xác nhận host và port Endpoint, sau đó kiểm tra các key có bị đảo cho nhau không. Block [Peer] của client phải chứa public key của server. sudo tcpdump -ni any udp port 51820 trên server cho biết packet có đến hay không; dmesg chỉ báo lỗi handshake của WireGuard sau khi bạn bật dynamic debug (echo module wireguard +p | sudo tee /sys/kernel/debug/dynamic_debug/control), khi đó key không khớp sẽ xuất hiện dưới dạng packet bị drop do MAC không hợp lệ.
Tunnel kết nối nhưng tôi không có Internet. Còn thiếu gì?
ping 10.8.0.1 hoạt động trong khi ping 1.1.1.1 bị timeout cho thấy vấn đề nằm ở forwarding hoặc NAT. Xác nhận sysctl net.ipv4.ip_forward có giá trị 1 và được thiết lập trong /etc/sysctl.d/, không chỉ bằng một sysctl -w sẽ mất hiệu lực sau khi reboot. Sau đó kiểm tra rule masquerade lấy đúng interface egress thực tế từ ip route show default, enp1s0 hoặc hiếm hơn là ens3, eth0.
Tôi có cần dòng DNS = trong cấu hình client không?
Với full tunnel và không có dòng DNS =, client tiếp tục dùng resolver mà nó nhận được từ mạng cục bộ. Các truy vấn DNS khi đó đi dưới dạng cleartext qua link cục bộ, còn mọi lưu lượng khác đi qua tunnel. Trỏ DNS đến một public resolver, hoặc chạy unbound/dnsmasq bind vào 10.8.0.1 và mở udp dport 53 iifname "wg0" trong input chain.
AllowedIPs thực sự kiểm soát điều gì?
Nó thực hiện 2 nhiệm vụ. Với lưu lượng outbound, đây là routing table: traffic khớp với AllowedIPs của một peer sẽ được mã hóa và gửi đến peer đó. Với lưu lượng inbound, đây là access-control list: packet sau khi giải mã có source nằm ngoài AllowedIPs của peer đó sẽ bị drop. Vì vậy phía server liệt kê một /32 cho mỗi client, trong khi phía client có thể liệt kê 0.0.0.0/0.
WireGuard có chạy được trên mọi VPS không?
Trên KVM VPS, WireGuard chạy với kernel module và không cần thiết lập thêm. Trên container virtualisation dùng chung kernel của host, như OpenVZ hoặc LXC, modprobe wireguard sẽ fail với Operation not supported; phương án thay thế là implementation userspace wireguard-go. Chạy sudo modprobe wireguard && echo ok trước mọi thao tác khác.