SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-22

UFW và IPv6: VPS của bạn đang mở cửa nào?

UFW trên Ubuntu 24.04 thường đã xử lý IPv6, nhưng cloud firewall hoặc service vẫn có thể chỉ được bảo vệ bằng IPv4. Kiểm tra và đóng port đang hở.

Cạm bẫy firewall IPv6 trong một câu

Firewall của bạn bảo vệ IPv4. VPS của bạn gần như chắc chắn cũng có một địa chỉ IPv6 public, và nhiều service mặc định listen trên địa chỉ đó. Nếu firewall chỉ áp dụng cho IPv4, hoặc bạn dựa vào cloud firewall chỉ lọc IPv4, tất cả các service đó đều có thể được truy cập từ toàn bộ Internet qua IPv6 trong khi phía IPv4 trông như đã được khóa. Bạn kiểm tra một port bằng curl, thấy kết nối bị từ chối và nghĩ rằng hệ thống an toàn. Attacker kết nối đến cùng port đó qua IPv6 và truy cập được vào hệ thống.

Hướng dẫn này chỉ ra khoảng trống đó xuất hiện ở đâu trên một VPS Ubuntu 24.04 thông thường, cách xem chính xác những gì bạn đang expose và cách đóng lỗ hổng này. UFW không phải thủ phạm. Trên bản cài Ubuntu hiện đại, UFW đã xử lý IPv6. Việc expose xuất phát từ các layer xung quanh UFW và từ những service mà bạn không biết là đang listen.

Vì sao VPS của bạn ngay từ đầu đã có IPv6

Hầu hết VPS hiện nay đều được cấp một địa chỉ IPv6 public, thường là cả một /64, cùng với địa chỉ IPv4. Hãy kiểm tra VPS của bạn:

ip -6 addr show scope global
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> ...
    inet6 2001:db8:2a::1/64 scope global

2001:db8:2a::1 đó có thể được định tuyến từ bất kỳ đâu trên Internet, giống hệt địa chỉ IPv4 của bạn. Bây giờ hãy xem các service nào đang listening:

sudo ss -tlnp
State   Recv-Q  Local Address:Port   Process
LISTEN  0       0.0.0.0:22           sshd
LISTEN  0       [::]:22              sshd
LISTEN  0       127.0.0.1:5432       postgres
LISTEN  0       [::]:8080            docker-proxy

Đọc kỹ cột Local Address. 0.0.0.0:22 nghĩa là “listening trên mọi địa chỉ IPv4”. [::]:22 nghĩa là “listening trên mọi địa chỉ IPv6”. 127.0.0.1:5432 bind vào loopback và hoàn toàn không public, nên dòng Postgres này an toàn. Hai dòng [::] trả lời toàn bộ Internet qua IPv6, còn dòng docker-proxy là loại service bạn thường quên rằng mình đã khởi động.

Hầu hết daemon đều bind vào :: theo mặc định, vì trên Linux, socket :: thường cũng chấp nhận IPv4. Vì vậy, trạng thái mặc định của một server mới là “trả lời trên cả hai stack, ở mọi nơi”. Firewall là lớp duy nhất đứng trước các service này. Vì thế, firewall chỉ kiểm tra một stack là một vấn đề thực sự.

Lỗ hổng IPv6 thực sự đến từ đâu

Có 4 nguồn phổ biến. Trên một máy chủ, bạn có thể gặp một hoặc nhiều nguồn cùng lúc.

1. Cloud firewall chỉ lọc IPv4. Nhiều sản phẩm firewall và security group của nhà cung cấp được phát triển ban đầu cho IPv4, nên bỏ qua IPv6 hoặc yêu cầu bạn tự thêm các rule IPv6 riêng. Nếu firewall duy nhất của bạn là firewall trong dashboard của nhà cung cấp và firewall đó không áp dụng cho IPv6, các service [::] của bạn vẫn mở với Internet, bất kể dashboard hiển thị thế nào về port 22 trên IPv4. Đọc tài liệu firewall của nhà cung cấp và tìm cụ thể từ IPv6.

2. Tự viết iptables nhưng không có ip6tables. Lệnh iptables chỉ tác động đến các bảng IPv4. IPv6 có một lệnh hoàn toàn riêng là ip6tables, cùng với bộ rule riêng. Nếu bạn viết một firewall script chứa đầy các dòng iptables -A INPUT ... nhưng không viết các rule ip6tables tương ứng, firewall IPv6 của bạn sẽ trống. Khi chain INPUT trống và policy mặc định là ACCEPT, mọi kết nối đều được cho phép:

sudo ip6tables -L INPUT -n
Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Đó là toàn bộ vấn đề hiển thị trên một màn hình. IPv4 được lọc, còn IPv6 chấp nhận mọi kết nối.

