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
Một /etc/wireguard/server.key cho phép mọi người đọc tương đương với việc không có VPN. Dòng umask 077 && wg genkey | sudo tee ... phổ biến vẫn hoạt động: trừ khi sudoers đặt umask_override, vốn tắt theo mặc định, sudo kết hợp umask của bạn với giá trị mặc định 0022 của chính nó và không bao giờ làm giảm quyền, nên file do tee tạo có mode 600. Key bị lộ khi chạy pipe đó mà không có umask 077, với umask thông thường là 022, hoặc trên máy có sudoers bật umask_override. Đặt mode một cách tường minh thì không còn phụ thuộc vào các điều kiện này.
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, với một dòng trong mỗi config.
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.
Firewall: cổng và đường chuyển tiếp
Một file nftables duy nhất xử lý cả filter và NAT. Chạy /etc/nftables.conf sẽ flush ruleset hiện tại, vì vậy không dùng lệnh này 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ể khóa bạn khỏi chính server của mình. Lưu ý forward chain không cho phép điều gì, wg0 đến wg0. Các peer truy cập được Internet nhưng không truy cập lẫn nhau; thêm iifname "wg0" oifname "wg0" accept nếu cần VPN ngang hàng. Việc một peer được phép truy cập gì trên chính server không thuộc chain này: các packet gửi đến máy sẽ đi qua input hook, không đi qua forward, nên input chain mới quyết định. Với cấu hình hiện tại, input chain cho peer truy cập SSH và không cho gì khác, kể cả ping đến 10.8.0.1. Thêm iifname "wg0" icmp type echo-request accept vào đó, cùng một rule iifname "wg0" cho mỗi service bạn muốn chỉ cho phép truy cập qua tunnel. Điều này đặc biệt cần thiết khi máy còn chạy như một remote development box chạy Claude Code trong tmux và bạn không muốn public phần đó ra Internet.
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 /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 dạng lỗi 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 client ghi log:
Handshake for peer 1 (10.0.0.10:51820) did not complete after 5 seconds, retrying (try 2)Không có gói nào đến nơi 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à trên firewall mạng của nhà cung cấp chưa; trên hầu hết control panel, đây là hai lớp cấu hình riêng; địa chỉ và port Endpoint có đúng không; các key có bị nhập chéo không. Key trong block [Peer] của client phải là public key của server. Dán private key hoặc public key của chính client sẽ gây ra đú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ẽ xuất hiện dưới dạng packet bị drop do 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 bị timeout: forwarding hoặc NAT chưa được cấu hình. Kiểm tra sysctl net.ipv4.ip_forward có giá trị 1, sau đó theo dõi 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 không có packet nào thì tên egress interface bị sai. Nếu counter tăng nhưng không có phản hồi thì cần kiểm tra 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 trang lớn bị dừng giữa chừng. Đây là lỗi path MTU: tunnel thêm overhead, và một link ở giữa làm drop packet quá lớn mà không gửi ICMP message quay lại. Đặt MTU trên [Interface] của client. Trên link thông thường 1500 byte, wg-quick đã tự chọn 1420 (1500 trừ 80 byte overhead), nên ghi 1420 không thay đổi gì. Hãy thử 1380, sau đó 1280. Nếu giảm MTU hết hiện tượng treo nhưng throughput vẫn thấp, đừng tiếp tục đoán các giá trị tròn. Hãy làm theo hướng dẫn tìm path MTU thực bằng phương pháp chia đôi và giới hạn TCP MSS. Cách này cũng giúp loại trừ các nguyên nhân không liên quan đến tunnel.
Interface không khởi động được. Address already in use có nghĩa là một process khác đang giữ UDP 51820. Cannot find device wg0 sau khi up fail thường có nghĩa là config bị từ chối. Đọ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.
Backup, upgrade và các điểm bắt đầu quá tải khi mở rộng
/etc/wireguard chính là server. Hãy backup nó (sudo tar czf wg-backup.tgz -C /etc wireguard, mode 600, lưu ngoài máy), 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 pin public key của server. Upgrade chỉ là một apt upgrade thông thường cộng với reboot khi có kernel update, và wg-quick@wg0 sẽ tự chạy lại nếu bạn đã enable nó.
State của từng peer rất nhỏ và crypto chạy trong kernel, nên giới hạn nằm ở CPU và băng thông được cấp cho VPS, không phải ở cấu hình này. Hãy đo bằng iperf3 qua tunnel thay vì tin vào con số được công bố. Khi mở rộng, phần gây quá tải là 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] là cách để các AllowedIPs trùng nhau lọt vào: hãy generate 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. WireGuard không có clustering, nên redundancy cần một server thứ hai với bộ key riêng. Việc rotation key vẫn thủ công, vì vậy hãy ghi lại ai đang giữ key nào và cách revoke từng key. Khi phần bookkeeping này vượt quá khả năng quản lý của một text file, giải pháp thông thường là đặt một control plane lên cùng data plane trong kernel. Một NetBird server 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 hệ thống tăng thêm một box không cần thiết, Tailscale sẽ host control plane thay bạn. Gói Personal miễn phí của Tailscale hỗ trợ 6 user với số lượng user device không giới hạn và ban đầu tối đa 50 tagged resource như server, đủ để hầu hết hệ thống cá nhân không phải trả phí. Sau ngưỡng đó, chi phí tính theo số người thay vì số máy, nên chi phí thực tế của một hộ gia đình hoặc team nhỏ phụ thuộc vào số người có login, không phụ thuộc vào số peer mà bạn phải thêm thủ công vào wg0.conf. Ở phía đó, việc quản lý AllowedIPs cho split tunnel chuyển 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à approve tập trung thay vì paste vào từng client file. Việc đánh đổi này có đáng hay không phụ thuộc vào những gì hosted control plane 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ề peer nào.
Tất cả yêu cầu một Linux box do bạn kiểm soát, một public IP, một kernel cho phép load module và một firewall mà bạn quản lý từ đầu đến cuối.
FAQ
Tại sao handshake của WireGuard 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 gói tin không đến nơi hoặc không được chấp nhận. Hãy kiểm tra cổng 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 Endpoint host và port, sau đó kiểm tra xem các key có bị nhầm lẫn 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 liệu các gói tin có đến được hay không; dmesg chỉ báo cáo lỗi handshake của WireGuard sau khi bạn bật chế độ debug động (echo module wireguard +p | sudo tee /sys/kernel/debug/dynamic_debug/control), khi đó lỗi sai lệch key sẽ xuất hiện dưới dạng invalid-MAC drop.
Tunnel đã kết nối nhưng tôi không có internet. Tôi đang 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. Hãy xác nhận sysctl net.ipv4.ip_forward có giá trị 1 và đã được thiết lập trong /etc/sysctl.d/, thay vì chỉ dùng sysctl -w vốn sẽ mất 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 ens3, hiếm hơn là eth0.
Tôi có cần dòng DNS = trong file cấu hình client không?
Với full tunnel mà không có dòng DNS =, client sẽ giữ lại trình phân giải (resolver) học được từ mạng nội bộ, và các truy vấn đó sẽ đi ra dưới dạng văn bản thuần (cleartext) qua kết nối nội bộ trong khi mọi lưu lượng khác đều được đưa vào tunnel. Hãy trỏ DNS vào 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 hai nhiệm vụ. Ở chiều outbound, nó là một bảng định tuyến: lưu lượng khớp với AllowedIPs của peer sẽ được mã hóa và gửi đến peer đó. Ở chiều inbound, nó là một danh sách kiểm soát truy cập (ACL): một gói tin đã giải mã có nguồn nằm ngoài AllowedIPs của peer đó sẽ bị drop. Đó là lý do tại sao 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, nó hoạt động với in-kernel module mà không cần thiết lập thêm. Trên các nền tảng ảo hóa container chia sẻ kernel của host, như OpenVZ hoặc LXC, modprobe wireguard sẽ thất bại với Operation not supported và giải pháp thay thế là triển khai userspace wireguard-go. Hãy chạy sudo modprobe wireguard && echo ok trước khi thực hiện bất kỳ thao tác nào khác.