Cara Selesaikan Isu Docker Memintas UFW
Docker memintas UFW kerana peraturan iptables yang ditulis secara automatik. Ketahui mengapa port 8080 tetap terbuka walaupun UFW menetapkan dasar deny dan cara membetulkannya.
Mengapa Docker memintas UFW
Docker memintas UFW kerana port kontena yang diterbitkan tidak pernah melalui peraturan firewall yang diuruskan oleh UFW. Apabila anda menjalankan docker run -p 8080:80, Docker menulis peraturan DNAT (destination network address translation) ke dalam chain PREROUTING pada jadual nat kernel. Peraturan tersebut menulis semula destinasi setiap paket kepada alamat peribadi kontena sebelum kernel menentukan ke mana paket itu akan pergi. Paket yang telah ditulis semula itu kemudiannya dihantar ke dalam kontena melalui chain FORWARD, yang dikawal oleh Docker. Peraturan UFW berada dalam chain INPUT, dan paket tersebut tidak pernah memasukinya. Oleh itu, ufw status menunjukkan penafian lalai (default deny), sudo ufw deny 8080 melaporkan kejayaan, dan port 8080 masih menjawab permintaan daripada seluruh internet.
Ini bukan pepijat Docker, dan UFW tidak rosak. Kedua-dua alat ini memprogramkan firewall kernel yang sama. Peraturan Docker bertindak pada titik yang lebih awal dalam laluan paket, jadi UFW tidak pernah dirujuk. Panduan ini menunjukkan cara pemintasan tersebut berlaku, menjelaskan mekanismenya, dan kemudian membincangkan dua penyelesaian yang berkesan: menerbitkan port pada 127.0.0.1, dan menapis dalam chain DOCKER-USER. Jika UFW merupakan perkara baharu bagi anda, sediakannya dengan panduan asas firewall UFW terlebih dahulu, kerana firewall dengan polisi penafian lalai tetap menjadi asas yang tepat untuk segala perkara lain pada pelayan.
Lihat pintasan pada pelayan anda sendiri
Mulakan daripada VPS yang mempunyai UFW aktif dengan polisi deny lalai untuk trafik masuk. Jalankan bekas web dengan port yang diterbitkan:
sudo ufw status verbose
docker run -d --name web -p 8080:80 nginx:1.29-alpineufw status verbose menunjukkan Default: deny (incoming), allow (outgoing) dan tiada peraturan untuk port 8080. Berdasarkan laporan firewall itu sendiri, port tersebut ditutup. Sekarang, uji daripada mesin yang berbeza, bukan daripada pelayan itu sendiri:
curl -I http://your-vps-ip:8080/HTTP/1.1 200 OKBekas tersebut memberikan respons. Tambahkan peraturan deny secara eksplisit dan uji sekali lagi:
sudo ufw deny 8080/tcpPort tersebut masih memberikan respons kerana peraturan deny terletak dalam rantaian yang tidak dilalui oleh paket. UFW tidak gagal. Ia tidak pernah dirujuk. Inilah sebab mengapa masalah ini sukar dikesan: tiada ralat dipaparkan di mana-mana, proses deploy berjaya, dan output status firewall kelihatan seperti pelayan yang dikunci dengan selamat.
Mekanisme: PREROUTING berjalan sebelum INPUT
Kernel memproses paket masuk mengikut urutan yang tetap, dan keseluruhan masalah berpunca daripada urutan tersebut.
PREROUTINGberjalan dahulu. Peraturan di sini boleh menulis semula destinasi paket, dan peraturan Docker untuk port yang diterbitkan melakukan perkara tersebut.- Keputusan penghalaan (routing) dibuat seterusnya. Paket yang ditujukan kepada hos itu sendiri pergi ke chain
INPUT. Paket yang ditujukan kepada mesin lain pergi ke chainFORWARD. - Peraturan UFW berada dalam
INPUT. Peraturan Docker berada dalamFORWARD.
Lihat peraturan Docker untuk kontena yang baru anda mulakan:
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:80Baris DNAT adalah punca segala-galanya. Mana-mana paket yang tiba untuk port 8080 akan ditulis semula destinasinya kepada 172.17.0.2:80, iaitu alamat kontena pada rangkaian bridge peribadi Docker. Selepas penulisan semula, paket tersebut tidak lagi ditujukan kepada hos, jadi keputusan penghalaan menghantarnya ke laluan FORWARD, di mana Docker telah pun menambah peraturan yang menerima trafik ke dalam rangkaiannya sendiri. Peraturan deny 8080/tcp anda menunggu dalam INPUT untuk paket yang tidak akan sampai.
Pada Ubuntu 24.04, arahan iptables merupakan antaramuka hadapan bagi nftables, tetapi urutan chain dan hasilnya adalah sama. UFW dan Docker kedua-duanya menulis ke dalam pipeline paket kernel yang sama, dan titik masuk Docker adalah lebih awal. Perkara ini tidak khusus untuk UFW sahaja: firewalld pada VPS Rocky atau AlmaLinux menapis pada titik yang sama dalam pipeline tersebut dan dipintas oleh peraturan DNAT yang sama, jadi penyelesaian di bawah adalah perkara yang perlu anda gunakan di sana juga.
Penyelesaian harian: terbitkan port pada 127.0.0.1
Kebanyakan kontena sebenarnya tidak perlu didedahkan kepada umum. Pangkalan data, pelayan aplikasi di sebalik reverse proxy, panel pentadbir, atau titik akhir metrik: tiada satu pun daripada ini harus menjawab permintaan daripada internet secara terus. Terbitkan servis tersebut pada alamat loopback:
docker run -d --name web -p 127.0.0.1:8080:80 nginx:1.29-alpineAtau dalam fail Compose:
services:
web:
image: nginx:1.29-alpine
ports:
- "127.0.0.1:8080:80"Ini berkesan kerana peraturan DNAT Docker kini hanya memadankan paket yang ditujukan kepada 127.0.0.1, dan paket daripada internet tidak mungkin membawa destinasi tersebut secara sah, jadi kernel akan menggugurkannya sebelum sebarang peraturan firewall dijalankan. Port tersebut boleh dicapai daripada hos, dan tidak boleh dicapai oleh peranti lain. Sahkan ikatan (binding) tersebut:
sudo ss -tlnp | grep 8080Anda mahukan 127.0.0.1:8080 dalam output, bukan 0.0.0.0:8080 atau [::]:8080. Kemudian, sahkan daripada mesin lain bahawa curl http://your-vps-ip:8080/ ditolak.
Bagi servis yang perlu menghadap internet, jalankan satu reverse proxy yang menguasai port 80 dan 443 serta menghalakan trafik mengikut nama hos, dan jangan terbitkan port lain. Itulah corak yang dibina oleh panduan reverse proxy Traefik, dan begitulah cara aplikasi self-hosted seperti Nextcloud pada VPS kekal tidak boleh dicapai kecuali melalui proksinya. Cara entri ports: diisytiharkan, serta aliran kerja Compose yang lain, diliputi dalam panduan asas Docker Compose.
Dengan setiap kontena dalaman berada pada loopback, UFW kembali menjalankan tugas asalnya: mengawal port yang disediakan oleh hos itu sendiri. Bina set peraturan tersebut di sini, kemudian jalankan perintah mengikut urutan:
Penapisan sebenar: chain DOCKER-USER
Kadangkala port kontena perlu kekal diterbitkan ke rangkaian tetapi dihadkan, contohnya port replika pangkalan data yang hanya boleh dicapai oleh satu alamat pejabat. Untuk itu, Docker menyediakan chain DOCKER-USER. Setiap paket yang menuju ke mana-mana kontena akan melalui DOCKER-USER sebelum peraturan terima (accept) Docker sendiri, dan Docker tidak pernah menulis peraturan ke dalamnya. Chain ini wujud untuk kegunaan anda, dan Docker tidak akan mengubah kandungannya apabila daemon dimulakan semula.
Satu perangkap sebelum menjalankan arahan: apabila paket sampai ke DOCKER-USER, penulisan semula DNAT telah pun berlaku. Port destinasi paket tersebut ialah port kontena (80 dalam contoh kita), bukan port yang diterbitkan (8080). Oleh itu, peraturan yang memadankan --dport 8080 tidak akan memadankan apa-apa. Cara yang boleh dipercayai adalah dengan memadankan port yang didail oleh klien pada asalnya, yang diingati oleh penjejak sambungan (connection tracker) kernel:
sudo iptables -I DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL ! -s 10.0.0.10 -j DROPBaca ia sebagai: untuk paket yang masuk melalui eth0 dan tergolong dalam sambungan yang port destinasi asalnya ialah 8080, gugurkan (drop) semua yang tidak dihantar dari 10.0.0.10. Padanan --ctdir ORIGINAL mengehadkan peraturan tersebut kepada arah klien-ke-kontena, supaya paket balasan tidak tersilap ditangkap. Gantikan eth0 dengan antara muka awam anda; ip route | grep default menamakannya. Uji dengan cara yang sama seperti sebelum ini: curl dari alamat yang dibenarkan akan berjaya, dan dari mana-mana tempat lain sambungan akan tamat masa (time out). Keadaan tergantung itu adalah tanda bahawa peraturan DROP sedang menjalankan tugasnya, bukannya port yang tiada servis di belakangnya, dan perbezaan antara sambungan yang ditolak dan yang tamat masa ialah cara terpantas untuk membezakan port yang ditapis daripada servis yang sekadar tidak mendengar.
Peraturan yang ditambah dengan arahan iptables akan hilang selepas but semula. Memandangkan UFW sudah menguruskan firewall ini, tempat yang bersih untuk mengekalkannya ialah /etc/ufw/after.rules. Tambahkan blok di hujung fail tersebut:
*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
COMMITKemudian jalankan sudo ufw reload. UFW akan memainkan semula fail tersebut pada setiap muat semula dan setiap but, jadi penapisan kontena anda kini berada di tempat yang sama dengan bahagian lain firewall anda, dan ia kekal wujud selepas but semula serta naik taraf Docker.
Mengapa anda tidak patut menyahdayakan integrasi iptables Docker
Jawapan lama bagi masalah ini mencadangkan penetapan { "iptables": false } dalam /etc/docker/daemon.json. Jangan lakukannya. Peraturan firewall Docker melakukan lebih daripada sekadar menerbitkan port. Peraturan masquerade ialah perkara yang memberikan akses internet keluar kepada kontena melalui alamat hos, jadi apabila integrasi dimatikan, kontena tidak boleh menarik imej, mencapai cermin pakej, atau memanggil mana-mana API (application programming interface) luaran. Peraturan DNAT ialah perkara yang membolehkan -p berfungsi, jadi port yang diterbitkan akan berhenti berfungsi sepenuhnya. Peraturan pengasingan yang memisahkan rangkaian Compose yang berbeza juga akan hilang. Anda akan membaiki pintasan tersebut dengan merosakkan rangkaian kontena, dan setiap peraturan tersebut akan menjadi tanggungjawab anda untuk ditulis dan diselenggara secara manual. Dokumentasi Docker sendiri menyifatkan tetapan tersebut sebagai tetapan untuk mereka yang berhasrat melakukan perkara itu secara khusus. Rantaian DOCKER-USER wujud tepat supaya tiada sesiapa pun memerlukan suis ini.
Sisi IPv6 bagi masalah yang sama
Periksa dahulu rupa port yang diterbitkan pada IPv6:
sudo ss -tlnp | grep 8080Sejak Docker Engine 27, Docker menguruskan ip6tables secara lalai. Pada rangkaian Docker yang mendayakan IPv6, port yang diterbitkan mendapat layanan DNAT yang sama dalam jadual IPv6, jadi pintasan yang sama wujud di sana dan penyelesaian yang sama terpakai: rantaian DOCKER-USER juga wujud dalam ip6tables, jadi cerminkan peraturan anda dengan sudo ip6tables -I DOCKER-USER ... dan uji dari luar menggunakan curl terhadap alamat IPv6 awam pelayan anda, contohnya curl -6 http://[2001:db8:2a::1]:8080/.
Pada rangkaian tanpa IPv6, klien IPv6 sebaliknya dikendalikan oleh docker-proxy, satu proses ruang pengguna biasa yang mendengar pada [::]:8080 dan memajukan trafik ke dalam kontena melalui IPv4. Trafik ke proses hos memang melalui INPUT, jadi UFW boleh menapis laluan tersebut, tetapi hanya apabila UFW menguruskan IPv6 sepenuhnya. Sama ada ia sedang melakukannya, dan cara lain jurang IPv6 terbuka pada VPS, adalah subjek bagi panduan UFW dan IPv6.
Menerbitkan pada loopback memintas keseluruhan persoalan: -p 127.0.0.1:8080:80 hanya mengikat loopback IPv4, jadi tiada pendengar IPv6 dan tiada apa-apa yang boleh dicapai dari luar pada mana-mana tindanan.
Corak yang mengekalkan kestabilan
- Terbitkan setiap port dalaman pada
127.0.0.1, supaya ia tidak terdedah sejak awal lagi. - Berikan bahagian awam kepada satu reverse proxy yang menguasai port 80 dan 443.
- Kekalkan UFW default deny untuk hos, dengan membenarkan SSH dan port proksi sahaja.
- Tapis port kontena yang benar-benar awam dalam
DOCKER-USER, dipadankan pada port destinasi asal, dan dikekalkan dalam/etc/ufw/after.rules. - Biarkan integrasi iptables Docker dihidupkan.
Setelah disediakan sekali, ini menghilangkan kejutan: ufw status menerangkan hos, dan DOCKER-USER menerangkan kontena. Tiada apa-apa yang diterbitkan secara tidak sengaja, dan docker run -p seterusnya yang anda taip akan mendedahkan dengan tepat apa yang anda maksudkan.
FAQ
Mengapa saya boleh mencapai container Docker saya sedangkan UFW menyekat port tersebut?
Kerana Docker menerbitkan port dengan peraturan DNAT dalam chain PREROUTING, yang menulis semula destinasi paket kepada alamat container sebelum sebarang penapisan berlaku. Paket tersebut kemudian melalui laluan FORWARD, manakala peraturan UFW berada dalam INPUT, iaitu chain yang tidak dilalui oleh paket tersebut. Firewall tidak dirujuk, jadi peraturan deny tidak memberi kesan kepada port container yang diterbitkan.
Bagaimanakah cara untuk membuat UFW menyekat port yang diterbitkan oleh Docker?
UFW sendiri tidak boleh melakukannya kerana peraturannya berada dalam chain yang salah. Sama ada berhenti mendedahkan port tersebut dengan menerbitkannya sebagai 127.0.0.1:8080:80 supaya hanya host yang boleh mencapainya, atau tapis dalam chain DOCKER-USER menggunakan peraturan iptables yang memadankan port destinasi asal melalui conntrack. Kekalkan peraturan tersebut dalam /etc/ufw/after.rules supaya ia bertahan selepas but semula dan ufw reload.
Patutkah saya menetapkan "iptables": false dalam daemon.json milik Docker?
Tidak. Tetapan itu membuang semua peraturan firewall dan NAT Docker, yang menyebabkan kerosakan lebih besar daripada sekadar pintasan tersebut. Container akan kehilangan akses internet keluar kerana peraturan masquerade telah tiada, dan port yang diterbitkan berhenti berfungsi kerana peraturan DNAT telah tiada. Gunakan penerbitan loopback dan chain DOCKER-USER sebaliknya; ia membetulkan pendedahan tersebut tanpa merosakkan rangkaian container.
Adakah Docker memintas UFW pada IPv6 juga?
Pada Docker Engine 27 dan versi lebih baharu, pengurusan ip6tables diaktifkan secara lalai, jadi port yang diterbitkan pada rangkaian Docker yang mempunyai IPv6 akan ditulis semula untuk memintas UFW sama seperti pada IPv4, dan memerlukan peraturan DOCKER-USER yang sama yang dicerminkan dengan ip6tables. Pada rangkaian tanpa IPv6, proses docker-proxy mendengar pada [::] dan trafik tersebut memang melalui INPUT, di mana UFW boleh menapisnya jika UFW menguruskan IPv6. Menerbitkan pada 127.0.0.1 mengelakkan kedua-dua kes tersebut, kerana tiada apa-apa yang mendengar pada IPv6 sama sekali.