3. Docker publish port đi thẳng qua firewall. Khi bạn chạy docker run -p 8080:80, Docker chèn rule của nó lên trước rule của UFW. Vì vậy, port đã publish vẫn có thể truy cập được ngay cả khi ufw status cho biết port đó bị từ chối. Trên các phiên bản Docker hiện đại, điều tương tự cũng áp dụng cho IPv6. Vì sao Docker bypass UFW và cách lọc port của container đúng cách giải thích cơ chế này và các cách khắc phục. Xem kiến thức cơ bản về Docker Compose trên VPS để biết cách khai báo các port đã publish.

4. UFW đã tắt IPv6. UFW có hỗ trợ IPv6, nhưng chỉ hoạt động khi được bật. Kiểm tra tùy chọn này:

grep IPV6 /etc/default/ufw

Ubuntu hiện đại ship với IPV6=yes, nên UFW áp dụng mỗi rule cho cả 2 stack. Nếu bạn thấy IPV6=no, có thể do đang dùng image cũ hoặc làm theo guide cũ, thì mọi rule UFW bạn đã viết chỉ áp dụng cho IPv4, còn IPv6 không được quản lý.

Xem chính xác những gì bạn đang expose

Đừng đoán. Hãy đo từ bên ngoài. Trước tiên, liệt kê các listener và ghi lại mọi listener đang bind vào :::

sudo ss -tlnp | grep '::'

Sau đó, từ một máy khác, kết nối đến địa chỉ IPv6 public của server và thử một port mà bạn cho là đang đóng:

curl -6 -v http://[2001:db8:2a::1]:8080/

Nếu lệnh đó trả về một page hoặc banner, port này đang mở trên IPv6. Port đóng sẽ trả về Connection refused hoặc timeout. Hai lỗi này không cho cùng một tín hiệu, và sự khác nhau giữa refused và timed out cho biết host đã trả lời rồi từ chối kết nối hay firewall đã âm thầm drop packet của bạn. Để có đầy đủ thông tin, hãy dùng nmap từ bên ngoài server để scan địa chỉ IPv6:

nmap -6 2001:db8:2a::1

Mọi port mà nmap báo là open trên IPv6 đều có thể được toàn bộ Internet truy cập, bất kể kết quả scan IPv4 của bạn là gì. So sánh hai kết quả scan IPv4 và IPv6 cạnh nhau là cách nhanh nhất để tìm ra điểm lệch: bất kỳ port nào mở trên -6 nhưng đóng trên IPv4 đều là một service mà firewall của bạn chưa xử lý.

Thu hẹp lỗ hổng

Cấu hình UFW áp dụng cho cả hai stack và đặt chính sách mặc định là deny. Xác nhận tùy chọn này, sau đó đặt chính sách mặc định deny đối với inbound và chỉ allow những gì bạn cần:

sudo sed -i 's/^IPV6=no/IPV6=yes/' /etc/default/ufw
sudo ufw default deny incoming
sudo ufw allow 22/tcp
sudo ufw enable
sudo ufw status verbose

Nếu UFW đã active trước khi bạn bật IPV6=yes, thay đổi này chỉ có hiệu lực sau khi chạy sudo ufw reload.

ufw status liệt kê mỗi rule 2 lần: một lần ở dạng thông thường và một lần có hậu tố (v6). Khi thấy các dòng (v6), UFW đang filter IPv6:

22/tcp                     ALLOW IN    Anywhere
22/tcp (v6)                ALLOW IN    Anywhere (v6)

Nếu bạn tự quản lý iptables, hãy tạo rule tương ứng cho mọi rule trong ip6tables, hoặc chuyển sang nftables. Các table inet của nftables quản lý IPv4 và IPv6 tại cùng một nơi, nên loại bỏ hoàn toàn nhóm lỗi này. Nếu tự viết rule, một filter table inet duy nhất của nftables là cách xử lý gọn nhất. Nếu VPS của bạn chạy Rocky hoặc AlmaLinux thay vì Ubuntu, hệ thống không có UFW để cấu hình. Khi đó, firewalld là front end bạn quản lý thay, và nó áp dụng các zone rule cho cả hai stack cùng lúc.

Bind các service không nên public vào loopback. Database, admin panel hoặc metrics endpoint hiếm khi cần public address. Bind chúng vào 127.0.0.1::1 để ngay từ đầu chúng không listen trên một địa chỉ có thể định tuyến. Với Postgres, đặt listen_addresses = 'localhost'. Với app server, bind vào 127.0.0.1 rồi đặt reverse proxy ở phía trước. Đóng listener hiệu quả hơn firewall, vì khi đó không còn gì để truy cập.

Không nên tin UFW sẽ bảo vệ các port mà Docker publish. Publish port của container vào một địa chỉ cụ thể thay vì mọi interface, chẳng hạn -p 127.0.0.1:8080:80, để port chỉ có thể truy cập từ host và các proxy mà bạn chủ động cấu hình. Khi container thực sự phải public, hãy đặt nó phía sau reverse proxy Traefik và chỉ publish proxy, không publish từng app.

