วิธีแก้ปัญหา Docker ข้ามการทำงานของ UFW อย่างถูกต้อง
ทำไม Docker ถึงเปิดพอร์ตผ่าน UFW ได้แม้ตั้งค่า deny ไว้แล้ว เรียนรู้กลไกของ iptables และวิธีแก้ไขด้วยการผูกพอร์ตกับ 127.0.0.1 หรือการกรองผ่าน chain DOCKER-USER
เหตุใด Docker จึงข้ามการทำงานของ UFW
Docker ข้ามการทำงานของ UFW เนื่องจากพอร์ตของคอนเทนเนอร์ที่ถูกเผยแพร่ (published ports) ไม่เคยผ่านกฎของไฟร์วอลล์ที่ UFW ดูแลอยู่ เมื่อคุณรัน docker run -p 8080:80 ตัว Docker จะเขียนกฎ DNAT (destination network address translation) ลงใน chain DOCKER ของตนเองในตาราง nat ของเคอร์เนล ซึ่ง chain PREROUTING จะกระโดดเข้าไปทำงาน กฎดังกล่าวจะเขียนปลายทางของแพ็กเก็ตใหม่ไปยังที่อยู่ส่วนตัวของคอนเทนเนอร์ ก่อนที่เคอร์เนลจะตัดสินใจว่าแพ็กเก็ตนั้นจะไปที่ใด จากนั้นแพ็กเก็ตที่ถูกเขียนใหม่จะถูกส่งต่อไปยังคอนเทนเนอร์ผ่านทาง chain FORWARD ซึ่ง Docker เป็นผู้ควบคุม กฎของ UFW นั้นอยู่ใน chain INPUT และแพ็กเก็ตก็ไม่เคยเข้าไปถึงจุดนั้น ดังนั้น ufw status จึงแสดงสถานะ default deny ส่วน sudo ufw deny 8080 รายงานว่าสำเร็จ แต่พอร์ต 8080 ก็ยังคงตอบสนองต่ออินเทอร์เน็ตทั้งหมดอยู่ดี
นี่ไม่ใช่ข้อผิดพลาดของ Docker และ UFW ก็ไม่ได้เสีย ทั้งสองเครื่องมือต่างตั้งโปรแกรมไฟร์วอลล์ของเคอร์เนลตัวเดียวกัน กฎของ Docker เพียงแค่ทำงานในจุดที่เร็วกว่าบนเส้นทางของแพ็กเก็ต UFW จึงไม่ถูกเรียกใช้งาน คู่มือนี้จะสาธิตการข้ามการทำงานดังกล่าว อธิบายกลไก และครอบคลุมวิธีแก้ไขสองวิธีที่ได้ผล ได้แก่ การเผยแพร่พอร์ตบน 127.0.0.1 และการกรองใน chain DOCKER-USER หากคุณยังใหม่กับ UFW ให้ตั้งค่าด้วย คู่มือพื้นฐานไฟร์วอลล์ UFW ก่อน เพราะไฟร์วอลล์แบบ default-deny ยังคงเป็นพื้นฐานที่ถูกต้องสำหรับทุกอย่างบนเซิร์ฟเวอร์
การตรวจสอบช่องโหว่การข้ามผ่านบนเซิร์ฟเวอร์ของคุณเอง
เริ่มต้นจาก VPS ที่เปิดใช้งาน UFW โดยตั้งค่านโยบายเริ่มต้นเป็นปฏิเสธ (deny) สำหรับทราฟฟิกขาเข้าทั้งหมด จากนั้นให้รันเว็บคอนเทนเนอร์โดยเปิดพอร์ตไว้:
sudo ufw status verbose
docker run -d --name web -p 8080:80 nginx:1.29-alpineufw status verbose จะแสดง Default: deny (incoming), allow (outgoing) และไม่มีกฎสำหรับพอร์ต 8080 ตามรายงานของไฟร์วอลล์ พอร์ตนี้ควรจะถูกปิดอยู่ ตอนนี้ให้ทดสอบจากเครื่องอื่นที่ไม่ใช่ตัวเซิร์ฟเวอร์เอง:
curl -I http://your-vps-ip:8080/HTTP/1.1 200 OKคอนเทนเนอร์ตอบสนองการเชื่อมต่อ ให้เพิ่มกฎปฏิเสธ (deny) อย่างชัดเจนแล้วทดสอบอีกครั้ง:
sudo ufw deny 8080/tcpพอร์ตยังคงตอบสนองการเชื่อมต่อ เนื่องจากกฎการปฏิเสธอยู่ในเชนที่แพ็กเก็ตไม่เคยผ่านไปถึง UFW ไม่ได้ทำงานผิดพลาด แต่ระบบไม่เคยเรียกใช้กฎดังกล่าว นี่คือสาเหตุที่ปัญหาถูกซ่อนไว้อย่างแนบเนียน: ไม่มีการแสดงข้อผิดพลาดใดๆ การปรับใช้บริการทำงานได้ปกติ และสถานะของไฟร์วอลล์ดูเหมือนเซิร์ฟเวอร์ที่ถูกล็อกไว้อย่างปลอดภัยตามปกติทุกประการ
กลไกการทำงาน: PREROUTING ทำงานก่อน INPUT
เคอร์เนลประมวลผลแพ็กเก็ตที่เข้ามาตามลำดับที่กำหนดไว้ตายตัว และปัญหาทั้งหมดก็เกิดขึ้นจากลำดับดังกล่าว
PREROUTINGทำงานเป็นลำดับแรก กฎที่ถูกเรียกใช้จากจุดนี้อาจเขียนปลายทางของแพ็กเก็ตใหม่ ซึ่งกฎของ Docker สำหรับพอร์ตที่เปิดใช้งานในเชนDOCKERที่PREROUTINGกระโดดไปหานั้น ทำหน้าที่ดังกล่าวโดยตรง- การตัดสินใจเรื่องเส้นทาง (routing decision) จะเกิดขึ้นถัดมา แพ็กเก็ตที่ระบุปลายทางเป็นโฮสต์เองจะถูกส่งไปยังเชน
INPUTส่วนแพ็กเก็ตที่ระบุปลายทางเป็นเครื่องอื่นจะถูกส่งไปยังเชนFORWARD - กฎของ UFW อยู่ใน
INPUTส่วนกฎของ Docker อยู่ในFORWARD
พิจารณากฎของ Docker สำหรับคอนเทนเนอร์ที่คุณเพิ่งเริ่มทำงาน:
sudo iptables -t nat -L DOCKER -nChain DOCKER (2 references)
target prot opt source destination
RETURN 0 -- 0.0.0.0/0 0.0.0.0/0
DNAT 6 -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.17.0.2:80บรรทัด DNAT คือคำตอบทั้งหมด แพ็กเก็ตใดก็ตามที่มาถึงพอร์ต 8080 จะถูกเขียนปลายทางใหม่เป็น 172.17.0.2:80 ซึ่งเป็นที่อยู่ของคอนเทนเนอร์บนเครือข่าย bridge ส่วนตัวของ Docker หลังจากเขียนใหม่แล้ว แพ็กเก็ตนั้นจะไม่ได้ระบุปลายทางเป็นโฮสต์อีกต่อไป การตัดสินใจเรื่องเส้นทางจึงส่งแพ็กเก็ตนั้นไปตามเส้นทาง FORWARD ซึ่ง Docker ได้เพิ่มกฎที่ยอมรับทราฟฟิกเข้าสู่เครือข่ายของตนเองไว้แล้ว กฎ deny 8080/tcp ของคุณจึงรออยู่ใน INPUT เพื่อรับแพ็กเก็ตที่จะไม่มีวันมาถึง
บน Ubuntu 24.04 คำสั่ง iptables เป็นส่วนติดต่อหน้างานที่ครอบอยู่บน nftables แต่ลำดับของเชนและผลลัพธ์นั้นเหมือนกันทุกประการ ทั้ง UFW และ Docker ต่างเขียนกฎลงในไปป์ไลน์การประมวลผลแพ็กเก็ตของเคอร์เนลชุดเดียวกัน และจุดเริ่มต้นของ Docker นั้นมาก่อน ทั้งหมดนี้ไม่ได้จำกัดอยู่แค่ UFW: firewalld บน VPS ที่ใช้ Rocky หรือ AlmaLinux ก็ทำการกรองที่จุดเดียวกันในไปป์ไลน์นี้ และถูกข้ามผ่านด้วยกฎ DNAT เดียวกัน ดังนั้นวิธีแก้ไขด้านล่างนี้จึงเป็นสิ่งที่คุณต้องนำไปใช้ในกรณีดังกล่าวเช่นกัน
การแก้ไขปัญหาในชีวิตประจำวัน: การเผยแพร่พอร์ตบน 127.0.0.1
คอนเทนเนอร์ส่วนใหญ่ไม่จำเป็นต้องเปิดเผยต่อสาธารณะตั้งแต่แรก ไม่ว่าจะเป็นฐานข้อมูล, แอปเซิร์ฟเวอร์ที่อยู่หลัง reverse proxy, แผงควบคุมผู้ดูแลระบบ หรือ metrics endpoint สิ่งเหล่านี้ไม่ควรตอบสนองต่ออินเทอร์เน็ตโดยตรง ให้เผยแพร่บริการเหล่านี้บน loopback address แทน:
docker run -d --name web -p 127.0.0.1:8080:80 nginx:1.29-alpineหรือในไฟล์ Compose:
services:
web:
image: nginx:1.29-alpine
ports:
- "127.0.0.1:8080:80"วิธีนี้ได้ผลเพราะกฎ DNAT ของ Docker จะจับคู่เฉพาะแพ็กเก็ตที่ระบุปลายทางเป็น 127.0.0.1 เท่านั้น และใน Docker Engine 28.0.0 เป็นต้นไป Docker จะเพิ่มกฎใน chain PREROUTING ของตาราง raw เพื่อดร็อปแพ็กเก็ตใดก็ตามสำหรับพอร์ต loopback นั้นที่เข้ามาผ่านอินเทอร์เฟซอื่นที่ไม่ใช่ loopback ในเวอร์ชันดังกล่าว พอร์ตจะสามารถเข้าถึงได้จากโฮสต์เท่านั้นและไม่สามารถเข้าถึงจากที่อื่นได้ สำหรับเวอร์ชันที่เก่ากว่า 28.0.0 ทาง Docker ระบุว่าโฮสต์ที่อยู่ใน L2 segment เดียวกัน เช่น เครื่องที่อยู่บน network switch เดียวกัน ยังคงสามารถเข้าถึงพอร์ตที่เผยแพร่ไปยัง localhost ได้ ดังนั้นให้ตรวจสอบ docker version และอัปเกรดก่อนที่คุณจะพึ่งพาวิธีนี้ ตรวจสอบการ binding ด้วยคำสั่ง:
sudo ss -tlnp | grep 8080คุณต้องการเห็น 127.0.0.1:8080 ในผลลัพธ์ ไม่ใช่ 0.0.0.0:8080 หรือ [::]:8080 จากนั้นยืนยันจากเครื่องอื่นว่า curl http://your-vps-ip:8080/ ไม่ได้รับการตอบกลับ แพ็กเก็ตที่ส่งไปยังที่อยู่สาธารณะของคุณจะไม่ถูกเขียนใหม่โดย Docker อีกต่อไป จึงไปถึง INPUT ซึ่งนโยบาย default deny ของ UFW และกฎ deny 8080/tcp จากก่อนหน้านี้จะดร็อปแพ็กเก็ตนั้นอย่างเงียบๆ แทนที่จะปฏิเสธ (reject) ส่งผลให้ curl ค้างและจบลงด้วย connection timeout ไม่ใช่ "connection refused"
สำหรับบริการที่ควรเปิดเผยต่ออินเทอร์เน็ต ให้รัน reverse proxy เพียงตัวเดียวที่ถือครองพอร์ต 80 และ 443 และทำหน้าที่กำหนดเส้นทางตาม hostname โดยไม่ต้องเผยแพร่พอร์ตอื่นใด นี่คือรูปแบบที่ คู่มือ Traefik reverse proxy สร้างขึ้น และเป็นวิธีที่แอป self-hosted อย่าง Nextcloud บน VPS จะยังคงไม่สามารถเข้าถึงได้จากภายนอกยกเว้นผ่าน proxy ของมันเอง วิธีการประกาศรายการ ports: และขั้นตอนการทำงานของ Compose ส่วนที่เหลือครอบคลุมอยู่ใน คู่มือพื้นฐาน Docker Compose
เมื่อคอนเทนเนอร์ภายในทุกตัวอยู่บน loopback แล้ว UFW จะกลับมาทำหน้าที่ปกติของมัน คือการปกป้องพอร์ตที่โฮสต์ให้บริการด้วยตัวเอง ให้สร้างชุดกฎเหล่านั้นที่นี่ จากนั้นรันคำสั่งตามลำดับ:
การกรองข้อมูลจริง: chain DOCKER-USER
บางครั้งพอร์ตของคอนเทนเนอร์จำเป็นต้องเปิดให้เครือข่ายเข้าถึงได้ แต่ต้องมีการจำกัดสิทธิ์ เช่น พอร์ตของ database replica ที่อนุญาตให้เข้าถึงได้จากที่อยู่ IP ของสำนักงานแห่งเดียวเท่านั้น สำหรับกรณีนี้ Docker ได้จัดเตรียม chain DOCKER-USER ไว้ให้ แพ็กเก็ตทุกรายการที่มุ่งหน้าไปยังคอนเทนเนอร์ใดก็ตามจะต้องผ่าน DOCKER-USER ก่อนที่จะถึงกฎการยอมรับ (accept rules) ของ Docker เอง chain นี้มีไว้สำหรับกฎของคุณโดยเฉพาะ และ Docker จะไม่ลบหรือแก้ไขกฎเหล่านี้ แม้จะมีการรีสตาร์ท daemon ก็ตาม
chain นี้เป็นส่วนหนึ่งของ firewall backend แบบ iptables เริ่มต้นของ Docker โดย Docker Engine 29 ได้เพิ่ม experimental nftables backend ซึ่งคุณต้องเปิดใช้งานด้วยตัวเลือก daemon firewall-backend และใน backend ดังกล่าวจะไม่มี chain DOCKER-USER อยู่: การกรองข้อมูลแบบกำหนดเองจะไปอยู่ในตาราง nftables แยกต่างหากที่คุณสร้างขึ้นเอง พร้อมกับ base chain บน hook forward หากคุณไม่ได้เปลี่ยนตัวเลือกดังกล่าว แสดงว่าคุณกำลังใช้งาน iptables และเนื้อหาทั้งหมดด้านล่างนี้จะมีผลกับคุณ ความแตกต่างระหว่างเครื่องมือทั้งสองนี้ได้อธิบายไว้ใน คู่มือเปรียบเทียบ iptables กับ nftables
ข้อควรระวังก่อนใช้คำสั่ง: ในขณะที่แพ็กเก็ตเดินทางมาถึง DOCKER-USER กระบวนการ DNAT rewrite ได้เกิดขึ้นเรียบร้อยแล้ว พอร์ตปลายทางของแพ็กเก็ตจะเป็นพอร์ตของคอนเทนเนอร์ (เช่น 80 ในตัวอย่างของเรา) ไม่ใช่พอร์ตที่เปิดเผยออกมา (8080) ดังนั้นกฎที่จับคู่กับ --dport 8080 จึงไม่สามารถจับคู่สิ่งใดได้ วิธีที่เชื่อถือได้คือการจับคู่พอร์ตที่ไคลเอนต์เรียกเข้ามาในตอนแรก ซึ่ง connection tracker ของ kernel จะจดจำข้อมูลนี้ไว้:
sudo iptables -I DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL ! -s 10.0.0.10 -j DROPอ่านกฎนี้ได้ว่า: สำหรับแพ็กเก็ตที่เข้ามาทาง eth0 และเป็นส่วนหนึ่งของการเชื่อมต่อที่มีพอร์ตปลายทางเดิมคือ 8080 ให้ทิ้ง (drop) ทุกอย่างที่ไม่ได้ส่งมาจาก 10.0.0.10 การจับคู่ด้วย --ctdir ORIGINAL จะจำกัดกฎนี้ให้มีผลเฉพาะทิศทางจากไคลเอนต์ไปยังคอนเทนเนอร์เท่านั้น เพื่อไม่ให้แพ็กเก็ตตอบกลับถูกจับโดยไม่ได้ตั้งใจ ให้แทนที่ eth0 ด้วยอินเทอร์เฟซสาธารณะของคุณ ส่วน ip route | grep default คือชื่อของอินเทอร์เฟซนั้น ให้ทดสอบด้วยวิธีเดิม: curl จากที่อยู่ที่ได้รับอนุญาตจะต้องสำเร็จ และจากที่อื่นการเชื่อมต่อจะต้องหมดเวลา (timeout) อาการค้างดังกล่าวเป็นสัญญาณบ่งบอกว่ากฎ DROP กำลังทำงานอยู่ ไม่ใช่พอร์ตที่ไม่มีบริการรองรับ และ ความแตกต่างระหว่างการเชื่อมต่อที่ถูกปฏิเสธกับการเชื่อมต่อที่หมดเวลา คือวิธีที่เร็วที่สุดในการแยกแยะพอร์ตที่ถูกกรองออกจากบริการที่ไม่ได้เปิดใช้งานอยู่
กฎที่เพิ่มด้วยคำสั่ง iptables จะหายไปเมื่อรีบูตเครื่อง เนื่องจาก UFW จัดการ firewall นี้อยู่แล้ว ตำแหน่งที่เหมาะสมที่สุดในการทำให้กฎคงอยู่ถาวรคือ /etc/ufw/after.rules ให้เพิ่มบล็อกคำสั่งไว้ที่ส่วนท้ายของไฟล์:
*filter
:DOCKER-USER - [0:0]
-A DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL ! -s 10.0.0.10 -j DROP
COMMITจากนั้นให้รัน sudo ufw reload ทาง UFW จะเรียกใช้ไฟล์ดังกล่าวซ้ำทุกครั้งที่มีการโหลดใหม่หรือบูตเครื่องใหม่ ดังนั้นการกรองคอนเทนเนอร์ของคุณจะถูกเก็บไว้ในที่เดียวกับส่วนอื่นๆ ของ firewall และจะยังคงอยู่ทั้งหลังการรีบูตและการอัปเกรด Docker
เหตุผลที่คุณไม่ควรปิดการทำงานร่วมกับ iptables ของ Docker
คำตอบเก่าๆ สำหรับปัญหานี้มักแนะนำให้ตั้งค่า { "iptables": false } ใน /etc/docker/daemon.json ซึ่งไม่ควรทำตาม กฎไฟร์วอลล์ของ Docker ทำหน้าที่มากกว่าแค่การเปิดพอร์ต กฎ masquerade คือสิ่งที่ทำให้คอนเทนเนอร์สามารถเข้าถึงอินเทอร์เน็ตภายนอกผ่านที่อยู่ของโฮสต์ได้ ดังนั้นหากปิดการทำงานส่วนนี้ คอนเทนเนอร์จะไม่สามารถเข้าถึง package mirror หรือเรียกใช้ API (application programming interface) ภายนอกใดๆ ได้เลย นอกจากนี้ กฎ DNAT และกฎการแยกเครือข่าย (isolation) จะหายไปด้วย และเอกสารของ Docker ได้เตือนไว้ว่าหากไม่มีกฎอื่นมาทดแทน พอร์ตของคอนเทนเนอร์จะกลายเป็นสิ่งที่โฮสต์อื่นในเครือข่ายท้องถิ่นสามารถเข้าถึงได้ การแก้ไขปัญหาการข้ามผ่านไฟร์วอลล์ด้วยวิธีนี้จะทำให้ระบบเครือข่ายของคอนเทนเนอร์เสียหาย และคุณจะต้องเป็นผู้เขียนและดูแลรักษากฎเหล่านั้นทั้งหมดด้วยตนเอง เอกสารของ Docker ระบุว่าการตั้งค่านี้ไม่เหมาะสมสำหรับผู้ใช้ส่วนใหญ่ และอธิบายว่าการสร้างกฎขึ้นมาใหม่ภายหลังนั้นมีความซับซ้อนอย่างยิ่ง ทั้งนี้ chain DOCKER-USER ถูกสร้างขึ้นมาเพื่อให้ไม่มีใครจำเป็นต้องใช้สวิตช์ตัวนี้อีกต่อไป
ด้าน IPv6 ของปัญหาเดียวกัน
ขั้นแรกให้ตรวจสอบว่าพอร์ตที่เปิดใช้งานมีลักษณะอย่างไรบน IPv6:
sudo ss -tlnp | grep 8080ตั้งแต่ Docker Engine 27 เป็นต้นมา Docker จะจัดการ ip6tables เป็นค่าเริ่มต้น บนเครือข่าย Docker ที่เปิดใช้งาน IPv6 พอร์ตที่ถูกเผยแพร่จะได้รับการจัดการ DNAT ในตาราง IPv6 เช่นเดียวกัน ดังนั้นช่องโหว่ในการข้ามผ่าน (bypass) จึงมีอยู่เช่นกันและสามารถใช้การแก้ไขแบบเดียวกันได้ นั่นคือ chain DOCKER-USER ก็มีอยู่ใน ip6tables เช่นกัน ให้คุณทำซ้ำกฎของคุณด้วย sudo ip6tables -I DOCKER-USER ... และทดสอบจากภายนอกด้วย curl ไปยังที่อยู่ IPv6 สาธารณะของเซิร์ฟเวอร์คุณ ตัวอย่างเช่น curl -6 http://[2001:db8:2a::1]:8080/ สิ่งนี้ใช้ได้กับ backend iptables แบบปกติของ Docker ส่วนใน backend แบบทดลองอย่าง nftables นั้นจะไม่มี chain DOCKER-USER ในทั้งสอง stack และการกรอง IPv6 จะไปอยู่ในตาราง nftables แบบ ip6 ของคุณเองแทน
บนเครือข่ายที่ไม่มี IPv6 ไคลเอนต์ IPv6 จะถูกจัดการโดย docker-proxy ซึ่งเป็นกระบวนการใน user-space ปกติที่คอยฟังที่ [::]:8080 และส่งต่อทราฟฟิกเข้าไปยังคอนเทนเนอร์ผ่าน IPv4 ส่วนทราฟฟิกที่ไปยังโฮสต์โพรเซสจะผ่าน INPUT ดังนั้น UFW จึงสามารถกรองเส้นทางนั้นได้ แต่จะทำได้ก็ต่อเมื่อ UFW กำลังจัดการ IPv6 อยู่เท่านั้น ว่า UFW กำลังจัดการอยู่หรือไม่ และช่องโหว่ IPv6 อื่นๆ ที่อาจเกิดขึ้นบน VPS ได้อย่างไรนั้น เป็นเนื้อหาของ คู่มือ UFW และ IPv6
การเผยแพร่พอร์ตบน loopback จะช่วยหลีกเลี่ยงปัญหาทั้งหมดนี้: -p 127.0.0.1:8080:80 จะผูกกับ IPv4 loopback เท่านั้น จึงไม่มี listener บน IPv6 และบน Docker Engine 28.0.0 เป็นต้นไป จะไม่มีสิ่งใดที่สามารถเข้าถึงได้จากภายนอกในทั้งสอง stack
รูปแบบที่มั่นคง
- เผยแพร่ทุกพอร์ตภายในบน
127.0.0.1เพื่อไม่ให้พอร์ตเหล่านั้นถูกเปิดเผยตั้งแต่แรก บน Docker Engine 28.0.0 หรือใหม่กว่า (รุ่นเก่ากว่าจะยอมให้โฮสต์บน L2 segment เดียวกันเข้าถึงได้) - กำหนดให้ฝั่งสาธารณะเป็นหน้าที่ของ reverse proxy เพียงตัวเดียวที่ถือครองพอร์ต 80 และ 443
- ตั้งค่า UFW ให้ปฏิเสธการเชื่อมต่อขาเข้าทั้งหมดเป็นค่าเริ่มต้นสำหรับโฮสต์ โดยอนุญาตเฉพาะ SSH และพอร์ตของ proxy เท่านั้น
- กรองพอร์ตคอนเทนเนอร์ที่เป็นสาธารณะจริงๆ ใน
DOCKER-USERบน iptables backend เริ่มต้นของ Docker โดยจับคู่กับพอร์ตปลายทางเดิมและบันทึกค่าไว้ใน/etc/ufw/after.rules - เปิดใช้งานการทำงานร่วมกับ iptables ของ Docker ไว้ตามเดิม
เมื่อตั้งค่าเสร็จสิ้นเพียงครั้งเดียว จะช่วยลดปัญหาที่ไม่คาดคิดได้ โดย ufw status จะอธิบายสถานะของโฮสต์ และ DOCKER-USER จะอธิบายสถานะของคอนเทนเนอร์ จะไม่มีพอร์ตใดถูกเผยแพร่ออกไปโดยไม่ได้ตั้งใจ และคำสั่ง docker run -p ถัดไปที่คุณพิมพ์จะเผยแพร่เฉพาะสิ่งที่คุณต้องการอย่างแท้จริง
FAQ
ทำไมฉันถึงเข้าถึง Docker container ได้ทั้งที่ UFW บล็อกพอร์ตนั้นไว้?
เพราะ Docker เผยแพร่พอร์ตด้วยกฎ DNAT ใน chain DOCKER ของตาราง nat ซึ่งเข้าถึงได้จาก PREROUTING โดยกฎนี้จะเขียนปลายทางของแพ็กเก็ตใหม่ไปยังที่อยู่ของ container ก่อนที่จะมีการกรองใดๆ เกิดขึ้น จากนั้นแพ็กเก็ตจะเดินทางผ่านเส้นทาง FORWARD ในขณะที่กฎของ UFW อยู่ใน chain INPUT ซึ่งเป็น chain ที่แพ็กเก็ตไม่เคยผ่านเข้าไป ดังนั้นไฟร์วอลล์จึงไม่ถูกเรียกใช้งาน และกฎการปฏิเสธ (deny rules) จึงไม่มีผลต่อพอร์ตของ container ที่ถูกเผยแพร่ออกมา
ฉันจะทำให้ UFW บล็อกพอร์ตที่ Docker เผยแพร่ออกมาได้อย่างไร?
UFW ไม่สามารถทำได้ด้วยตัวเองเนื่องจากกฎของมันอยู่ใน chain ที่ไม่ถูกต้อง คุณควรหยุดการเปิดเผยพอร์ตโดยเผยแพร่เป็น 127.0.0.1:8080:80 เพื่อให้เฉพาะโฮสต์เท่านั้นที่เข้าถึงได้บน Docker Engine 28.0.0 ขึ้นไป หรือทำการกรองใน chain DOCKER-USER ด้วยกฎ iptables ที่จับคู่กับพอร์ตปลายทางเดิมผ่าน conntrack โดยต้องบันทึกกฎนั้นไว้ใน /etc/ufw/after.rules เพื่อให้คงอยู่หลังการรีบูตและ ufw reload
ฉันควรตั้งค่า "iptables": false ใน daemon.json ของ Docker หรือไม่?
ไม่ควร การตั้งค่าดังกล่าวจะลบกฎไฟร์วอลล์และ NAT ส่วนใหญ่ของ Docker ออก ซึ่งจะทำให้เกิดปัญหามากกว่าแค่เรื่องการข้ามผ่านไฟร์วอลล์ container จะสูญเสียการเข้าถึงอินเทอร์เน็ตขาออกเนื่องจากกฎ masquerade ถูกลบไป และ Docker ได้แจ้งเตือนว่าหากไม่มีกฎทดแทน พอร์ตของ container จะกลายเป็นสิ่งที่โฮสต์บนเครือข่ายท้องถิ่นเข้าถึงได้ ให้ใช้การเผยแพร่ผ่าน loopback และ chain DOCKER-USER แทน ซึ่งจะช่วยแก้ไขปัญหาการเปิดเผยพอร์ตโดยไม่ทำให้ระบบเครือข่ายของ container เสียหาย
Docker ข้ามผ่าน UFW บน IPv6 ด้วยหรือไม่?
บน Docker Engine 27 ขึ้นไป การจัดการ ip6tables จะเปิดใช้งานโดยค่าเริ่มต้น ดังนั้นพอร์ตที่เผยแพร่บนเครือข่าย Docker ที่รองรับ IPv6 จะถูกเขียนเส้นทางใหม่โดยข้ามผ่าน UFW เช่นเดียวกับบน IPv4 และบน backend iptables เริ่มต้นของ Docker จำเป็นต้องใช้กฎ DOCKER-USER เดียวกันโดยทำซ้ำด้วย ip6tables สำหรับบน backend nftables แบบทดลองตั้งแต่ Engine 29 จะไม่มี chain DOCKER-USER ดังนั้นการกรองดังกล่าวจะต้องทำในตาราง nftables ของคุณเอง สำหรับเครือข่ายที่ไม่มี IPv6 กระบวนการ docker-proxy จะฟังบน [::] และทราฟฟิกนั้นจะผ่าน INPUT ซึ่ง UFW สามารถกรองได้หาก UFW จัดการ IPv6 อยู่ การเผยแพร่บน 127.0.0.1 จะช่วยหลีกเลี่ยงทั้งสองกรณีนี้ได้ เพราะจะไม่มีการฟังบน IPv6 เลย