SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor

UFW IPv6: VPS bị hở port qua IPv6

VPS Ubuntu 24.04 có thể mở service qua IPv6 dù IPv4 đã khóa. Kiểm tra port đang listen và đóng khoảng trống giữa UFW, cloud firewall và iptables.

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

Gần như mọi VPS hiện nay đều có một địa chỉ IPv6 public, thường là cả một /64, bên cạnh địa chỉ IPv4. 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 trong hướng dẫn này là placeholder từ 2001:db8::/32, một prefix dành cho tài liệu và không bao giờ được định tuyến trên Internet. Địa chỉ thực mà server của bạn in ra ở dòng đó có thể được truy cập từ bất kỳ đâu trên Internet, giống hệt địa chỉ IPv4. Bây giờ hãy xem những gì đang lắng nghe:

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

Hãy đọc kỹ cột Local Address. 0.0.0.0:22 có nghĩa là “lắng nghe trên mọi địa chỉ IPv4”. [::]:22 có nghĩa là “lắng nghe 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 dễ quên rằng mình đã khởi động.

Hầu hết daemon mặc định bind vào ::, vì trên Linux, socket :: thường cũng nhận kết nối 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 địa chỉ”. Firewall là lớp duy nhất đứng trước các kết nối đó. Vì thế, firewall chỉ theo dõi một stack là một vấn đề thực sự.

IPv6 thực sự bị hở từ đâu

Có 3 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 firewall của nhà cung cấp và sản phẩm security group được xây dựng xoay quanh IPv4. Chúng có thể bỏ qua IPv6 hoặc yêu cầu bạn tự thêm 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 public dù cấu hình cổng 22 trên IPv4 hiển thị thế nào. Đọ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 một bộ rule riêng. Nếu bạn viết một script firewall đầ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ẽ rỗng. Khi chain INPUT rỗng và policy mặc định là ACCEPT, mọi lưu lượng đều được cho phép:

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

Toàn bộ vấn đề được thể hiện trong một màn hình đầu ra này. IPv4 được lọc, còn IPv6 chấp nhận lưu lượng từ mọi nơi.

3. Docker publish port trực tiếp, bỏ qua firewall của bạn. Khi 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 ngay cả khi ufw status cho biết port đó bị deny. Các phiên bản Docker hiện tại mặc định quản lý rule ip6tables. Do đó, khi IPv6 được bật trên network của container, điều tương tự cũng xảy ra với 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 các khái niệm cơ bản về Docker Compose trên VPS để biết cách khai báo các port đã publish.

Lưu ý về tùy chọn IPv6 của UFW. UFW có hỗ trợ IPv6, và tùy chọn này nằm trong một file. Kiểm tra bằng lệnh:

grep IPV6 /etc/default/ufw

Ubuntu hiện đại đi kèm với IPV6=yes, nên UFW áp dụng mỗi rule cho cả hai stack. Nếu thấy IPV6=no do dùng image cũ hoặc làm theo tài liệu cũ, điều đó không có nghĩa IPv6 đang bị để hở. Với thiết lập đó, UFW sẽ drop toàn bộ lưu lượng IPv6, ngoại trừ trên loopback interface. Khi đó vấn đề diễn ra theo hướng ngược lại: mọi dịch vụ bạn định cung cấp qua IPv6 sẽ ngừng phản hồi. Đặt thành yes nếu muốn UFW lọc IPv6 theo các rule của bạn thay vì chặn toàn bộ IPv6.

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 và ::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 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 đã phản hồi trước đó giờ phải từ chối kết nối hoặc timeout, và nmap phải báo cổng ở trạng thái filtered hoặc closed. Nếu một cổng vẫn mở, hãy kiểm tra lại 3 nguồn ở trên: một service vẫn bind vào :: nhưng không có rule nào phía 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 an toàn hơn là không đưa các service nhạy cảm lên Internet công khai. Đặt SSH và các panel quản trị phía sau VPN WireGuard rồi firewall các cổng đó để chúng chỉ phản hồi qua tunnel; khi đó không còn phải xét vấn đề exposure qua IPv6 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 triển khai Fail2ban phía trước SSH cùng với firewall có chính sách mặc định deny.

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

FAQ

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

Trên Ubuntu 24.04 hiện đại, có. UFW đọc IPV6=yes từ /etc/default/ufw rồi áp dụng từng rule cho cả IPv4 và IPv6. ufw status hiển thị các rule IPv6 với hậu tố (v6). Vấn đề thường xảy ra khi bạn dựa vào firewall của nhà cung cấp nhưng firewall đó chỉ lọc IPv4, khi các rule iptables viết thủ công không có rule ip6tables tương ứng, hoặc khi Docker publish một cổng vượt qua UFW. IPV6=no (từ image cũ hoặc tutorial cũ) không mở IPv6. Khi dùng cấu hình này, UFW đang active sẽ drop toàn bộ traffic IPv6, ngoại trừ traffic trên loopback. 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 đang expose 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 đó nghĩa là listener nhận kết nố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. Bất kỳ cổng nào open trong lần scan IPv6 nhưng closed trên IPv4 đều là điểm hở của bạn.

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

Khi bạn publish một cổng bằng -p, Docker chèn các firewall rule riêng của nó trước rule của UFW. Vì vậy, cổng đã publish vẫn truy cập được dù ufw status liệt kê cổng đó là denied. Đ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 một reverse proxy và chỉ publish reverse proxy.

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

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

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

Đặt bind address của service trong file cấu hình riêng của service đó. Bind vào 127.0.0.1 để chỉ dùng IPv4 loopback, hoặc 0.0.0.0 để listen trên 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ó host hoặc bind flag. Xác nhận kết quả bằng sudo ss -tlnp và kiểm tra để Local Address không còn hiển thị [::].