Thêm rule IPv6 vào firewall của provider, hoặc chấp nhận rằng firewall đó không bảo vệ IPv6 và để UFW hoặc nftables trên host đảm nhiệm việc này.

Xác minh rằng cổng thực sự đã đóng

Chạy lại cùng bài kiểm tra từ bên ngoài sau khi thay đổi:

curl -6 -v http://[2001:db8:2a::1]:8080/
nmap -6 2001:db8:2a::1

Cổng trước đây trả lời phải từ chối kết nối hoặc hết thời gian chờ, và nmap phải báo cổng đó ở trạng thái filtered hoặc closed. Nếu cổng vẫn mở, hãy kiểm tra lại 4 nguồn ở trên: một service vẫn bind vào :: nhưng không có rule nào đứng trước, một rule của Docker nằm trước UFW, hoặc firewall của nhà cung cấp chưa xử lý IPv6.

Cách chắc chắn hơn là không đưa các service nhạy cảm lên Internet công cộng. Đặt SSH và panel quản trị sau VPN WireGuard rồi firewall các cổng tương ứng để chúng chỉ trả lời trên tunnel. Khi đó, vấn đề exposure qua IPv6 không còn áp dụng cho các service này. Để giảm tốc độ các lượt quét brute-force nhắm vào những gì vẫn public, hãy đặt Fail2ban trước SSH trên firewall có chính sách mặc định từ chối.

Nếu bạn chưa quen với port, hãy đọc trước port là gì và service listen như thế nào.

FAQ

UFW có chặn IPv6 theo mặc định không?

Trên bản cài Ubuntu 24.04 hiện đại, có. UFW đọc IPV6=yes từ /etc/default/ufw và áp dụng mỗi rule cho cả IPv4 lẫn IPv6. ufw status hiển thị các rule IPv6 với hậu tố (v6). Vấn đề thường xuất hiện khi IPV6=no (từ một image cũ hoặc tutorial cũ), khi bạn dựa vào firewall của nhà cung cấp nhưng firewall đó chỉ lọc IPv4, hoặc khi Docker publish một port vượt qua UFW. Kiểm tra trạng thái này bằng grep IPV6 /etc/default/ufw.

Làm cách nào để kiểm tra VPS của tôi đang expose những gì trên IPv6?

Chạy sudo ss -tlnp và ghi lại mọi listener có địa chỉ local bắt đầu bằng [::]. Điều này có nghĩa service trả lời trên mọi interface IPv6. Sau đó, từ một máy khác, kiểm tra trực tiếp địa chỉ IPv6 public của server bằng curl -6 -v http://[YOUR:IPV6::ADDR]:PORT/, hoặc scan địa chỉ đó bằng nmap -6 YOUR:IPV6::ADDR. Mọi port mở khi scan IPv6 nhưng đóng trên IPv4 đều là điểm hở của bạn.

Vì sao tôi vẫn truy cập được port của Docker container khi UFW báo port đó bị chặn?

Khi bạn publish một port bằng -p, Docker chèn rule firewall của nó trước rule của UFW. Vì vậy, port đã publish vẫn truy cập được dù ufw status liệt kê port đó là bị deny. Điều này xảy ra trên IPv4, và cũng xảy ra trên IPv6 khi Docker bật hỗ trợ IPv6. Hãy publish vào một địa chỉ cụ thể như -p 127.0.0.1:8080:80, hoặc đặt container phía sau reverse proxy và chỉ publish reverse proxy.

Nếu firewall IPv4 của tôi được cấu hình chặt chẽ, tôi vẫn cần firewall IPv6 không?

Có. IPv4 và IPv6 là 2 network stack riêng, với các rule firewall riêng. Một bộ rule IPv4 hoàn chỉnh không có tác dụng với lưu lượng IPv6. Nếu VPS của bạn có địa chỉ IPv6 public, như hầu hết VPS hiện nay, mọi service đang listen trên :: vẫn có thể truy cập qua IPv6 cho đến khi một rule firewall IPv6 hoặc binding vào loopback chặn nó.

Làm cách nào để service chỉ listen trên IPv4 hoặc chỉ listen trên localhost?

Đặt địa chỉ bind của service trong file cấu hình riêng của nó. Bind vào 127.0.0.1 để chỉ dùng loopback IPv4, hoặc 0.0.0.0 để dùng mọi địa chỉ IPv4 mà không có listener IPv6. Postgres dùng listen_addresses, SSH dùng ListenAddress, còn hầu hết app server đều có tùy chọn host hoặc bind. Xác nhận kết quả bằng sudo ss -tlnp và kiểm tra rằng Local Address không còn hiển thị [::].