วิธีตั้งค่า UFW ป้องกันช่องโหว่ IPv6 บน VPS ให้ปลอดภัย
VPS ส่วนใหญ่มักเปิดพอร์ตทิ้งไว้บน IPv6 โดยที่คุณไม่รู้ตัวแม้จะตั้งค่า IPv4 ไว้ดีแล้ว เรียนรู้วิธีตรวจสอบด้วยคำสั่ง ip -6 addr และวิธีปิดช่องโหว่บน Ubuntu 24.04
กับดักของไฟร์วอลล์ IPv6 ในประโยคเดียว
ไฟร์วอลล์ของคุณทำหน้าที่ปกป้อง IPv4 แต่ VPS ของคุณเกือบทั้งหมดจะมีที่อยู่ IPv6 สาธารณะมาให้ด้วย และบริการจำนวนมากก็เปิดรับการเชื่อมต่อผ่าน IPv6 เป็นค่าเริ่มต้น หากไฟร์วอลล์ของคุณครอบคลุมเฉพาะ IPv4 หรือคุณพึ่งพาไฟร์วอลล์บนคลาวด์ที่กรองเฉพาะ IPv4 บริการเหล่านั้นทั้งหมดจะสามารถเข้าถึงได้จากอินเทอร์เน็ตผ่าน IPv6 ในขณะที่ฝั่ง IPv4 ของคุณดูเหมือนจะถูกล็อกไว้อย่างแน่นหนา คุณอาจทดสอบพอร์ตด้วย curl แล้วพบว่าการเชื่อมต่อถูกปฏิเสธจึงรู้สึกปลอดภัย แต่ผู้โจมตีสามารถเชื่อมต่อเข้าสู่พอร์ตเดียวกันผ่าน IPv6 และเข้าถึงระบบของคุณได้โดยตรง
คู่มือนี้จะแสดงให้เห็นว่าช่องโหว่นี้เกิดขึ้นได้อย่างไรบน Ubuntu 24.04 VPS ทั่วไป วิธีตรวจสอบว่าคุณกำลังเปิดเผยข้อมูลอะไรออกไปบ้าง และวิธีปิดช่องโหว่ดังกล่าว UFW ไม่ใช่ตัวการในเรื่องนี้ บนการติดตั้ง Ubuntu สมัยใหม่ UFW จัดการ IPv6 ไว้ให้อยู่แล้ว แต่การเปิดเผยข้อมูลเกิดขึ้นจากเลเยอร์รอบข้างและจากบริการที่คุณไม่ทราบว่ากำลังเปิดรับการเชื่อมต่ออยู่
เหตุผลที่ VPS ของคุณมี IPv6 ตั้งแต่แรก
VPS เกือบทุกแห่งในปัจจุบันมาพร้อมกับที่อยู่ IPv6 สาธารณะ ซึ่งมักจะเป็น /64 ทั้งชุด ควบคู่ไปกับที่อยู่ IPv4 ของคุณ ให้ตรวจสอบที่อยู่ของคุณด้วยคำสั่งนี้:
ip -6 addr show scope global2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> ...
inet6 2001:db8:2a::1/64 scope global2001:db8:2a::1 ในคู่มือนี้เป็นตัวแทนที่มาจาก 2001:db8::/32 ซึ่งเป็น prefix ที่สงวนไว้สำหรับเอกสารประกอบและจะไม่ถูกส่งผ่านบนอินเทอร์เน็ต ที่อยู่จริงที่เซิร์ฟเวอร์ของคุณแสดงในบรรทัดนั้นสามารถเข้าถึงได้จากทุกที่บนอินเทอร์เน็ต เช่นเดียวกับที่อยู่ IPv4 ของคุณ ตอนนี้ให้ดูว่ามีบริการใดกำลังรอรับการเชื่อมต่ออยู่บ้าง:
sudo ss -tlnpState 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ให้สังเกตคอลัมน์ Local Address อย่างละเอียด 0.0.0.0:22 หมายถึง "รอรับการเชื่อมต่อบนทุกที่อยู่ IPv4" [::]:22 หมายถึง "รอรับการเชื่อมต่อบนทุกที่อยู่ IPv6" 127.0.0.1:5432 ถูกผูกไว้กับ loopback และไม่ได้เปิดเผยต่อสาธารณะ ดังนั้นบรรทัดของ Postgres จึงมีความปลอดภัย ส่วนบรรทัด [::] สองบรรทัดนั้นตอบสนองต่ออินเทอร์เน็ตทั้งหมดผ่าน IPv6 และบรรทัด docker-proxy คือบริการที่คุณอาจลืมไปว่าได้เริ่มทำงานไว้
Daemon ส่วนใหญ่จะผูกกับ :: โดยค่าเริ่มต้น เนื่องจากบน Linux ซ็อกเก็ต :: มักจะยอมรับการเชื่อมต่อ IPv4 ได้ด้วย ดังนั้นสถานะเริ่มต้นของเซิร์ฟเวอร์ใหม่คือ "ตอบสนองบนทั้งสอง stack และเข้าถึงได้จากทุกที่" ไฟร์วอลล์ของคุณเป็นเพียงสิ่งเดียวที่คอยป้องกันอยู่เบื้องหน้า นั่นคือเหตุผลว่าทำไมไฟร์วอลล์ที่ตรวจสอบเพียง stack เดียวจึงเป็นปัญหาที่แท้จริง
ที่มาของช่องโหว่ IPv6
มีสาเหตุทั่วไป 3 ประการ ซึ่งในเซิร์ฟเวอร์หนึ่งเครื่องอาจพบสาเหตุเดียวหรือหลายสาเหตุพร้อมกันได้
1. Cloud firewall ที่กรองเฉพาะ IPv4 Firewall และผลิตภัณฑ์กลุ่ม security-group ของผู้ให้บริการหลายรายพัฒนาขึ้นมาโดยอิงกับ IPv4 เป็นหลัก จึงมักละเลย IPv6 หรือต้องการให้คุณเพิ่มกฎสำหรับ IPv6 แยกต่างหากด้วยตนเอง หาก firewall เพียงตัวเดียวที่คุณมีคือตัวที่อยู่ในหน้า dashboard ของผู้ให้บริการและมันไม่ครอบคลุม IPv6 บริการ [::] ของคุณจะเปิดรับการเชื่อมต่อจากภายนอกเสมอ ไม่ว่าหน้าจอจะแสดงสถานะของพอร์ต 22 บน IPv4 ไว้อย่างไรก็ตาม โปรดอ่านเอกสารประกอบ firewall ของผู้ให้บริการและมองหาคำว่า IPv6 โดยเฉพาะ
2. การเขียน iptables ด้วยตนเองโดยไม่มี ip6tables คำสั่ง iptables จะจัดการเฉพาะตารางของ IPv4 เท่านั้น ส่วน IPv6 จะใช้คำสั่งแยกต่างหากคือ ip6tables ซึ่งมีกฎของตัวเอง หากคุณเขียนสคริปต์ firewall ที่เต็มไปด้วยบรรทัด iptables -A INPUT ... แต่ไม่ได้เขียนกฎ ip6tables ที่สอดคล้องกันไว้ firewall สำหรับ IPv6 ของคุณจะว่างเปล่า และ chain INPUT ที่ว่างเปล่าพร้อมนโยบายเริ่มต้นเป็น ACCEPT จะอนุญาตให้ทุกอย่างผ่านเข้ามาได้:
sudo ip6tables -L INPUT -nChain INPUT (policy ACCEPT)
target prot opt source destinationผลลัพธ์ดังกล่าวคือกับดักทั้งหมดที่สรุปไว้ในหน้าเดียว IPv4 ถูกกรองเรียบร้อย แต่ IPv6 เปิดรับการเชื่อมต่อจากทั่วโลก
3. Docker เผยแพร่พอร์ตโดยข้าม firewall ของคุณ เมื่อคุณรันคำสั่ง docker run -p 8080:80 ตัว Docker จะแทรกกฎของตัวเองไว้หน้ากฎของ UFW ดังนั้นพอร์ตที่ถูกเผยแพร่จะสามารถเข้าถึงได้แม้ว่า ufw status จะระบุว่าพอร์ตนั้นถูกปฏิเสธก็ตาม ปัจจุบัน Docker จัดการกฎ ip6tables โดยอัตโนมัติ ดังนั้นเมื่อเปิดใช้งาน IPv6 บนเครือข่ายของ container กฎเดียวกันนี้จะมีผลกับ IPv6 ด้วย เหตุใด Docker จึงข้าม UFW และวิธีการกรองพอร์ตของ container อย่างถูกต้อง จะอธิบายกลไกและวิธีแก้ไข ดู พื้นฐานของ Docker Compose บน VPS เพื่อทำความเข้าใจว่าพอร์ตที่เผยแพร่เหล่านี้ถูกประกาศอย่างไร
หมายเหตุเกี่ยวกับสวิตช์ IPv6 ของ UFW UFW รองรับ IPv6 และสวิตช์การตั้งค่าจะอยู่ในไฟล์เดียว ให้ตรวจสอบดังนี้:
grep IPV6 /etc/default/ufwUbuntu รุ่นใหม่จะตั้งค่าเป็น IPV6=yes ดังนั้น UFW จะใช้กฎแต่ละข้อกับทั้งสอง stack หากคุณพบค่า IPV6=no ซึ่งอาจมาจาก image หรือคู่มือเก่า IPv6 จะไม่ได้ถูกเปิดทิ้งไว้ แต่ด้วยการตั้งค่านั้น UFW จะดรอป traffic ของ IPv6 ทั้งหมด ยกเว้นบน interface loopback ซึ่งจะทำให้เกิดปัญหาในทางกลับกัน คือบริการใดก็ตามที่คุณตั้งใจจะให้บริการผ่าน IPv6 จะหยุดตอบสนอง ให้ตั้งค่าเป็น yes หากคุณต้องการให้ UFW กรอง IPv6 ตามกฎที่คุณกำหนด แทนที่จะบล็อกทิ้งทั้งหมด
ตรวจสอบสิ่งที่เปิดเผยอยู่จริง
อย่าคาดเดา ให้วัดผลจากภายนอก เริ่มจากการแสดงรายการพอร์ตที่เปิดใช้งานอยู่และจดบันทึกทุกพอร์ตที่ผูกกับ :::
sudo ss -tlnp | grep '::'จากนั้น ให้ใช้เครื่องอื่นเชื่อมต่อไปยัง IPv6 address สาธารณะของเซิร์ฟเวอร์ แล้วลองทดสอบพอร์ตที่คุณคิดว่าปิดอยู่:
curl -6 -v http://[2001:db8:2a::1]:8080/หากการเชื่อมต่อแสดงหน้าเว็บหรือข้อความ banner แสดงว่าพอร์ตนั้นเปิดอยู่บน IPv6 พอร์ตที่ปิดอยู่จะส่งผลเป็น Connection refused หรือการหมดเวลา (timeout) ความล้มเหลวทั้งสองรูปแบบนี้ให้สัญญาณที่ต่างกัน และ ความแตกต่างระหว่างการปฏิเสธและการหมดเวลา จะบอกคุณได้ว่าโฮสต์ตอบกลับและปฏิเสธคุณ หรือไฟร์วอลล์ทิ้งแพ็กเก็ตของคุณไปโดยไม่มีการตอบสนอง เพื่อให้เห็นภาพรวมทั้งหมด ให้สแกน IPv6 address ด้วย nmap จากภายนอกเซิร์ฟเวอร์:
nmap -6 2001:db8:2a::1ทุกพอร์ตที่ nmap รายงานว่าเปิดอยู่บน IPv6 คือพอร์ตที่อินเทอร์เน็ตทั้งโลกสามารถเข้าถึงได้ ไม่ว่าการสแกน IPv4 ของคุณจะแสดงผลอย่างไร การเปรียบเทียบผลการสแกน IPv4 และ IPv6 ควบคู่กันเป็นวิธีที่เร็วที่สุดในการค้นหาช่องโหว่: สิ่งใดก็ตามที่เปิดอยู่บน -6 แต่ปิดอยู่บน IPv4 คือบริการที่ไฟร์วอลล์ของคุณยังไม่ได้ป้องกันไว้
ปิดช่องโหว่
ทำให้ UFW ครอบคลุมทั้งสอง stack และตั้งค่าเริ่มต้นเป็น deny ยืนยันการสลับโหมด จากนั้นตั้งค่านโยบายขาเข้าเริ่มต้นเป็น deny และอนุญาตเฉพาะสิ่งที่จำเป็นเท่านั้น:
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หาก UFW ทำงานอยู่แล้วในขณะที่คุณสลับ IPV6=yes การเปลี่ยนแปลงจะยังไม่มีผลจนกว่าคุณจะรัน sudo ufw reload
ufw status จะแสดงรายการกฎแต่ละข้อสองครั้ง ครั้งหนึ่งเป็นแบบปกติและอีกครั้งที่มี suffix เป็น (v6) เมื่อคุณเห็นบรรทัด (v6) แสดงว่า UFW กำลังกรอง IPv6 อยู่:
22/tcp ALLOW IN Anywhere
22/tcp (v6) ALLOW IN Anywhere (v6)หากคุณจัดการ iptables ด้วยตนเอง ให้ทำซ้ำกฎทุกข้อใน ip6tables หรือย้ายไปใช้ nftables ซึ่งตาราง inet จะครอบคลุมทั้ง IPv4 และ IPv6 ในที่เดียวและขจัดความผิดพลาดประเภทนี้ทั้งหมด ตารางกรอง inet ของ nftables เพียงตารางเดียวเป็นวิธีแก้ไขที่สะอาดที่สุดเมื่อคุณเขียนกฎด้วยตนเอง หาก VPS ของคุณใช้ Rocky หรือ AlmaLinux แทนที่จะเป็น Ubuntu จะไม่มี UFW ให้กำหนดค่า และ firewalld คือส่วนหน้าที่คุณต้องจัดการแทน ซึ่งจะใช้กฎ zone กับทั้งสอง stack พร้อมกัน
ผูกบริการที่คุณไม่ต้องการให้เป็นสาธารณะไว้กับ loopback ฐานข้อมูล, แผงควบคุมผู้ดูแลระบบ หรือ endpoint สำหรับวัดผลแทบไม่จำเป็นต้องมีที่อยู่สาธารณะเลย ให้ผูกบริการเหล่านั้นไว้กับ 127.0.0.1 และ ::1 เพื่อไม่ให้มันฟังบนที่อยู่ที่สามารถเข้าถึงได้จากภายนอกตั้งแต่แรก สำหรับ Postgres ให้ตั้งค่า listen_addresses = 'localhost' สำหรับแอปเซิร์ฟเวอร์ ให้ผูกไว้กับ 127.0.0.1 และวาง reverse proxy ไว้ด้านหน้า การปิด listener นั้นดีกว่าการใช้ไฟร์วอลล์กั้น เพราะจะไม่มีช่องทางให้เข้าถึงได้เลย
อย่าเชื่อใจ UFW ในการป้องกันพอร์ตที่ Docker เปิดใช้งาน ให้เผยแพร่พอร์ตของ container ไปยังที่อยู่ที่เฉพาะเจาะจงแทนที่จะเป็นทุก interface เช่น -p 127.0.0.1:8080:80 เพื่อให้พอร์ตนั้นสามารถเข้าถึงได้จากโฮสต์และสิ่งที่คุณตั้งใจ proxy ไปหาเท่านั้น เมื่อ container จำเป็นต้องเป็นสาธารณะจริงๆ ให้วางไว้หลัง Traefik reverse proxy และเผยแพร่เฉพาะตัว proxy เท่านั้น ไม่ใช่เผยแพร่ทุกแอปแยกกัน
เพิ่มกฎ IPv6 ลงในไฟร์วอลล์ของผู้ให้บริการของคุณ หรือยอมรับว่านั่นไม่ใช่ไฟร์วอลล์สำหรับ IPv6 ของคุณ และปล่อยให้ UFW หรือ nftables บนโฮสต์ทำหน้าที่นั้นแทน
ตรวจสอบว่าพอร์ตปิดจริง
รันการทดสอบจากภายนอกซ้ำอีกครั้งหลังจากที่คุณได้ทำการเปลี่ยนแปลง:
curl -6 -v http://[2001:db8:2a::1]:8080/
nmap -6 2001:db8:2a::1พอร์ตที่เคยตอบสนองก่อนหน้านี้ควรจะปฏิเสธการเชื่อมต่อหรือหมดเวลา (time out) และ nmap ควรรายงานสถานะเป็น filtered หรือ closed หากพอร์ตยังคงเปิดอยู่ ให้ย้อนกลับไปตรวจสอบแหล่งที่มาทั้งสามจุดข้างต้น ได้แก่ บริการที่ยังคง bind อยู่กับ :: โดยไม่มีกฎไฟร์วอลล์กั้น, กฎของ Docker ที่แทรกอยู่หน้า UFW หรือไฟร์วอลล์ของผู้ให้บริการที่ไม่ได้ตรวจสอบ IPv6 เลย
การเก็บรักษาบริการที่มีความละเอียดอ่อนไม่ให้เข้าถึงได้จากอินเทอร์เน็ตสาธารณะเลยนั้นมีความปลอดภัยสูงกว่า การนำ SSH และแผงควบคุมผู้ดูแลระบบไปไว้หลัง WireGuard VPN และตั้งค่าไฟร์วอลล์ให้พอร์ตเหล่านั้นตอบสนองเฉพาะการเชื่อมต่อผ่านอุโมงค์ (tunnel) เท่านั้น ซึ่งจะทำให้ประเด็นเรื่องการเปิดเผย IPv6 ไม่ส่งผลต่อบริการเหล่านี้ เพื่อชะลอการสแกนแบบ brute-force ที่พุ่งเป้าไปยังบริการที่ยังคงเป็นสาธารณะ ให้ติดตั้ง Fail2ban หน้า SSH เพิ่มเติมบนไฟร์วอลล์ที่มีนโยบาย default-deny
หากคุณยังไม่คุ้นเคยกับเรื่องพอร์ต พอร์ตคืออะไรและบริการต่างๆ ฟังการเชื่อมต่ออย่างไร คือบทความพื้นฐานที่คุณควรอ่านก่อน
FAQ
UFW บล็อก IPv6 โดยค่าเริ่มต้นหรือไม่?
บน Ubuntu 24.04 เวอร์ชันปัจจุบัน คำตอบคือใช่ UFW จะอ่าน IPV6=yes จาก /etc/default/ufw และนำกฎแต่ละข้อไปใช้กับทั้ง IPv4 และ IPv6 โดยที่ ufw status จะแสดงกฎ IPv6 พร้อมส่วนต่อท้าย (v6) ปัญหาจะเกิดขึ้นเมื่อคุณพึ่งพาไฟร์วอลล์ของผู้ให้บริการที่กรองเฉพาะ IPv4, เมื่อกฎ iptables ที่เขียนขึ้นเองไม่มีกฎ ip6tables คู่กัน หรือเมื่อ Docker เปิดพอร์ตโดยข้าม UFW ไป ทั้งนี้ IPV6=no (จากอิมเมจหรือบทเรียนเก่า) ไม่ได้เปิด IPv6 ดังนั้นเมื่อ UFW ทำงานอยู่ มันจะดรอปทราฟฟิก IPv6 ทั้งหมด ยกเว้นบน loopback คุณสามารถตรวจสอบการตั้งค่านี้ได้ด้วย grep IPV6 /etc/default/ufw
ฉันจะตรวจสอบได้อย่างไรว่า VPS ของฉันเปิดพอร์ตอะไรไว้บน IPv6 บ้าง?
ให้รันคำสั่ง sudo ss -tlnp แล้วสังเกต listener ทุกตัวที่มี local address ขึ้นต้นด้วย [::] ซึ่งหมายความว่ามันตอบสนองบนทุกอินเทอร์เฟซ IPv6 จากนั้นให้ทดสอบจากเครื่องอื่นโดยเชื่อมต่อไปยัง public IPv6 address ของเซิร์ฟเวอร์โดยตรงด้วย curl -6 -v http://[YOUR:IPV6::ADDR]:PORT/ หรือสแกนด้วย nmap -6 YOUR:IPV6::ADDR พอร์ตใดก็ตามที่เปิดอยู่ในการสแกน IPv6 แต่ปิดอยู่บน IPv4 คือช่องโหว่ของคุณ
ทำไมฉันถึงเข้าถึงพอร์ตของ Docker container ได้ ทั้งที่ UFW แจ้งว่าถูกบล็อก?
Docker จะแทรกกฎไฟร์วอลล์ของตัวเองไว้หน้ากฎของ UFW เมื่อคุณเปิดพอร์ตด้วย -p ดังนั้นพอร์ตที่เปิดไว้จึงเข้าถึงได้แม้ว่า ufw status จะแสดงสถานะว่าถูกปฏิเสธก็ตาม ปัญหานี้เกิดขึ้นทั้งบน IPv4 และ IPv6 (เมื่อเปิดใช้งาน IPv6 ของ Docker) วิธีแก้ไขคือให้เปิดพอร์ตไปยัง address เฉพาะ เช่น -p 127.0.0.1:8080:80 หรือวาง container ไว้หลัง reverse proxy แล้วเปิดพอร์ตเฉพาะตัว proxy เท่านั้น
ฉันยังจำเป็นต้องมีไฟร์วอลล์ IPv6 หากไฟร์วอลล์ IPv4 ของฉันแน่นหนาแล้วหรือไม่?
จำเป็น IPv4 และ IPv6 เป็น network stack ที่แยกจากกันและมีกฎไฟร์วอลล์แยกกัน กฎ IPv4 ที่สมบูรณ์แบบไม่สามารถป้องกันทราฟฟิก IPv6 ได้ หาก VPS ของคุณมี public IPv6 address (ซึ่งเกือบทุกเครื่องมี) บริการใดก็ตามที่ฟังบน :: จะยังคงเข้าถึงได้ผ่าน IPv6 จนกว่าจะมีกฎไฟร์วอลล์ IPv6 หรือการผูก (binding) เข้ากับ loopback มาหยุดไว้
ฉันจะตั้งค่าให้บริการฟังเฉพาะ IPv4 หรือเฉพาะ localhost ได้อย่างไร?
ให้ตั้งค่า bind address ของบริการนั้นในไฟล์คอนฟิกของมันเอง โดยผูกเข้ากับ 127.0.0.1 สำหรับ IPv4 loopback เท่านั้น หรือ 0.0.0.0 สำหรับทุก IPv4 address โดยไม่มี listener ของ IPv6 ตัวอย่างเช่น Postgres ใช้ listen_addresses, SSH ใช้ ListenAddress และแอปเซิร์ฟเวอร์ส่วนใหญ่จะมี flag สำหรับกำหนด host หรือ bind ให้ตรวจสอบผลลัพธ์ด้วย sudo ss -tlnp และตรวจสอบให้แน่ใจว่า Local Address ไม่แสดง [::] อีกต่อไป