Cách tự cài WireGuard VPN trên VPS Linux
Hướng dẫn cài đặt WireGuard trên VPS, từ tạo key, cấu hình wg0.conf đến xử lý lỗi IP forwarding và NAT. Phù hợp cho các hệ thống dùng kernel Linux 5.6+.
Những gì bạn đang xây dựng
Một WireGuard VPN trên server bạn sở hữu chỉ tốn khoảng bốn mươi dòng config: một cặp key, một file interface, một sysctl, một rule NAT, và một lỗ hổng firewall. Việc cài đặt rất đơn giản, nên phần lớn hướng dẫn này sẽ tập trung vào những lỗi thường gặp — quyền hạn của key, AllowedIPs, forwarding và DNS.
WireGuard là một Layer 3 tunnel chạy trong kernel, đã có sẵn từ Linux 5.6, nên Ubuntu 24.04 và Debian 13 hỗ trợ nó mà không cần module bên ngoài. Không thương lượng cipher, không có certificate authority, không có bước nhập username/password: một peer thực chất là một public key cộng với các địa chỉ IP mà key đó được phép sử dụng. Một packet nếu thất bại khi kiểm tra MAC sẽ bị drop mà không có phản hồi, vì vậy port này sẽ không trả lời các lệnh scan. Mặt khác: không có server xác thực (auth server), nên để thu hồi quyền truy cập, bạn chỉ cần xóa peer đó trên máy.
Kiểm tra ảo hóa trước tiên
WireGuard cần một kernel mà bạn có thể load module vào, và trên một KVM VPS thì nó hoạt động ngay lập tức. Trên các loại ảo hóa container dùng chung kernel với host — như OpenVZ, LXC — lệnh đầu tiên sẽ lỗi với RTNETLINK answers: Operation not supported, và phương án dự phòng là implementation userspace của wireguard-go. Hãy kiểm tra bằng sudo modprobe wireguard && echo ok trước.
Tạo keys mà không làm lộ chúng
Một file /etc/wireguard/server.key có quyền read cho mọi user cũng giống như việc không dùng VPN vậy. Dòng umask 077 && wg genkey | sudo tee ... thông thường là không đáng tin cậy, vì sudo áp dụng umask riêng của nó lên file mà tee tạo ra. Hãy thiết lập mode một cách tường minh.
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, chỉ mất một dòng trong mỗi config.
Interface của 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 nó; một cảnh báo khi khởi động rằng file này có thể bị truy cập bởi mọi user nghĩa là bạn đã bỏ qua bước trên. Address là địa chỉ của server bên trong tunnel, mang subnet mask của toàn bộ mạng VPN. Hãy chọn một dải IP mà bạn sẽ không gặp ở môi trường bên ngoài — 192.168.1.0/24 sẽ bị xung đột với một nửa số router gia đình mà client của bạn đang kết nối, và khi đó tunnel sẽ bị mất kết nối một cách âm thầm do trùng route local.
AllowedIPs của một peer ở phía server là một /32, tức là địa chỉ tunnel duy nhất mà client đó sở hữu. Nếu bạn cấp cho hai peer cùng một IP được phép (allowed IP), nó sẽ chuyển sang peer được cấu hình cuối cùng, và peer đầu tiên sẽ ngừng nhận traffic mà không có lỗi nào được in ra. Hãy để SaveConfig trống, nếu không wg-quick down sẽ ghi đè file này từ trạng thái đang chạy.
Biến máy thành một router
Một Linux server sẽ drop các packet không gửi đến địa chỉ của nó. Mặc định, cả forwarding và source NAT đều chưa được bật.
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 file sysctl -w trần trụi sẽ hoạt động cho đến khi reboot lần kế tiếp, sau đó nó sẽ ngừng hoạt động một cách âm thầm. NAT cần interface egress — tức là NIC kết nối ra internet, chứ không phải wg0. Đừng giả định là eth0; hãy lấy thông tin từ ip route show default, vì các image hiện tại thường dùng tên như enp1s0 hoặc ens3.
Firewall: port và đường dẫn forward
Một file nftables sẽ đảm nhận cả filter và NAT. Hãy viết /etc/nftables.conf — nó sẽ flush (xóa sạch) ruleset hiện tại, vì vậy hãy bỏ qua bước này nếu máy đã được quản lý bởi ufw hoặc 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
}
}Áp dụng nó bằng sudo systemctl enable --now nftables, và hãy giữ một session SSH thứ hai đang mở: policy drop cộng với một lỗi gõ sai trong rule SSH sẽ khiến bạn bị khóa khỏi chính server của mình. Hãy lưu ý những gì forward chain không cho phép — wg0 đến wg0. Các peer có thể truy cập internet, chứ không phải truy cập lẫn nhau; hãy thêm iifname "wg0" oifname "wg0" accept nếu bạn muốn làm VPN peer-to-peer. Cùng một chain này cũng quản lý những gì một peer có thể chạm tới trên chính server, điều này rất quan trọng khi server đóng vai trò là một remote development box chạy Claude Code trong tmux và bạn không muốn expose phần đó ra công chúng.
Trên máy dùng ufw: ufw allow 51820/udp, DEFAULT_FORWARD_POLICY="ACCEPT" trong /etc/default/ufw, và một rule POSTROUTING MASQUERADE *nat ở đầu file /etc/ufw/before.rules.
Khởi chạy với systemd
sudo systemctl enable --now wg-quick@wg0
sudo wg showwg-quick sẽ tạo interface, thêm các địa chỉ và cài đặt các route được lấy từ AllowedIPs. enable --now là phần quan trọng: một lệnh wg-quick up wg0 chạy tay sẽ mất đi sau khi reboot, và việc nâng cấp kernel cũng đồng nghĩa với việc phải reboot.
Config của client, và thiết lập mà mọi người thường làm 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 hai nhiệm vụ khác nhau cùng một lúc, và việc nhầm lẫn giữa chúng là nguyên nhân gây ra hầu hết các sự bối rối về WireGuard.
Về chiều outbound, nó là một routing table. Một packet có đích đến 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 — một full tunnel, với server là default route. Một split tunnel là một danh sách hẹp hơn: AllowedIPs = 10.8.0.0/24, 10.20.0.0/16 mang traffic VPN cộng với một mạng nội bộ đằng sau server, và mọi thứ khác vẫn giữ route local của nó. Danh sách hẹp đó chính là thứ cho phép bạn giữ các dịch vụ hoàn toàn tách biệt khỏi internet công cộng — một private Nextcloud instance trên một VPS gắn với địa chỉ tunnel, hoặc các nested-virtualisation lab VMs chạy trên cùng một máy, sẽ luôn khả dụng với các peer và vô hình với tất cả những người khác.
Về chiều inbound, nó là một access-control list. Một packet đã giải mã từ một peer có source address không nằm trong AllowedIPs của peer đó sẽ bị drop. Đó là lý do tại sao server liệt kê 10.8.0.2/32 cho laptop: một entry 0.0.0.0/0 ở đó sẽ cho phép client đó giả mạo bất kỳ địa chỉ nào trong tunnel.
PersistentKeepalive dành cho các client nằm sau NAT, nơi router chỉ giữ mapping UDP mở khi có packet đang chảy qua. Khi nó hết hạn, server không thể kết nối tới client được nữa. PersistentKeepalive = 25 giữ cho mapping luôn mở — hãy thiết lập nó ở phía client, đừng thiết lập trên một server có IP public.
DNS, và lỗi rò rỉ mà không ai để ý
Với AllowedIPs = 0.0.0.0/0 và không có dòng DNS =, client sẽ giữ resolver mà nó học được từ mạng local — ví dụ router ở quán cafe tại 192.168.1.1. Route đó có độ ưu tiên cao hơn default route, nên các truy vấn DNS sẽ đi ra qua link local dưới dạng cleartext trong khi mọi thứ khác đều được tunnelled. Traffic thì riêng tư; nhưng danh sách các tên miền thì không.
Có hai lựa chọn trung thực. Trỏ DNS đến một public resolver (DNS = 9.9.9.9) và các truy vấn sẽ đi qua tunnel và thoát ra từ server của bạn, mặc dù resolver đó vẫn thấy chúng. Hoặc chạy unbound hoặc dnsmasq gắn với 10.8.0.1, thiết lập DNS = 10.8.0.1, và thêm udp dport 53 iifname "wg0" accept vào input chain — thiết lập dòng đó và quên resolver đi, và sẽ không có gì resolve được cả.
Trên các client Linux, wg-quick áp dụng DNS thông qua resolvconf; nếu thiếu nó bạn sẽ gặp resolvconf: command not found. Hãy cài đặt openresolv, hoặc thiết lập PostUp = resolvectl dns %i 10.8.0.1 trên một client sử dụng systemd-resolved.
Thêm và xóa peer mà không làm ngắt tunnel
Restart interface để thêm một user sẽ làm ngắt kết nối của tất cả những người đang kết nối. Hãy thêm block [Peer] vào wg0.conf, sau đó reload set peer ngay tại chỗ.
sudo bash -c 'wg syncconf wg0 <(wg-quick strip wg0)'wg-quick strip in ra config mà không có các keys chỉ dùng cho wg-quick (Address, DNS, PostUp), và syncconf áp dụng các thay đổi trong khi các session đang chạy vẫn được duy trì. Nó chỉ cập nhật các peer: một thay đổi về Address vẫn yêu cầu phải down/up lại hoàn toàn. Thu hồi quyền bằng sudo wg set wg0 peer <public key> remove, sau đó xóa block đó khỏi file nếu không nó sẽ xuất hiện lại ở lần reload kế tiếp.
Các lỗi thường gặp, kèm theo các thông báo bạn sẽ thấy
Handshake không bao giờ hoàn tất. wg show liệt kê peer mà không có latest handshake, và log của client báo:
Handshake for peer 1 (10.0.0.10:51820) did not complete after 5 seconds, retrying (try 2)Không có gì được gửi đến, hoặc không có gì được chấp nhận. Hãy kiểm tra theo thứ tự: UDP 51820 đã mở trên firewall của VPS và trên firewall mạng của nhà cung cấp (một thiết lập riêng biệt trên hầu hết các panel) chưa; địa chỉ Endpoint và port đã đúng chưa; các key có bị đổi chỗ cho nhau không. Key trong block [Peer] của client phải là public key của server, và ngược lại — việc dán private key, hoặc chính public key của client, sẽ gây ra đúng triệu chứng này. sudo tcpdump -ni any udp port 51820 trên server sẽ cho biết liệu các packet có đến được hay không. Kernel module mặc định không log gì cả; các message của WireGuard chỉ xuất hiện trong dmesg sau khi bạn bật dynamic debug (echo module wireguard +p | sudo tee /sys/kernel/debug/dynamic_debug/control), và khi đã bật, một lỗi sai key sẽ hiển thị là invalid-MAC drop.
Handshake thành công, nhưng không có internet. ping 10.8.0.1 thành công nhưng ping 1.1.1.1 bị timeout: do thiếu forwarding hoặc NAT. Kiểm tra xem sysctl net.ipv4.ip_forward có đọc được 1 không, sau đó theo dõi các counter trong khi client ping, với sudo nft list ruleset hoặc sudo iptables -t nat -L POSTROUTING -n -v. Nếu counter của rule masquerade bằng 0 nghĩa là tên interface egress bị sai; nếu counter tăng nhưng không có phản hồi thì vấn đề nằm ở policy của forward chain.
Internet hoạt động, 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 nó đang trỏ đến một resolver không thể truy cập được từ bên trong tunnel.
Một số trang HTTPS bị treo. SSH và ping vẫn ổn; các trang nặng thì bị đứng. Đó là lỗi path MTU: tunnel thêm overhead, và một link nào đó ở giữa sẽ drop các packet quá khổ mà không gửi lại thông báo ICMP. Hãy giảm MTU trên client [Interface] — thử 1420, sau đó 1380, rồi đến 1280.
Interface từ chối khởi động. Address already in use nghĩa là một process khác đang giữ port UDP 51820. Cannot find device wg0 sau khi up thất bại thường có nghĩa là config bị từ chối; hãy đọc journalctl -u wg-quick@wg0 -n 50.
Chuyển đổi từ Streisand hoặc OpenVPN
Streisand không còn được bảo trì và repository đã bị lưu trữ, việc chạy VPN trên một công cụ tự động hóa đã bỏ hoang là một vấn đề bảo mật tiềm tàng. Không có cách nâng cấp tại chỗ, và PKI của OpenVPN không thể chuyển đổi sang: WireGuard không có certificate, không có CA và không có thời hạn hết hạn, vì vậy mỗi client sẽ nhận một cặp key mới hoàn toàn.
Hãy chuyển đổi song song — WireGuard trên UDP 51820 có thể tồn tại cùng lúc với OpenVPN trên 1194 trên cùng một máy. Hãy dựng wg0, chuyển từng client một, sau đó dừng dịch vụ cũ. Mô hình username/password và thu hồi (revocation) của OpenVPN không được chuyển sang; nếu bạn cần tài khoản hoặc nhật ký kiểm tra (audit trail), hãy xây dựng nó ở lớp phía trên WireGuard.
Backup, nâng cấp, và vấn đề khi mở rộng quy mô
/etc/wireguard chính là server. Hãy backup nó (sudo tar czf wg-backup.tgz -C /etc wireguard, mode 600, lưu trữ bên ngoài server) và bạn có thể dựng lại trên một VPS mới chỉ trong vài phút. Nếu mất private key của server, mọi config của client đều phải được cấp lại, vì client luôn cố định (pin) public key của server. Nâng cấp chỉ là một lệnh apt upgrade thông thường cộng với một lần reboot để cập nhật kernel, và wg-quick@wg0 sẽ tự động quay lại nếu bạn đã bật nó.
Trạng thái của mỗi peer rất nhỏ và crypto chạy trong kernel, nên giới hạn sẽ nằm ở CPU và băng thông của VPS chứ không phải ở bất kỳ thứ gì trong config này — hãy đo lường bằng iperf3 xuyên qua tunnel thay vì tin vào các con số quảng cáo. Thứ gây áp lực khi mở rộng quy mô là vận hành. Mỗi peer cần một IP tunnel duy nhất, và việc sửa tay sáu mươi block [Peer] sẽ khiến các lỗi trùng lặp AllowedIPs dễ dàng lọt vào: hãy tạo config từ một script. Một server là một UDP endpoint và là một điểm lỗi duy nhất (single point of failure), và WireGuard không có tính năng clustering: dự phòng nghĩa là bạn cần một server thứ hai với các key riêng. Việc xoay vòng key (key rotation) vẫn là thủ công, vì vậy hãy ghi chép lại ai giữ key nào và cách bạn thu hồi một key.
Tất cả những điều này cần một máy Linux mà bạn có quyền kiểm soát — một IP public, một kernel mà bạn có thể load module vào, và một firewall mà bạn nắm quyền kiểm soát từ đầu đến cuối.
FAQ
Tại sao WireGuard handshake không bao giờ hoàn tất?
wg show liệt kê một peer mà không có latest handshake nghĩa là các packet không đến được hoặc không được chấp nhận. Hãy 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 đã đúng, sau đó kiểm tra xem các key có bị đổi chỗ 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 thấy liệu các packet có đến được hay không; dmesg chỉ báo cáo các 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), và khi đó lỗi sai key sẽ hiển thị là invalid-MAC drop.
Tunnel đã kết nối nhưng tôi không có internet. Thiếu gì vậy?
ping 10.8.0.1 hoạt động trong khi ping 1.1.1.1 bị timeout cho thấy vấn đề ở forwarding hoặc NAT. Xác nhận sysctl net.ipv4.ip_forward đọc được 1 và nó đã được thiết lập trong /etc/sysctl.d/, chứ không chỉ là một sysctl -w sẽ mất khi reboot. Sau đó kiểm tra rule masquerade đang gọi tên interface egress thực của bạn từ ip route show default — enp1s0 hoặc ens3, hiếm khi là eth0.
Tôi có cần dòng DNS = trong config client không?
Với một full tunnel và không có dòng DNS =, client sẽ giữ resolver mà nó học được từ mạng local, và các truy vấn đó sẽ đi ra dưới dạng cleartext qua link local trong khi mọi thứ khác đều được tunnelled. Hãy trỏ DNS đến một public resolver, hoặc chạy unbound/dnsmasq gắn với 10.8.0.1 và mở udp dport 53 iifname "wg0" trong input chain.
AllowedIPs thực sự kiểm soát cái gì?
Nó làm hai việc. Về chiều outbound, nó là một routing table: traffic khớp với AllowedIPs của một peer sẽ được mã hóa và gửi đến peer đó. Về chiều inbound, nó là một access-control list: một packet đã giải mã có source nằm ngoài AllowedIPs của peer đó sẽ bị drop. Đó là lý do tại sao phía server liệt kê /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 bất kỳ VPS nào không?
Trên KVM VPS, nó hoạt động với module có sẵn trong kernel và không cần thiết lập thêm. Trên ảo hóa container dùng chung kernel với host, như OpenVZ hoặc LXC, modprobe wireguard sẽ lỗi với Operation not supported và phương án dự phòng là implementation userspace của wireguard-go. Hãy chạy sudo modprobe wireguard && echo ok trước bất kỳ việc gì khác.